完善 任务列表页面

This commit is contained in:
小肥羊 2025-11-04 18:45:19 +08:00
parent 963448382d
commit 24502a526d
21 changed files with 670 additions and 296 deletions

View File

@ -6,63 +6,63 @@ using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using VideoAnalysisCore.Common; using VideoAnalysisCore.Common;
namespace Learn.Archives.API.Expand namespace Learn.VideoAnalysis.Expand
{ {
public static class AuthorizeExpand public static class AuthorizeExpand
{ {
public static IServiceCollection AddPermissionAuthentication(this IServiceCollection services) public static IServiceCollection AddPermissionAuthentication(this IServiceCollection services)
{ {
services.AddAuthentication() services.AddAuthentication()
.AddJwtBearer(Authentication.vdAdmin, options => .AddJwtBearer(Authentication.vdAdmin, options =>
{ {
options.RequireHttpsMetadata = false; options.RequireHttpsMetadata = false;
options.UseSecurityTokenValidators = true; options.UseSecurityTokenValidators = true;
options.MapInboundClaims = false; // .NET 5+ options.MapInboundClaims = false; // .NET 5+
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
options.TokenValidationParameters = new TokenValidationParameters options.TokenValidationParameters = new TokenValidationParameters
{ {
SaveSigninToken = false,//保存token,后台验证token是否生效(重要) SaveSigninToken = false,//保存token,后台验证token是否生效(重要)
RequireExpirationTime = true, // 设置请求需要携带accesstoken的过期时间 RequireExpirationTime = true, // 设置请求需要携带accesstoken的过期时间
ValidateIssuer = false,//必须验证签发人 ValidateIssuer = false,//必须验证签发人
ValidateAudience = false,//验证受众 ValidateAudience = false,//验证受众
ValidateLifetime = true,//是否验证Token有效期 ValidateLifetime = true,//是否验证Token有效期
ValidateIssuerSigningKey = true,//是否验证签名,不验证 会被篡改数据,不安全 ValidateIssuerSigningKey = true,//是否验证签名,不验证 会被篡改数据,不安全
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AppCommon.Config.AuthKey.Secret)),//解密的密钥 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AppCommon.Config.AuthKey.Secret)),//解密的密钥
}; };
options.Events = new JwtBearerEvents options.Events = new JwtBearerEvents
{ {
OnMessageReceived = context => OnMessageReceived = context =>
{ {
var token = context.Request.Headers["Authorization"].FirstOrDefault(); var token = context.Request.Headers["Authorization"].FirstOrDefault();
// 3. 安全提取令牌 // 3. 安全提取令牌
if (!string.IsNullOrEmpty(token) && token.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) if (!string.IsNullOrEmpty(token) && token.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{ {
// 移除"Bearer "前缀并清除两端空格 // 移除"Bearer "前缀并清除两端空格
token = token.Substring("Bearer ".Length).Trim(); token = token.Substring("Bearer ".Length).Trim();
context.Token = token; context.Token = token;
} }
return Task.CompletedTask; return Task.CompletedTask;
}, },
OnAuthenticationFailed = context => OnAuthenticationFailed = context =>
{ {
context.Response.StatusCode = 403; context.Response.StatusCode = 403;
return Task.CompletedTask; return Task.CompletedTask;
}, },
OnChallenge = context => OnChallenge = context =>
{ {
context.HandleResponse(); context.HandleResponse();
if (context.Response.StatusCode == 403) if (context.Response.StatusCode == 403)
return Task.CompletedTask; return Task.CompletedTask;
context.Response.Clear(); context.Response.Clear();
context.Response.ContentType = "application/json"; context.Response.ContentType = "application/json";
context.Response.StatusCode = 401; context.Response.StatusCode = 401;
var data = new { Code = 401, Message = context.Error + context.AuthenticateFailure?.Message }; var data = new { Code = 401, Message = context.Error + context.AuthenticateFailure?.Message };
context.Response.WriteAsync(data.ToJson()); context.Response.WriteAsync(data.ToJson());
return Task.CompletedTask; return Task.CompletedTask;
} }
}; };
}); });
return services; return services;
} }

View File

@ -0,0 +1,106 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace Learn.VideoAnalysis.Expand
{
public static class SwaggerExpand
{
public static void AddSwaggerExpand(this IServiceCollection s, string name = "")
{
s.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Description = name
});
c.OperationFilter<SwaggerFileUploadFilter>();
//按Http类型排序
c.OrderActionsBy(o => o.GroupName);
c.DocInclusionPredicate((docName, apiDesc) =>
{
try
{
if (!apiDesc.TryGetMethodInfo(out MethodInfo methodInfo)) return false;
var versions = methodInfo.DeclaringType.GetCustomAttributes(true)
.OfType<ApiExplorerSettingsAttribute>().Select(attr => attr.GroupName);
if (docName.ToLower() == "v1" && versions.FirstOrDefault() == null)
return true;
return versions.Any(v => v.ToString() == docName);
}
catch (Exception ex)
{
throw;
}
});
//添加全局安全性需求
c.AddSecurityRequirement(new OpenApiSecurityRequirement{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "bearerAuth"
}
}, Array.Empty<string>()
}
});
//添加一个必须的全局安全信息和AddSecurityDefinition方法指定的方案名称要一致这里是Bearer。
c.AddSecurityDefinition("bearerAuth",
new OpenApiSecurityScheme
{
Description = "使用JWT授权头。示例:\"Authorization: Bearer {token}\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
//内容为以 bearer开头
Scheme = "bearer",
BearerFormat = "JWT"
});
DirectoryInfo dirs = new DirectoryInfo(AppContext.BaseDirectory);
FileInfo[] files = dirs.GetFiles("*.xml");
foreach (var path in files)
{
c.IncludeXmlComments(path.FullName);
}
});
//s.AddSwaggerGenNewtonsoftSupport();
}
}
class SwaggerFileUploadFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (context.ApiDescription.ActionDescriptor.Parameters.Any(w => w.ParameterType == typeof(IFormCollection)))
{
Dictionary<string, OpenApiSchema> schema = new Dictionary<string, OpenApiSchema>();
schema["fileName"] = new OpenApiSchema { Description = "选择上传文件", Type = "string", Format = "binary" };
Dictionary<string, OpenApiMediaType> content = new Dictionary<string, OpenApiMediaType>();
content["multipart/form-data"] = new OpenApiMediaType { Schema = new OpenApiSchema { Type = "object", Properties = schema } };
operation.RequestBody = new OpenApiRequestBody() { Content = content };
}
}
}
}

