完善 预览功能

This commit is contained in:
小肥羊 2025-11-05 11:39:58 +08:00
parent 24502a526d
commit bcd6f63bb3
9 changed files with 455 additions and 91 deletions

View File

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

View File

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

View File

@ -9,11 +9,11 @@ export interface Question {
}
export interface VideoKnowRes {
Theme: string;
Content: string;
KnowPoint: string;
KnowPointId: number;
QuestionArr: Question[];
theme: string;
content: string;
knowPoint: string;
knowPointId: number;
questionArr: Question[];
startTime: number;
}
@ -25,8 +25,8 @@ export interface SenseVoiceRes {
export interface ShowTaskInfoRes {
captions: SenseVoiceRes[];
captions1: SenseVoiceRes[];
VideoKnows: VideoKnowRes[];
MediaUrl: string;
videoKnows: VideoKnowRes[];
mediaUrl: string;
}
export interface RowRloadResult {
progress: string;

View File

@ -303,13 +303,13 @@ export class SearchConditions {
this.defaultConditions = [];
this.Conditions = [];
}
/** 是否显示搜索 */
/** 是否显示搜索 [true]*/
show?: boolean;
/** 显示分页器 */
/** 显示分页器 [true]*/
showPage?:boolean;
/** 当前页码 */
/** 当前页码 [0]*/
PageIndex?: number;
/** 每页大小 */
/** 每页大小 [20]*/
PageSize?: number;
/** 排序字段 */
OrderBy?: string;

View File

@ -248,12 +248,9 @@ function handleResetForm() {
function tableClose() {
handleAddCallback();
}
function pageSizeChange(o) {
table.value.search.PageSize = o;
fetchPagedData();
}
function pageIndexChange(o) {
table.value.search.PageIndex = o - 1;
function paginationChange(currentPage: number, pageSize: number) {
table.value.search.PageIndex = currentPage - 1;
table.value.search.PageSize = pageSize;
fetchPagedData();
}
@ -375,7 +372,7 @@ function fetchPagedData() {
table.value.data = res.data.map((s, i) => {
return { ...s, customId: i };
});
table.value.pageData = res.data;
table.value.pageData = res;
}
});
}
@ -556,13 +553,10 @@ function fetchPagedData() {
"
>
<el-pagination
:current-page="table.search.PageIndex + 1"
:page-sizes="[20, 40, 80, 100]"
:page-size="table.search.PageSize"
layout="prev, pager, next,sizes, total"
:total="table.pageData.total"
@size-change="pageSizeChange"
@current-change="pageIndexChange"
@change="paginationChange"
/>
</div>

View File

@ -23,11 +23,20 @@ export default {
},
{
path: "/welcome/showTask",
name: "ShowTask",
name: "showTask",
component: () => import("@/views/welcome/showTask.vue"),
meta: {
title: "预览任务",
showLink: false
}
},
{
path: "/welcome/runningTask",
name: "runningTask",
component: () => import("@/views/welcome/runningTask.vue"),
meta: {
title: "进行中任务",
showLink: VITE_HIDE_HOME === "true" ? false : true
showLink: true
}
}
]

View File

@ -17,16 +17,16 @@ import { ReStart, RowRload } from "@/api/videoTask";
import { Refresh } from "@element-plus/icons-vue";
import { message } from "@/utils/message";
import { json } from "stream/consumers";
import { useRouter } from "vue-router";
const ControllerName = "VideoTask";
defineOptions({
name: ControllerName,
});
const props = defineProps<{
data: any;
}>();
const route = useRouter();
function searchCallback(data) {}
const table = ref<{ initTable: (config: TableConfig) => void }>();
const tableData: TableConfig = intTableData({
@ -40,6 +40,7 @@ const tableData: TableConfig = intTableData({
//
show: true,
PageSize: 10,
showPage: true,
},
operationColumn: true, //
operationColumnData: [],
@ -100,6 +101,7 @@ let redisChannelEnum = ref<ComboModel[]>([]);
const showTable = ref(false);
onMounted(async () => {
//
tableData.column.videoType.setting.datasource = await getenum("AttachmentsInfoType");
tableData.column.lastEnum.setting.datasource = await getenum("RedisChannelEnum");
tableData.column.subject.setting.datasource = await getenum("SubjectEnum");
@ -132,11 +134,12 @@ async function RloadTaskInfo(row: any) {
element.time = formatDateToChinese(
row.TaskInfo.startTime[element.title.toLowerCase()]
);
if (element.value < row.TaskInfo.lastEnum) {
let i = row.TaskInfo.stepData.indexOf(element);
if (i < row.TaskInfo.active) {
element.status = "finish";
} else if (element.value == 60) {
element.status = "success";
} else if (element.value == row.TaskInfo.lastEnum) {
} else if (i == row.TaskInfo.active) {
element.status = "process";
} else {
element.status = "wait";
@ -198,7 +201,16 @@ const stepData = ref<StepData[]>([
<el-button type="danger" @click="showDialog(props.row.id)"
>重试</el-button
>
<el-button type="primary">预览</el-button>
<el-button
type="primary"
@click="
route.push({
path: '/welcome/showTask',
query: { id: props.row.id.toString() },
})
"
>预览</el-button
>
</div>
</div>

View File

@ -0,0 +1,233 @@
<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 { message } from "@/utils/message";
import { isEmpty } from "@pureadmin/utils";
import { ref, onMounted, nextTick } from "vue";
import { useRoute } from "vue-router";
defineOptions({
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] };
});
};
//
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 () => {
// 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>

View File

@ -10,14 +10,14 @@
<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 class="knowTtileTheme">{{ getTimeRange(item) }} {{ item.theme }}</div>
<span class="kSpan">#{{ item.knowPointId }} {{ item.knowPoint }}</span>
</div>
<div>概览: {{ item.Content }}</div>
<div>概览: {{ item.content }}</div>
<br />
<div v-if="item.QuestionArr && item.QuestionArr.length > 0">
<div v-if="item.questionArr && item.questionArr.length > 0">
<div
v-for="(q, qIndex) in item.QuestionArr"
v-for="(q, qIndex) in item.questionArr"
:key="qIndex"
class="knowQuestion"
@click="spClickTime(q.startTime)"
@ -32,7 +32,7 @@
:src="q.pPTImageUrl"
width="320"
height="180"
:alt="'问题图片' + qIndex"
:alt="'试题' + qIndex"
/>
</div>
<br />
@ -41,16 +41,16 @@
<br />
</div>
<button class="kBtn" @click="spClick(index, $event)">
<span>{{ getFirstChar(item.Theme) }} {{ item.Theme }}</span>
<span>{{ getTimeRange(item) }} {{ item.theme }}</span>
<br />
<span class="kSpan textEllipsis"
>#{{ item.KnowPointId }} {{ item.KnowPoint }}</span
>#{{ item.knowPointId }} {{ item.knowPoint }}</span
>
</button>
</div>
</div>
</div>
<video ref="videoPlayerEL" controls autoplay>
<video ref="videoPlayerEL" @timeupdate="timeupdateVideo" controls autoplay>
<source :src="videoSrc" type="video/mp4" />
</video>
<div ref="subtitleAreaEL" class="subtitles">{{ currentSubtitle }}</div>
@ -62,11 +62,14 @@
<script setup lang="ts">
import { SenseVoiceRes, ShowTaskInfo, VideoKnowRes } from "@/api/videoTask";
import { message } from "@/utils/message";
import { isEmpty } from "@pureadmin/utils";
import { ref, onMounted, nextTick } from "vue";
import { useRoute } from "vue-router";
const subtitleAreaEL = ref<HTMLElement | null>(null);
const subtitleArea1EL = ref<HTMLElement | null>(null);
defineOptions({
name: "showTask",
});
const videoPlayerEL = ref<HTMLVideoElement | null>(null);
//
@ -125,42 +128,32 @@ const initKD = () => {
const setDB = (
a: SenseVoiceRes[],
a1: SenseVoiceRes[],
videoKnows: VideoKnowRes[],
vKnows: VideoKnowRes[],
c: string
) => {
subtitles.value = a;
subtitles1.value = a1;
b1.value = videoKnows;
b1.value = vKnows;
videoKnows.value = vKnows;
videoSrc.value = c;
//
init();
videoPlayerEL.value?.load();
videoPlayerEL.value?.play();
};
//
const init = () => {
if (!videoPlayerEL) return;
//
videoPlayerEL.value.addEventListener("timeupdate", function () {
function timeupdateVideo() {
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 || "";
let subtitleI = subtitles.value.findIndex(
(subtitle) => currentTime >= subtitle.start && currentTime <= subtitle.end
);
if (subtitleI > -1 && currentSubtitle.value !== subtitles.value[subtitleI].text) {
currentSubtitle.value = subtitles.value[subtitleI].text;
currentSubtitle1.value = subtitles1.value[subtitleI]?.text || "";
} else if (subtitleI == -1) {
currentSubtitle.value = "";
currentSubtitle1.value = "";
}
});
//
const segment = displayButton.value.findLast((s: any) => currentTime >= s.startTime);
@ -171,8 +164,7 @@ const init = () => {
}
lastSegments.value = segment;
}
});
};
}
/**
* 扩展功能格式化开始和结束时间范围
* @param segment 包含 StartTime EndTime 的对象
@ -206,12 +198,15 @@ onMounted(async () => {
},
};
}
//
const route = useRoute();
const id = route.params.id;
let info = await ShowTaskInfo(id);
setDB(info.captions, info.captions1, info.VideoKnows, info.MediaUrl);
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);
setDB(info.captions, info.captions1, info.videoKnows, info.mediaUrl);
});
// Window
@ -221,3 +216,124 @@ declare global {
}
}
</script>
<style scoped>
* {
padding: 0;
margin: 0;
}
.locked {
right: 0px !important;
}
#video-container {
position: relative;
width: 1660px;
height: 850px;
float: left;
overflow-x: hidden;
}
.kSpan {
color: rgba(120, 120, 120, 0.66);
font-size: 0.8rem;
width: 330px;
}
.textEllipsis {
white-space: nowrap; /* 禁止换行 */
overflow: hidden; /* 隐藏溢出内容 */
text-overflow: ellipsis; /* 显示省略号 */
}
video {
width: 94%;
height: 85%;
}
.gudingBtn {
width: 32px;
height: 32px;
/* border-radius: 16px; */
line-height: 27px;
text-align: center;
}
.subtitles {
position: absolute;
bottom: 200px;
width: 100%;
text-align: center;
color: white;
background-color: rgba(0, 0, 0, 0.7);
font-size: 18px;
}
#segmentsContainer:is(:hover) {
right: 0px !important;
}
#segmentsContainer {
transition: right 0.7s;
z-index: 999;
overflow-x: hidden;
background-color: #e3e3e3c2;
position: absolute;
right: -300px;
display: flex;
flex-direction: column;
width: 400px;
height: 750px;
gap: 10px;
overflow-y: scroll;
float: left;
flex-wrap: nowrap;
padding: 10px;
align-content: flex-start;
justify-content: flex-start;
align-items: center;
}
.kBtn {
width: 340px;
height: 60px;
font-size: 1.3rem;
text-align: left;
cursor: pointer;
color: rgb(103, 194, 58);
background-color: rgb(240, 249, 235);
border: 1px solid rgb(179, 225, 157);
}
.knowTtileTheme {
font-size: 1.3rem;
cursor: pointer;
color: rgb(103, 194, 58);
}
.kBtn:hover {
background-color: rgb(248, 230, 191) !important;
border: 1px solid rgb(206, 187, 81);
}
.knowDiv {
}
.knowQuestion {
cursor: pointer;
border: 2px solid #ff000059;
border-radius: 10px;
background-color: #cddc393d;
}
.knowDiv:hover .knowTtile {
width: 340px;
display: block;
background-color: rgb(240, 249, 235);
border: 1px solid rgb(179, 225, 157);
}
.knowTtile {
position: absolute;
text-align: left;
display: none;
}
</style>