Compare commits

...

2 Commits

Author SHA1 Message Date
小肥羊 a31352b8f5 修改 ui打包配置 2025-11-06 16:24:31 +08:00
小肥羊 54ba342153 完善 web 执行中任务页面 2025-11-06 15:11:11 +08:00
10 changed files with 143 additions and 270 deletions

View File

@ -49,6 +49,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="WebUI\dist\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Update="appsettings.json"> <Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>

View File

@ -13,7 +13,7 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"launchUrl": "/swagger/index.html", "launchUrl": "ui/index.html",
"applicationUrl": "http://*:5238", "applicationUrl": "http://*:5238",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"

View File

@ -1,5 +1,5 @@
# 线上环境平台打包路径 # 线上环境平台打包路径
VITE_PUBLIC_PATH = / VITE_PUBLIC_PATH = /ui/
# 线上环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数" # 线上环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash" VITE_ROUTER_HISTORY = "hash"
@ -13,7 +13,4 @@ VITE_CDN = false
VITE_COMPRESSION = "none" VITE_COMPRESSION = "none"
# 接口地址 VITE_API_BASEURL = "/"
VITE_API_BASEURL = "https://learn-archives-admin.23544.com/api"
#数据中心后台地址
VITE_API_USERCENTER_URL = "https://dcb.23544.com/api"

View File

@ -56,3 +56,12 @@ export const ShowTaskInfo = (id: any) => {
params: { id } params: { id }
}); });
}; };
/** 展示数据 */
export const RunningTaskList = (data: any) => {
return http.request<any>("post", "/api/VideoTask/RunningTaskList", {
data
});
};

View File