View File

@ -15,6 +15,7 @@ using System.Diagnostics;
using VideoAnalysisCore.AICore.FFMPGE; using VideoAnalysisCore.AICore.FFMPGE;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Unicode; using System.Text.Unicode;
using System.Text.Json;
@ -26,11 +27,6 @@ namespace Learn.VideoAnalysis
{ {
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
//.AddInteractiveWebAssemblyComponents();
builder.Services.AddLogging(loggingBuilder => builder.Services.AddLogging(loggingBuilder =>
{ {
@ -39,33 +35,12 @@ namespace Learn.VideoAnalysis
loggingBuilder.SetMinimumLevel(LogLevel.Warning); // 设置最小日志级别为 Warning loggingBuilder.SetMinimumLevel(LogLevel.Warning); // 设置最小日志级别为 Warning
}); });
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Learn.VideoAnalysis",
Version = "v1",
Description = "教学视频分析平台v1"
});
var file = Path.Combine(AppContext.BaseDirectory, "Learn.VideoAnalysis.xml"); // xml文档绝对路径
var file1 = Path.Combine(AppContext.BaseDirectory, "VideoAnalysisCore.xml"); // xml文档绝对路径
c.IncludeXmlComments(file, true); // true : 显示控制器层注释
c.IncludeXmlComments(file1, true); // true : 显示控制器层注释
c.OrderActionsBy(o => o.RelativePath); // 对action的名称进行排序如果有多个就可以看见效果了。
});
//绑定 appsetting 配置
//初始化 插件 //初始化 插件
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerExpand("AI视频分析");
//绑定 appsetting 配置
builder.Configuration.AddAppConfig(args); builder.Configuration.AddAppConfig(args);
builder.Services.AddPermissionAuthentication();
builder.Services.AddSqlSugarExpand(); builder.Services.AddSqlSugarExpand();
builder.Services.AddRedisExpand(); builder.Services.AddRedisExpand();
@ -84,6 +59,13 @@ namespace Learn.VideoAnalysis
{ {
options.Filters.Add(typeof(ExceptionFilter)); options.Filters.Add(typeof(ExceptionFilter));
}); });
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);//中文转换时不使用Unicode
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;// 默认小驼峰 null 大驼峰
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
});
builder.Services.AddScoped(sp => builder.Services.AddScoped(sp =>
@ -115,11 +97,8 @@ namespace Learn.VideoAnalysis
var app = builder.Build(); var app = builder.Build();
AppCommon.Services = app.Services; AppCommon.Services = app.Services;
app.UseMiddleware<BasicAuthMiddleware>("Swagger"); app.UseMiddleware<BasicAuthMiddleware>("Swagger");
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
_ = app.Services.GetRequiredService<RedisInit>(); _ = app.Services.GetRequiredService<RedisInit>();
app.UseSwagger(); app.UseSwagger();
app.UseSwaggerUI(); app.UseSwaggerUI();

View File

@ -8,7 +8,7 @@ VITE_PUBLIC_PATH = /
VITE_ROUTER_HISTORY = "hash" VITE_ROUTER_HISTORY = "hash"
# 接口地址 # 接口地址
VITE_API_BASEURL = "http://192.168.2.33:5238/api" VITE_API_BASEURL = "http://192.168.2.33:5238"
# # 接口地址 # # 接口地址

View File

@ -8,7 +8,7 @@ import type { Res } from "@/utils/http/types";
* @return {object} * @return {object}
*/ */
export function getenum(type) { export function getenum(type) {
return http.request<ComboModel[]>("get", `Public/enum/${type}`); return http.request<ComboModel[]>("get", `api/Public/enum/${type}`);
} }
/** /**
* @description * @description
@ -16,5 +16,5 @@ export function getenum(type) {
* @return {object} * @return {object}
*/ */
export function getenumDic(type) { export function getenumDic(type) {
return http.request<any>("get", `Public/enum/${type}/Dic`); return http.request<any>("get", `api/Public/enum/${type}/Dic`);
} }

View File

@ -9,23 +9,27 @@ export class hTableAPI {
this.url = url; this.url = url;
} }
PageList(data = {}) { PageList(data = {}) {
return http.request<Res<any>>("post", `${this.url}/PageList`, { data }); return http.request<Res<any>>("post", `api/${this.url}/PageList`, { data });
} }
Info(tag) { Info(tag) {
const pUrl = `${this.url}/${tag}`; const pUrl = `api/${this.url}/${tag}`;
let getUrl = pUrl; let getUrl = pUrl;
return http.request<Res<any>>("get", getUrl); return http.request<Res<any>>("get", getUrl);
} }
edit(data) { edit(data) {
return http.request<Res<any>>("post", `${this.url}/Edit`, { data }); return http.request<Res<any>>("post", `api/${this.url}/Edit`, { data });
} }
delete(data) { delete(data) {
return http.request<Res<any>>("post", `${this.url}/Del`, { data }); return http.request<Res<any>>("post", `api/${this.url}/Del`, { data });
} }
querycombo(data = {}) { querycombo(data = {}) {
return http.request<Res<ComboModel[]>>("post", `${this.url}/QueryCombo`, { return http.request<Res<ComboModel[]>>(
data "post",
}); `api/${this.url}/QueryCombo`,
{
data
}
);
} }
} }

