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>
<None Include="WebUI\dist\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>

View File

@ -13,7 +13,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "/swagger/index.html",
"launchUrl": "ui/index.html",
"applicationUrl": "http://*:5238",
"environmentVariables": {
"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参数"
VITE_ROUTER_HISTORY = "hash"
@ -13,7 +13,4 @@ VITE_CDN = false
VITE_COMPRESSION = "none"
# 接口地址
VITE_API_BASEURL = "https://learn-archives-admin.23544.com/api"
#数据中心后台地址
VITE_API_USERCENTER_URL = "https://dcb.23544.com/api"
VITE_API_BASEURL = "/"

View File

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

View File

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

View File

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

View File

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

View File

@ -192,7 +192,7 @@ namespace VideoAnalysisCore.Controllers
await baseService.UpdateAsync(task);
}
//todo重新开始执行GPT分析
return BadRequest("任务实现");
return BadRequest("任务实现");
return Ok();
}
@ -270,7 +270,7 @@ namespace VideoAnalysisCore.Controllers
public async Task ReStart(long id, RedisChannelEnum selectEnum)
{
await redisManager.ClearTaskError(id);
await Task.Run(async () =>
_ = Task.Run(async () =>
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 };
}
}
}