@ -1,3 +1,4 @@
import { promises } from "node:dns";
import { Ref } from "vue"; import { Ref } from "vue";
import { array } from "vue-types"; import { array } from "vue-types";
@ -326,8 +327,11 @@ export class SearchConditions {
/** 表格配置 */ /** 表格配置 */
export interface TableConfig { export interface TableConfig {
/** 搜索回调函数 */ /** 搜索回调函数 [返回值为true则不调用基础PageAPI]*/
searchCallback?: (s: SearchConditions) => void; searchCallback?: (
s: SearchConditions,
tv: TableConfig
) => Promise<boolean> | Promise<void> | boolean | void;
/** 新增/修改回调函数 */ /** 新增/修改回调函数 */
editCallback?: (from: any) => void; editCallback?: (from: any) => void;
/** 编辑表单初始化回调函数 */ /** 编辑表单初始化回调函数 */

View File

@ -103,10 +103,11 @@ const appB_S = ref<HTMLElement>(null);
function appStyle() { function appStyle() {
if (tableHeight.value !== 0) return; if (tableHeight.value !== 0) return;
tableHeight.value = tableHeight.value =
appB.value.parentElement.parentElement.offsetHeight - appB.value.parentElement.parentElement.parentElement.offsetHeight -
145 - 145 -
appB_S.value.offsetHeight + appB_S.value.offsetHeight +
0; 0;
console.log("tableHeight.value", tableHeight.value);
return tableHeight; return tableHeight;
} }
@ -288,7 +289,7 @@ function searchReload() {
} }
} }
// //
function handleReloadPaged(reload = true) { async function handleReloadPaged(reload = true) {
if (defaultOrderBy == null) { if (defaultOrderBy == null) {
defaultOrderBy = { defaultOrderBy = {
OrderBy: table.value.search.OrderBy, OrderBy: table.value.search.OrderBy,
@ -332,12 +333,9 @@ function handleReloadPaged(reload = true) {
if (!reload && table.value.search.Conditions.length > 0) { if (!reload && table.value.search.Conditions.length > 0) {
table.value.search.PageIndex = 0; table.value.search.PageIndex = 0;
} }
if (table.value.searchCallback) {
table.value.searchCallback(table.value.search);
}
// //
if (table.value.expandColumn) expands.value = []; if (table.value.expandColumn) expands.value = [];
fetchPagedData(); return fetchPagedData();
} }
// //
async function fetchInitData() { async function fetchInitData() {
@ -360,7 +358,10 @@ function getByteLen(str) {
return len; return len;
} }
// //
function fetchPagedData() { async function fetchPagedData() {
if (table.value.searchCallback) {
if (await table.value.searchCallback(table.value.search, table.value)) return;
}
for (const iterator of table.value.search.defaultConditions) { for (const iterator of table.value.search.defaultConditions) {
if (!iterator) continue; if (!iterator) continue;
if (!table.value.search.Conditions.find((s) => s == iterator)) { if (!table.value.search.Conditions.find((s) => s == iterator)) {

View File

@ -4,6 +4,7 @@ import {
ComboModel, ComboModel,
ConditionalType, ConditionalType,
intTableData, intTableData,
SearchConditions,
TableColumnSearch, TableColumnSearch,
TableConfig, TableConfig,
} from "@/components/hTable/hTable"; } from "@/components/hTable/hTable";
@ -17,6 +18,7 @@ import { ReStart, RowRload } from "@/api/videoTask";
import { Refresh } from "@element-plus/icons-vue"; import { Refresh } from "@element-plus/icons-vue";
import { message } from "@/utils/message"; import { message } from "@/utils/message";
import { json } from "stream/consumers"; import { json } from "stream/consumers";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
@ -26,15 +28,20 @@ defineOptions({
name: ControllerName, name: ControllerName,
}); });
const props = defineProps<{
searchCallback?: (
s: SearchConditions,
tv: TableConfig
) => boolean | Promise<boolean> | void;
}>();
const route = useRouter(); const route = useRouter();
function searchCallback(data) {}
const table = ref<{ initTable: (config: TableConfig) => void }>(); const table = ref<{ initTable: (config: TableConfig) => void }>();
const tableData: TableConfig = intTableData({ const tableData: TableConfig = intTableData({
apiUrl: ControllerName, apiUrl: ControllerName,
expandColumn: true, expandColumn: true,
selectColumn: false, // selectColumn: false, //
border: false, // border: false, //
searchCallback: searchCallback, searchCallback: props.searchCallback,
expandChange: expandChange, expandChange: expandChange,
search: { search: {
// //
@ -122,6 +129,25 @@ async function submitRowRload() {
async function expandChange(row: any, expandedRows: any[]) { async function expandChange(row: any, expandedRows: any[]) {
if (expandedRows.find((s) => s == row)) RloadTaskInfo(row); if (expandedRows.find((s) => s == row)) RloadTaskInfo(row);
} }
function previewTask(row: any) {
let pageName = "showTask";
let queryData = { id: row.id.toString() };
useMultiTagsStoreHook().handleTags("push", {
path: `/welcome/showTask_` + row.id,
name: pageName,
query: queryData,
meta: {
title: `任务预览` + row.id.toString().slice(-4),
dynamicLevel: 3,
},
});
//
route.push({ name: pageName, query: queryData });
}
function firstLetterToLower(str) {
if (typeof str !== "string" || !str) return str;
return str[0].toLowerCase() + str.slice(1);
}
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;
@ -132,7 +158,7 @@ async function RloadTaskInfo(row: any) {
if (row.TaskInfo.startTime != null) { if (row.TaskInfo.startTime != null) {
for (const element of row.TaskInfo.stepData) { for (const element of row.TaskInfo.stepData) {
element.time = formatDateToChinese( element.time = formatDateToChinese(
row.TaskInfo.startTime[element.title.toLowerCase()] row.TaskInfo.startTime[firstLetterToLower(element.title)]
); );
let i = row.TaskInfo.stepData.indexOf(element); let i = row.TaskInfo.stepData.indexOf(element);
if (i < row.TaskInfo.active) { if (i < row.TaskInfo.active) {
@ -184,10 +210,12 @@ const stepData = ref<StepData[]>([
<!-- 拓展内容 --> <!-- 拓展内容 -->
<div class="expanded-content expandSlot"> <div class="expanded-content expandSlot">
<h3>任务详情</h3> <h3>任务详情</h3>
<div class="InfoEx"> <div class="InfoEx" v-if="props.row.TaskInfo != null">
<div> <div>
<span>进度</span> <span>进度</span>
<div class="content">{{ props.row.lastEnum }} {{ props.row.progress }}</div> <div class="content">
{{ props.row.TaskInfo.lastEnum }} {{ props.row.TaskInfo.progress }}
</div>
</div> </div>
<div> <div>
<span>操作</span> <span>操作</span>
@ -198,19 +226,8 @@ const stepData = ref<StepData[]>([
@click="RloadTaskInfo(props.row)" @click="RloadTaskInfo(props.row)"
circle circle
/> />
<el-button type="danger" @click="showDialog(props.row.id)" <el-button type="danger" @click="showDialog(props.row)">重试</el-button>
>重试</el-button <el-button type="primary" @click="previewTask(props.row)">预览</el-button>
>
<el-button
type="primary"
@click="
route.push({
path: '/welcome/showTask',
query: { id: props.row.id.toString() },
})
"
>预览</el-button
>
</div> </div>
</div> </div>

View File

@ -1,233 +1,41 @@
<template>
<div id="video-container">
<div v-if="videoKnows.length > 0">
<div id="segmentsContainer" class="sc" :class="{ locked: isLocked }">
<h2>
<button class="gudingBtn" @click="toggleLock">
{{ isLocked ? "🔓" : "🔒" }}
</button>
</h2>
<div v-for="(item, index) in videoKnows" :key="index" class="knowDiv">
<div class="knowTtile">
<div style="cursor: pointer" @click="spClick(index, $event)">
<div class="knowTtileTheme">{{ getTimeRange(item) }} {{ item.Theme }}</div>
<span class="kSpan">#{{ item.KnowPointId }} {{ item.KnowPoint }}</span>
</div>
<div>概览: {{ item.Content }}</div>
<br />
<div v-if="item.QuestionArr && item.QuestionArr.length > 0">
<div
v-for="(q, qIndex) in item.QuestionArr"
:key="qIndex"
class="knowQuestion"
@click="spClickTime(q.startTime)"
>
<h3>
问题: <span class="kSpan">{{ q.startTime }} </span>
</h3>
<div class="kSpan">{{ q.topicStem }}</div>
<div>{{ q.question }}</div>
<img
style="text-align: center"
:src="q.pPTImageUrl"
width="320"
height="180"
:alt="'问题图片' + qIndex"
/>
</div>
<br />
</div>
<br />
<br />
</div>
<button class="kBtn" @click="spClick(index, $event)">
<span>{{ getFirstChar(item.Theme) }} {{ item.Theme }}</span>
<br />
<span class="kSpan textEllipsis"
>#{{ item.KnowPointId }} {{ item.KnowPoint }}</span
>
</button>
</div>
</div>
</div>
<video ref="videoPlayerEL" controls autoplay>
<source :src="videoSrc" type="video/mp4" />
</video>
<div ref="subtitleAreaEL" class="subtitles">{{ currentSubtitle }}</div>
<div ref="subtitleArea1EL" class="subtitles" :style="subtitleStyle">
{{ currentSubtitle1 }}
</div>
</div>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { SenseVoiceRes, ShowTaskInfo, VideoKnowRes } from "@/api/videoTask"; import ahTable from "@/components/hTable/index.vue";
import {
ComboModel,
ConditionalType,
intTableData,
SearchConditions,
TableColumnSearch,
TableConfig,
} from "@/components/hTable/hTable";
import { onMounted, ref } from "vue";
import { getenum } from "@/api/enum";
import { ElMessage } from "element-plus";
import { ReStart, RowRload, RunningTaskList } from "@/api/videoTask";
import { Refresh } from "@element-plus/icons-vue";
import { message } from "@/utils/message"; import { message } from "@/utils/message";
import { isEmpty } from "@pureadmin/utils"; import videoTask from "./index.vue";
import { ref, onMounted, nextTick } from "vue";
import { useRoute } from "vue-router";
defineOptions({ defineOptions({
name: "runningTask", name: `runningTask`,
}); });
async function searchCallback(s: SearchConditions, tv: TableConfig): Promise<boolean> {
const subtitleAreaEL = ref<HTMLElement | null>(null); //
const subtitleArea1EL = ref<HTMLElement | null>(null); let res = await RunningTaskList(s);
const videoPlayerEL = ref<HTMLVideoElement | null>(null); tv.data = res.data.map((s, i) => {
return { ...s, customId: i };
//
const videoKnows = ref<VideoKnowRes[]>([]);
const currentSubtitle = ref("");
const currentSubtitle1 = ref("");
const isLocked = ref(false);
const displayButton = ref<any[]>([]);
const lastSegments = ref<any>(null);
const videoSrc = ref("");
const subtitles = ref<SenseVoiceRes[]>([]);
const b1 = ref([]);
const subtitles1 = ref<SenseVoiceRes[]>([]);
//
const subtitleStyle = {
bottom: "101px",
backgroundColor: "rgb(99 129 103 / 50%)",
};
//
const getFirstChar = (str: string): string => {
return str ? str.charAt(0) : "";
};
//
const toggleLock = () => {
isLocked.value = !isLocked.value;
};
//
const spClick = (index: number, event: Event) => {
if (videoPlayerEL && displayButton.value[index]) {
videoPlayerEL.value.currentTime = displayButton.value[index].startTime;
}
};
//
const spClickTime = (startTime: number) => {
if (videoPlayerEL) {
videoPlayerEL.value.currentTime = startTime;
}
};
//
const initKD = () => {
const btns = document.getElementsByClassName("kBtn");
if (btns.length === 0) return;
displayButton.value = b1.value.map((s: any, i: number) => {
return { ...s, button: btns[i] };
}); });
}; tv.pageData = res;
return true;
// //trueAPI
const setDB = (
a: SenseVoiceRes[],
a1: SenseVoiceRes[],
videoKnows: VideoKnowRes[],
c: string
) => {
subtitles.value = a;
subtitles1.value = a1;
b1.value = videoKnows;
videoSrc.value = c;
//
init();
};
//
const init = () => {
if (!videoPlayerEL) return;
//
videoPlayerEL.value.addEventListener("timeupdate", function () {
if (displayButton.value.length === 0) initKD();
const currentTime = videoPlayerEL.value.currentTime;
if (subtitleAreaEL) subtitleAreaEL.value.textContent = "";
if (subtitleArea1EL) subtitleArea1EL.value.textContent = "";
//
subtitles.value.forEach((subtitle, index) => {
if (
currentTime >= subtitle.start &&
currentTime <= subtitle.end &&
subtitleAreaEL &&
subtitleAreaEL.value.textContent !== subtitle.text
) {
currentSubtitle.value = subtitle.text;
currentSubtitle1.value = subtitles1.value[index]?.text || "";
}
});
//
const segment = displayButton.value.findLast((s: any) => currentTime >= s.startTime);
if (segment) {
segment.button.style.backgroundColor = "rgb(238, 200, 118)";
if (lastSegments.value && lastSegments.value !== segment) {
lastSegments.value.button.style.backgroundColor = "rgb(240, 249, 235)";
}
lastSegments.value = segment;
}
});
};
/**
* 扩展功能格式化开始和结束时间范围
* @param segment 包含 StartTime EndTime 的对象
* @returns 格式化的时间范围字符串 (MM:SS - MM:SS)
*/
function getTimeRange(segment: VideoKnowRes): string {
const startTime = segment.startTime ?? 0;
const startMinutes = Math.floor(startTime / 60);
const startSeconds = Math.floor(startTime % 60);
const sf = startMinutes.toString().padStart(2, "0");
const sm = startSeconds.toString().padStart(2, "0");
return `${sf}:${sm}`;
} }
//
onMounted(async () => { onMounted(async () => {
// MathJax //
if (window.MathJax) {
window.MathJax = {
tex: {
inlineMath: [
["$", "$"],
["\\(", "\\)"],
],
displayMath: [
["$$", "$$"],
["\\[", "\\]"],
],
},
};
}
//
const route = useRoute();
const data = isEmpty(route.params) ? route.query : route.params;
if (isEmpty(data.id) || data.id == null) {
message("无效的任务", { type: "warning" });
return;
}
let info = await ShowTaskInfo(data.id);
debugger;
setDB(info.captions, info.captions1, info.VideoKnows, info.MediaUrl);
}); });
// Window
declare global {
interface Window {
MathJax: any;
}
}
</script> </script>
<template>
<div>
<videoTask :searchCallback="searchCallback"></videoTask>
</div>
</template>

View File

@ -2,11 +2,13 @@
<div id="video-container"> <div id="video-container">
<div v-if="videoKnows.length > 0"> <div v-if="videoKnows.length > 0">
<div id="segmentsContainer" class="sc" :class="{ locked: isLocked }"> <div id="segmentsContainer" class="sc" :class="{ locked: isLocked }">
<h2> <div>
<button class="gudingBtn" @click="toggleLock"> <h2>
{{ isLocked ? "🔓" : "🔒" }} <button class="gudingBtn" @click="toggleLock">
</button> {{ isLocked ? "🔓" : "🔒" }}</button
</h2> >视频分段
</h2>
</div>
<div v-for="(item, index) in videoKnows" :key="index" class="knowDiv"> <div v-for="(item, index) in videoKnows" :key="index" class="knowDiv">
<div class="knowTtile"> <div class="knowTtile">
<div style="cursor: pointer" @click="spClick(index, $event)"> <div style="cursor: pointer" @click="spClick(index, $event)">
@ -137,7 +139,13 @@ const setDB = (
videoKnows.value = vKnows; videoKnows.value = vKnows;
videoSrc.value = c; videoSrc.value = c;
videoPlayerEL.value?.load(); videoPlayerEL.value?.load();
videoPlayerEL.value?.play(); try {
nextTick(() => {
initKD();
});
} catch (error) {
console.error("视频自动播放失败:", error);
}
}; };
function timeupdateVideo() { function timeupdateVideo() {
if (displayButton.value.length === 0) initKD(); if (displayButton.value.length === 0) initKD();
@ -254,7 +262,6 @@ video {
height: 32px; height: 32px;
/* border-radius: 16px; */ /* border-radius: 16px; */
line-height: 27px; line-height: 27px;
text-align: center;
} }
.subtitles { .subtitles {
@ -289,7 +296,7 @@ video {
padding: 10px; padding: 10px;
align-content: flex-start; align-content: flex-start;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: flex-start;
} }
.kBtn { .kBtn {
@ -312,6 +319,7 @@ video {
.kBtn:hover { .kBtn:hover {
background-color: rgb(248, 230, 191) !important; background-color: rgb(248, 230, 191) !important;
border: 1px solid rgb(206, 187, 81); border: 1px solid rgb(206, 187, 81);
display: block;
} }
.knowDiv { .knowDiv {
@ -323,7 +331,9 @@ video {
border-radius: 10px; border-radius: 10px;
background-color: #cddc393d; background-color: #cddc393d;
} }
.knowDiv:hover .kBtn {
display: none;
}
.knowDiv:hover .knowTtile { .knowDiv:hover .knowTtile {
width: 340px; width: 340px;
display: block; display: block;
@ -332,7 +342,7 @@ video {
} }
.knowTtile { .knowTtile {
position: absolute; /* position: absolute; */
text-align: left; text-align: left;
display: none; display: none;
} }

View File

@ -192,7 +192,7 @@ namespace VideoAnalysisCore.Controllers
await baseService.UpdateAsync(task); await baseService.UpdateAsync(task);
} }
//todo重新开始执行GPT分析 //todo重新开始执行GPT分析
return BadRequest("任务实现"); return BadRequest("任务实现");
return Ok(); return Ok();
} }
@ -270,7 +270,7 @@ namespace VideoAnalysisCore.Controllers
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 () => _ = Task.Run(async () =>
await redisManager.InsertChannel(selectEnum, id) await redisManager.InsertChannel(selectEnum, id)
); );
} }
@ -356,8 +356,32 @@ namespace VideoAnalysisCore.Controllers
} }
/// <summary>
/// 预览任务结果
/// </summary>
/// <param name="model">查询模型</param>
/// <returns></returns>
[HttpPost]
public async Task<object> RunningTaskList([FromBody] QueryRequestBase model)
{
var oldTaskArr = redisManager.Redis.LRange<long>(RedisExpandKey.IDTask, 0, 999);
var sqlquery = base.BaseQuery(model)
.Where(s => oldTaskArr.Contains(s.Id))
.Select(s => new VideoTask
{
Id = s.Id,
TagId = s.TagId,
VideoType = s.VideoType,
LastEnum = s.LastEnum,
Subject = s.Subject,
ComeFrom = s.ComeFrom,
MediaUrl = s.MediaUrl,
CreateTime = s.CreateTime,
});
RefAsync<int> total = 0;
var data = await sqlquery.ToPageListAsync(model.PageIndex + 1, model.PageSize, total);
return new PageResult<VideoTask>() { Data = data, Total = total };
}
} }
} }