View File

@ -34,7 +34,7 @@ export type RefreshTokenResult = {
/** 登录 */ /** 登录 */
export const getLogin = (data?: object) => { export const getLogin = (data?: object) => {
return http.request<UserResult>("post", "/api/Public/Login", { data }); return http.request<UserResult>("post", "/api/Admin/Login", { data });
}; };
/** 刷新`token` */ /** 刷新`token` */

View File

@ -37,22 +37,22 @@ export interface RowRloadResult {
/** 刷新任务实时数据 */ /** 刷新任务实时数据 */
export const RowRload = (id: any) => { export const RowRload = (id: any) => {
return http.request<RowRloadResult>("post", "/api/VideoTask/RowRload", { return http.request<RowRloadResult>("get", "/api/VideoTask/RowRload", {
params: id params: { id }
}); });
}; };
/** 重试任务 */ /** 重试任务 */
export const ReStart = (id: any, selectEnum: number) => { export const ReStart = (id: any, selectEnum: number) => {
return http.request<any>("post", "/api/VideoTask/ReStart", { return http.request<any>("get", "/api/VideoTask/ReStart", {
params: { id, selectEnum } params: { id, selectEnum }
}); });
}; };
/** 重试任务 */ /** 展示数据 */
export const ShowTaskInfo = (id: any) => { export const ShowTaskInfo = (id: any) => {
return http.request<ShowTaskInfoRes>("post", "/api/VideoTask/ShowTaskInfo", { return http.request<ShowTaskInfoRes>("get", "/api/VideoTask/ShowTaskInfo", {
params: { id } params: { id }
}); });
}; };

View File

@ -210,7 +210,7 @@ function handleDelete(obj, row) {
ElMessageBox.confirm("此操作将永久删除勾选记录, 是否继续?") ElMessageBox.confirm("此操作将永久删除勾选记录, 是否继续?")
.then(() => { .then(() => {
Api.delete(ids).then((res) => { Api.delete(ids).then((res) => {
if (res.code === 200) { if (true) {
handleReloadPaged(); handleReloadPaged();
ElMessage.success("删除成功"); ElMessage.success("删除成功");
} else { } else {
@ -371,8 +371,8 @@ function fetchPagedData() {
} }
} }
Api.PageList(table.value.search).then((res) => { Api.PageList(table.value.search).then((res) => {
if (res.code === 200) { if (true) {
table.value.data = res.data.data.map((s, i) => { table.value.data = res.data.map((s, i) => {
return { ...s, customId: i }; return { ...s, customId: i };
}); });
table.value.pageData = res.data; table.value.pageData = res.data;

View File

@ -208,6 +208,9 @@ class PureHttp {
router.push({ router.push({
path: "/error/403" path: "/error/403"
}); });
} else if (error.response?.status !== 200) {
ElMessage.warning("请求失败" + $error.message + " ");
console.log("请求失败" ,$error);
} }
// 所有的响应异常 区分来源为取消请求/非取消请求 // 所有的响应异常 区分来源为取消请求/非取消请求
return Promise.reject($error); return Promise.reject($error);
@ -234,11 +237,7 @@ class PureHttp {
PureHttp.axiosInstance PureHttp.axiosInstance
.request(config) .request(config)
.then((response: any) => { .then((response: any) => {
debugger resolve(response);
if (response.code != null && response.code !== 200) {
message(response.message, { type: "error" });
} resolve(response);
}) })
.catch(error => { .catch(error => {
if (error.status != 200) { if (error.status != 200) {

View File

@ -0,0 +1,170 @@
<script setup lang="ts">
import Motion from "./utils/motion";
import { useRouter } from "vue-router";
import { message } from "@/utils/message";
import { loginRules } from "./utils/rule";
import { ref, reactive, toRaw } from "vue";
import { debounce } from "@pureadmin/utils";
import { useNav } from "@/layout/hooks/useNav";
import { useEventListener } from "@vueuse/core";
import type { FormInstance } from "element-plus";
import { useLayout } from "@/layout/hooks/useLayout";
import { useUserStoreHook } from "@/store/modules/user";
import { initRouter, getTopMenu } from "@/router/utils";
import { bg, avatar, illustration } from "./utils/static";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import dayIcon from "@/assets/svg/day.svg?component";
import darkIcon from "@/assets/svg/dark.svg?component";
import Lock from "~icons/ri/lock-fill";
import User from "~icons/ri/user-3-fill";
defineOptions({
name: "Login",
});
const router = useRouter();
const loading = ref(false);
const disabled = ref(false);
const ruleFormRef = ref<FormInstance>();
const { initStorage } = useLayout();
initStorage();
const { dataTheme, overallStyle, dataThemeChange } = useDataThemeChange();
dataThemeChange(overallStyle.value);
const { title } = useNav();
const ruleForm = reactive({
account: "",
password: "",
});
const onLogin = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (valid) {
loading.value = true;
useUserStoreHook()
.loginByUsername({
account: ruleForm.account,
password: ruleForm.password,
})
.then((res) => {
//
return initRouter().then(() => {
disabled.value = true;
router
.push(getTopMenu(true).path)
.then(() => {
message("登录成功", { type: "success" });
})
.finally(() => (disabled.value = false));
});
})
.finally(() => (loading.value = false));
}
});
};
const immediateDebounce: any = debounce((formRef) => onLogin(formRef), 1000, true);
useEventListener(document, "keydown", ({ code }) => {
if (["Enter", "NumpadEnter"].includes(code) && !disabled.value && !loading.value)
immediateDebounce(ruleFormRef.value);
});
</script>
<template>
<div class="select-none">
<img :src="bg" class="wave" />
<div class="flex-c absolute right-5 top-3">
<!-- 主题 -->
<el-switch
v-model="dataTheme"
inline-prompt
:active-icon="dayIcon"
:inactive-icon="darkIcon"
@change="dataThemeChange"
/>
</div>
<div class="login-container">
<div class="img">
<component :is="toRaw(illustration)" />
</div>
<div class="login-box">
<div class="login-form">
<avatar class="avatar" />
<Motion>
<h2 class="outline-hidden">{{ title }}</h2>
</Motion>
<!-- <el-form
ref="ruleFormRef"
:model="ruleForm"
:rules="loginRules"
size="large"
> -->
<el-form ref="ruleFormRef" :model="ruleForm" size="large">
<Motion :delay="100">
<el-form-item
:rules="[
{
required: true,
message: '请输入账号',
trigger: 'blur',
},
]"
prop="account"
>
<el-input
v-model="ruleForm.account"
clearable
placeholder="账号"
:prefix-icon="useRenderIcon(User)"
/>
</el-form-item>
</Motion>
<Motion :delay="150">
<el-form-item prop="password">
<el-input
v-model="ruleForm.password"
clearable
show-password
placeholder="密码"
:prefix-icon="useRenderIcon(Lock)"
/>
</el-form-item>
</Motion>
<Motion :delay="250">
<el-button
class="w-full mt-4!"
size="default"
type="primary"
:loading="loading"
:disabled="disabled"
@click="onLogin(ruleFormRef)"
>
登录
</el-button>
</Motion>
</el-form>
</div>
</div>
</div>
</div>
</template>
<style scoped>
@import url("@/style/login.css");
</style>
<style lang="scss" scoped>
:deep(.el-input-group__append, .el-input-group__prepend) {
padding: 0;
}
</style>

View File

@ -0,0 +1,40 @@
import { h, defineComponent, withDirectives, resolveDirective } from "vue";
/** 封装@vueuse/motion动画库中的自定义指令v-motion */
export default defineComponent({
name: "Motion",
props: {
delay: {
type: Number,
default: 50
}
},
render() {
const { delay } = this;
const motion = resolveDirective("motion");
return withDirectives(
h(
"div",
{},
{
default: () => [this.$slots.default()]
}
),
[
[
motion,
{
initial: { opacity: 0, y: 100 },
enter: {
opacity: 1,
y: 0,
transition: {
delay
}
}
}
]
]
);
}
});

View File

@ -0,0 +1,28 @@
import { reactive } from "vue";
import type { FormRules } from "element-plus";
/** 密码正则密码格式应为8-18位数字、字母、符号的任意两种组合 */
export const REGEXP_PWD =
/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/;
/** 登录校验 */
const loginRules = reactive<FormRules>({
password: [
{
validator: (rule, value, callback) => {
if (value === "") {
callback(new Error("请输入密码"));
} else if (!REGEXP_PWD.test(value)) {
callback(
new Error("密码格式应为8-18位数字、字母、符号的任意两种组合")
);
} else {
callback();
}
},
trigger: "blur"
}
]
});
export { loginRules };

View File

@ -0,0 +1,5 @@
import bg from "@/assets/login/bg.png";
import avatar from "@/assets/login/avatar.svg?component";
import illustration from "@/assets/login/illustration.svg?component";
export { bg, avatar, illustration };

View File

@ -8,17 +8,19 @@ import {
TableConfig, TableConfig,
} from "@/components/hTable/hTable"; } from "@/components/hTable/hTable";
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import { fa } from "element-plus/es/locales.mjs"; import { de, fa } from "element-plus/es/locales.mjs";
import { hTableAPI } from "@/api/hTable"; import { hTableAPI } from "@/api/hTable";
import { getenum } from "@/api/enum"; import { getenum } from "@/api/enum";
import { ruleRequired, ruleRequiredNumber } from "@/utils/rules"; import { ruleRequired, ruleRequiredNumber } from "@/utils/rules";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { RowRload } from "@/api/videoTask"; import { ReStart, RowRload } from "@/api/videoTask";
import { Refresh } from "@element-plus/icons-vue"; import { Refresh } from "@element-plus/icons-vue";
const ControllerName = "ExamClassInfo"; import { message } from "@/utils/message";
import { json } from "stream/consumers";
const ControllerName = "VideoTask";
defineOptions({ defineOptions({
name: "ClassExam", name: ControllerName,
}); });
const props = defineProps<{ const props = defineProps<{
@ -37,48 +39,50 @@ const tableData: TableConfig = intTableData({
search: { search: {
// //
show: true, show: true,
PageSize: 999, PageSize: 10,
}, },
operationColumn: true, // operationColumn: true, //
operationColumnData: [ operationColumnData: [],
{
topBtn: false, //
show: true,
label: "详情",
btnType: "custom",
btnStyle: "primary",
custom: {
title: "考试班级详情", // title
src: "exam/classExamRecord", //
width: "1600px", //
height: "800px", //
},
},
],
column: { column: {
// //
schoolName: { id: {
label: "学校", label: "ID",
width: "180px", width: "140px",
search: new TableColumnSearch(true, ConditionalType.Like),
},
grade: {
label: "年级",
width: "120px",
custom: (row) => row.gradeLevel + row.gradeYear + "届",
search: new TableColumnSearch(true), search: new TableColumnSearch(true),
}, },
className: { tagId: {
label: "班级", label: "标签ID",
width: "80px", width: "140px",
search: new TableColumnSearch(true, ConditionalType.Like), search: new TableColumnSearch(true),
}, },
examName: { videoType: {
label: "最近考试", label: "任务类型",
width: "100px",
type: "dropdown",
search: new TableColumnSearch(true),
},
lastEnum: {
label: "最后状态",
width: "200px", width: "200px",
type: "dropdown",
search: new TableColumnSearch(true),
}, },
peopleCount: { subject: {
label: "参考人数", label: "学科",
width: "100px",
type: "dropdown",
search: new TableColumnSearch(true),
},
comeFrom: {
label: "IP",
width: "120px",
},
mediaUrl: {
label: "媒体地址",
width: "300px",
},
createTime: {
label: "创建时间",
}, },
}, },
data: [], data: [],
@ -92,35 +96,67 @@ const dialogRef = ref({
value: null as any, value: null as any,
data: Object as any, data: Object as any,
}); });
let subjectEnum = ref<ComboModel[]>([]); let redisChannelEnum = ref<ComboModel[]>([]);
const showTable = ref(false); const showTable = ref(false);
onMounted(async () => { onMounted(async () => {
// //
subjectEnum.value = await getenum("SubjectEnum"); tableData.column.videoType.setting.datasource = await getenum("AttachmentsInfoType");
tableData.column.lastEnum.setting.datasource = await getenum("RedisChannelEnum");
tableData.column.subject.setting.datasource = await getenum("SubjectEnum");
redisChannelEnum.value = tableData.column.lastEnum.setting.datasource;
showTable.value = true; showTable.value = true;
}); });
async function showDialog(row) {
dialogRef.value.data = row;
dialogRef.value.value = row.lastEnum;
dialogRef.value.visible = true;
}
async function submitRowRload() {
await ReStart(dialogRef.value.data.id, dialogRef.value.value);
dialogRef.value.visible = false;
message("重试任务", { type: "success" });
}
async function expandChange(row: any, expandedRows: any[]) { async function expandChange(row: any, expandedRows: any[]) {
let res = await RowRload(row.id); if (expandedRows.find((s) => s == row)) RloadTaskInfo(row);
row.TaskInfo = res;
} }
async function RloadTaskInfo(row: any) { async function RloadTaskInfo(row: any) {
let res = await RowRload(row.id); let res = await RowRload(row.id);
row.TaskInfo = res; row.TaskInfo = res;
row.TaskInfo.active = stepData.value.findIndex((s) => s.value == row.TaskInfo.lastEnum); row.TaskInfo.stepData = JSON.parse(JSON.stringify(stepData.value));
for (const element of stepData.value) { row.TaskInfo.active = row.TaskInfo.stepData.findIndex(
element.time = row.TaskInfo.startTime[element.title]; (s) => s.title == row.TaskInfo.lastEnum
if (element.value < row.TaskInfo.lastEnum) { );
element.status = "finish"; if (row.TaskInfo.startTime != null) {
} else if (element.value == 60) { for (const element of row.TaskInfo.stepData) {
element.status = "success"; element.time = formatDateToChinese(
} else if (element.value == row.TaskInfo.lastEnum) { row.TaskInfo.startTime[element.title.toLowerCase()]
element.status = "process"; );
} else { if (element.value < row.TaskInfo.lastEnum) {
element.status = "wait"; element.status = "finish";
} else if (element.value == 60) {
element.status = "success";
} else if (element.value == row.TaskInfo.lastEnum) {
element.status = "process";
} else {
element.status = "wait";
}
} }
} }
} }
function formatDateToChinese(dateString) {
const date = new Date(dateString);
if (isNaN(date.getTime())) return "";
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
interface StepData { interface StepData {
status: "" | "wait" | "process" | "finish" | "error" | "success"; status: "" | "wait" | "process" | "finish" | "error" | "success";
time: string | null; time: string | null;
@ -144,49 +180,59 @@ const stepData = ref<StepData[]>([
<template #expandSlot="{ props }"> <template #expandSlot="{ props }">
<!-- 拓展内容 --> <!-- 拓展内容 -->
<div class="expanded-content expandSlot"> <div class="expanded-content expandSlot">
<el-descriptions title="任务详情" :column="2" border> <h3>任务详情</h3>
<el-descriptions-item label="操作"> <div class="InfoEx">
<el-button <div>
type="primary" <span>进度</span>
:icon="Refresh" <div class="content">{{ props.row.lastEnum }} {{ props.row.progress }}</div>
@click="RloadTaskInfo(props.row.id)" </div>
circle <div>
/> <span>操作</span>
<el-button type="danger">重试</el-button> <div class="content">
<el-button type="primary">预览</el-button> <el-button
</el-descriptions-item> type="primary"
<el-descriptions-item :label="props.row.lastEnum"> :icon="Refresh"
{{ props.row.progress }} @click="RloadTaskInfo(props.row)"
</el-descriptions-item> circle
<el-descriptions-item label="步骤" :rowspan="2"> />
<el-button type="danger" @click="showDialog(props.row.id)"
>重试</el-button
>
<el-button type="primary">预览</el-button>
</div>
</div>
<div class="grid_item_full_width" v-if="props.row.TaskInfo != null">
<span>步骤</span>
<el-steps <el-steps
style="max-width: 1000px" class="content"
:active="stepData.findIndex((s) => s.value == props.row.lastEnum)" style="max-width: 100%"
:active="props.row.TaskInfo?.active"
align-center align-center
> >
<el-step <el-step
v-for="s in stepData" v-for="s in props.row.TaskInfo.stepData"
:title="s.title" :title="s.title"
:description="s.time" :description="s.time"
:status="s.status" :status="s.status"
/> />
</el-steps> </el-steps>
</el-descriptions-item> </div>
<el-descriptions-item <div
v-if="props.row.TaskInfo?.errorMessage != ''" v-if="
label="错误信息" props.row.TaskInfo?.errorMessage != null &&
props.row.TaskInfo?.errorMessage.length > 0
"
> >
{{ props.row.TaskInfo?.errorMessage }} <span>错误信息</span>
</el-descriptions-item> <div class="content">{{ props.row.TaskInfo?.errorMessage }}</div>
</el-descriptions> </div>
<h2>任务详情</h2> </div>
<div>1</div>
</div> </div>
</template> </template>
</ahTable> </ahTable>
<el-dialog v-model="dialogRef.visible" title="重试任务" width="800"> <el-dialog v-model="dialogRef.visible" title="重试任务" width="430">
<h3>重试任务</h3>
<h3>ID : {{ dialogRef.data.id }}</h3> <h3>ID : {{ dialogRef.data.id }}</h3>
<p></p> <p></p>
<p>将从哪个步骤重试?</p> <p>将从哪个步骤重试?</p>
@ -197,16 +243,22 @@ const stepData = ref<StepData[]>([
style="width: 240px" style="width: 240px"
> >
<el-option <el-option
v-for="item in subjectEnum" v-for="item in redisChannelEnum"
:key="item.value" :key="item.value"
:label="item.text" :label="item.text"
:value="item.value" :value="item.value"
/> />
</el-select> </el-select>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogRef.visible = false">取消</el-button>
<el-button type="primary" @click="submitRowRload()"> 提交 </el-button>
</div>
</template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<style> <style scoped>
.expandSlot { .expandSlot {
padding: 10px 20px; padding: 10px 20px;
background: #f5f7fa; background: #f5f7fa;
@ -220,4 +272,24 @@ const stepData = ref<StepData[]>([
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden; overflow: hidden;
} }
.InfoEx {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-top: 20px;
}
.InfoEx span {
font-weight: bold;
margin-right: 10px;
}
.InfoEx .content {
padding: 5px;
}
/* 关键:让某个网格项占满一行(跨所有列) */
.grid_item_full_width {
grid-column: 1 / -1; /* 从第1列跨到最后一列 */
}
/* :deep(.el-step__description) {
font-size: 16px !important;
} */
</style> </style>

View File

@ -1,4 +1,5 @@
using FreeRedis; using FFmpeg.NET.Services;
using FreeRedis;
using Microsoft.Extensions.DependencyModel; using Microsoft.Extensions.DependencyModel;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using SqlSugar; using SqlSugar;
@ -13,7 +14,7 @@ using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using UserCenter.Model.Interface; using UserCenter.Model;
using VideoAnalysisCore.AICore.SherpaOnnx; using VideoAnalysisCore.AICore.SherpaOnnx;
using VideoAnalysisCore.Model; using VideoAnalysisCore.Model;
using VideoAnalysisCore.Model.Dto; using VideoAnalysisCore.Model.Dto;
@ -203,16 +204,11 @@ namespace VideoAnalysisCore.Common
.Where(u => !u.Name.StartsWith(nameof(Microsoft)) .Where(u => !u.Name.StartsWith(nameof(Microsoft))
&& !u.Name.StartsWith(nameof(System)) && !u.Name.StartsWith(nameof(System))
&& !u.Name.StartsWith("netstandard") && !u.Name.StartsWith("netstandard")
&& (u.Type == "project")); && u.Type == "project");
var assemblies = assembliesStr.Select(a => AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(a.Name))).ToList(); var assemblies = assembliesStr.Select(a => AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(a.Name))).ToList();
var assemblies1 = Assembly.GetEntryAssembly().GetReferencedAssemblies().Where(x => x.Name.StartsWith("App.") || x.Name.StartsWith("UserCenter.")) var assembly = Assembly.Load("UserCenter.Model");
.Select(a => AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(a.Name))).ToList(); assemblies.Add(assembly);
foreach (var item in assemblies1)
{
if (!assemblies.Contains(item))
assemblies.Add(item);
}
return assemblies; return assemblies;
} }
/// <summary> /// <summary>

View File

@ -0,0 +1,51 @@

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
using System.Diagnostics;
using System.Security.Claims;
using UserCenter.Model;
using VideoAnalysisCore.Common;
using VideoAnalysisCore.Controllers.Dto;
namespace VideoAnalysisCore.Controllers
{
/// <summary>
/// 通用接口
/// </summary>
[Authorize(AuthenticationSchemes = Authentication.vdAdmin)]
[Route("api/[controller]/[action]")]
public class AdminController : ControllerBase
{
public AdminController()
{
}
/// <summary>
/// 登录
/// </summary>
/// <param name="req">请求体</param>
/// <returns></returns>
[HttpPost, AllowAnonymous]
public IActionResult Login([FromBody] AdminLoginReq req)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
if (string.IsNullOrWhiteSpace(req.Account)
|| string.IsNullOrWhiteSpace(req.Password))
return BadRequest("无效的登录信息");
return Ok(new
{
//按钮权限
Permissions = "*",
//用户名
UserName = "管理员",
NickName = "管理员",
AccessToken = JwtHelper.GetToken(AppCommon.Config.AuthKey,
[
new Claim("Id","999"),
])
});
}
}
}

View File

@ -1,56 +0,0 @@
using VideoAnalysisCore.Common;
using Microsoft.AspNetCore.Mvc;
using System.Reflection;
using MapsterMapper;
using Mapster;
using VideoAnalysisCore.AICore.SherpaOnnx;
using UserCenter.Model.Enum;
using VideoAnalysisCore.AICore.GPT.ChatGPT;
using VideoAnalysisCore.AICore.GPT;
using System.Text.Json;
using VideoAnalysisCore.Model.Enum;
using Yitter.IdGenerator;
using VideoAnalysisCore.Model;
using Microsoft.AspNetCore.Http;
using VideoAnalysisCore.Model.Dto;
using VideoAnalysisCore.Controllers.Dto;
using VideoAnalysisCore.Common.Expand;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;
using VideoAnalysisCore.Model.À¾¨ÖÇ¿â;
using VideoAnalysisCore.Model.Interface;
using System.Security.Claims;
using UserCenter.Model;
using Microsoft.AspNetCore.Authorization;
using SqlSugar;
namespace VideoAnalysisCore.Controllers
{
[ApiController]
[Route("[controller]/[action]")]
[Authorize(AuthenticationSchemes = Authentication.vdAdmin)]
public class ApiController : ControllerBase
{
private readonly IMapper mp;
private readonly Repository<VideoTask> videoTaskDB;
private readonly RedisManager redisManager;
private readonly Repository<VideoKonwPoint> videoKonwDB;
private readonly Repository<CourseInfo> courseInfoDB;
public readonly SenseVoice senseVoice;
public ApiController(Repository<VideoTask> videoTaskDB,
IMapper mp, Repository<VideoKonwPoint> videoKonwDB, Repository<CourseInfo> courseInfoDB, RedisManager redisManager, SenseVoice senseVoice)
{
this.videoTaskDB = videoTaskDB;
this.mp = mp;
this.videoKonwDB = videoKonwDB;
this.courseInfoDB = courseInfoDB;
this.redisManager = redisManager;
this.senseVoice = senseVoice;
}
}
}

View File

@ -75,33 +75,8 @@ namespace VideoAnalysisCore.Controllers
AppCommon.Config AppCommon.Config
}); });
} }
#endif
/// <summary>
/// 视频处理
/// </summary>
/// <param name="req">请求体</param>
/// <returns></returns>
[HttpPost, AllowAnonymous]
public async Task<ActionResult<object>> Login(AdminLoginReq req)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
if (string.IsNullOrWhiteSpace(req.Account)
|| string.IsNullOrWhiteSpace(req.Password))
return BadRequest("无效的登录信息");
return Ok(new #endif
{
//按钮权限
Permissions = "*",
//用户名
UserName = "管理员",
NickName = "管理员",
AccessToken = JwtHelper.GetToken(AppCommon.Config.AuthKey,
[
new Claim("Id","999"),
])
});
}
} }
} }

View File

@ -177,8 +177,8 @@ namespace VideoAnalysisCore.Controllers
/// <param name="tagId">自定义id</param> /// <param name="tagId">自定义id</param>
/// <param name="subject">切换任务所属学科 null忽略</param> /// <param name="subject">切换任务所属学科 null忽略</param>
/// <returns></returns> /// <returns></returns>
[HttpGet(Name = "ReStart")] [HttpGet]
public async Task<IActionResult> ReStart(long taskId, string? tagId, SubjectEnum? subject) public async Task<IActionResult> ReStartTask(long taskId, string? tagId, SubjectEnum? subject)
{ {
var task = await baseService.AsQueryable() var task = await baseService.AsQueryable()
.WhereIF(taskId != 0, s => s.Id == taskId) .WhereIF(taskId != 0, s => s.Id == taskId)
@ -192,7 +192,7 @@ namespace VideoAnalysisCore.Controllers
await baseService.UpdateAsync(task); await baseService.UpdateAsync(task);
} }
//todo重新开始执行GPT分析 //todo重新开始执行GPT分析
return BadRequest("任务为实现");
return Ok(); return Ok();
} }
@ -241,9 +241,9 @@ namespace VideoAnalysisCore.Controllers
public override async Task<dynamic> PageList([FromBody] QueryRequestBase model) public override async Task<dynamic> PageList([FromBody] QueryRequestBase model)
{ {
var sqlquery = base.BaseQuery(model) var sqlquery = base.BaseQuery(model)
.Select(s =>new VideoTask .Select(s => new VideoTask
{ {
Id = s.Id, Id = s.Id,
TagId = s.TagId, TagId = s.TagId,
VideoType = s.VideoType, VideoType = s.VideoType,
LastEnum = s.LastEnum, LastEnum = s.LastEnum,
@ -267,7 +267,7 @@ namespace VideoAnalysisCore.Controllers
/// <param name="selectEnum">任务类型</param> /// <param name="selectEnum">任务类型</param>
/// <returns></returns> /// <returns></returns>
[HttpGet] [HttpGet]
public async Task ReStart(long id, RedisChannelEnum selectEnum) public async Task ReStart(long id, RedisChannelEnum selectEnum)
{ {
await redisManager.ClearTaskError(id); await redisManager.ClearTaskError(id);
await Task.Run(async () => await Task.Run(async () =>
@ -282,17 +282,21 @@ namespace VideoAnalysisCore.Controllers
/// <param name="id">任务id</param> /// <param name="id">任务id</param>
/// <returns></returns> /// <returns></returns>
[HttpGet] [HttpGet]
public async Task<object> RowRload(long id) public async Task<IActionResult> RowRload(long id)
{ {
if (id == 0)
return BadRequest("无效id");
var d = await redisManager.Redis.HMGetAsync<string>(RedisExpandKey.Task(id), var d = await redisManager.Redis.HMGetAsync<string>(RedisExpandKey.Task(id),
"Progress", "LastEnum", "StartTime", "ErrorMessage"); "Progress", "LastEnum", "StartTime", "ErrorMessage");
return Ok(new return Ok(new
{ {
Progress = d[0], Progress = d[0],
LastEnum = d[1]?.ToEnum<RedisChannelEnum>().ToString(), LastEnum = d[1]?.ToEnum<RedisChannelEnum>().ToString(),
StartTime = JsonSerializer.Deserialize<Dictionary<RedisChannelEnum, DateTime>>( d[2]), StartTime = d[2] != null
? JsonSerializer.Deserialize<Dictionary<RedisChannelEnum, DateTime>>(d[2])
: null,
ErrorMessage = d[3], ErrorMessage = d[3],
}) ; });
} }
/// <summary> /// <summary>
@ -301,7 +305,7 @@ namespace VideoAnalysisCore.Controllers
/// <param name="id">任务id</param> /// <param name="id">任务id</param>
/// <returns></returns> /// <returns></returns>
[HttpGet] [HttpGet]
public async Task<object> ShowTaskInfo(long id) public async Task<object> ShowTaskInfo(long id)
{ {
var nowTask = await baseService.GetFirstAsync(s => s.Id == id); var nowTask = await baseService.GetFirstAsync(s => s.Id == id);
if (nowTask is null) if (nowTask is null)
@ -342,7 +346,8 @@ namespace VideoAnalysisCore.Controllers
.Where(s => s.StageId == item.StageId).ToArray(); .Where(s => s.StageId == item.StageId).ToArray();
} }
return Ok(new { return Ok(new
{
Captions = captionsArr, Captions = captionsArr,
Captions1 = captionsArr1, Captions1 = captionsArr1,
VideoKnows = videoKnows, VideoKnows = videoKnows,

View File

@ -75,7 +75,7 @@
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageReference Include="SqlSugar.IOC" Version="2.0.0" /> <PackageReference Include="SqlSugar.IOC" Version="2.0.0" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.205" /> <PackageReference Include="SqlSugarCore" Version="5.1.4.205" />
<PackageReference Include="UserCenter.Model" Version="1.3.5" /> <PackageReference Include="UserCenter.Model" Version="1.4.9" />
<PackageReference Include="Whisper.net" Version="1.5.0" /> <PackageReference Include="Whisper.net" Version="1.5.0" />
<PackageReference Include="Whisper.net.Runtime" Version="1.5.0" /> <PackageReference Include="Whisper.net.Runtime" Version="1.5.0" />
<PackageReference Include="xFFmpeg.NET" Version="6.0.0" /> <PackageReference Include="xFFmpeg.NET" Version="6.0.0" />