Parcourir la source

fix(@six/wisdom-legacy): 成果管理 - 视频管理页面接口对接

Co-authored-by: Cursor <cursoragent@cursor.com>
cmj il y a 2 jours
Parent
commit
1b121c3d79

+ 51 - 38
apps/wisdom-legacy/src/api/outcome/video.mock.ts

@@ -11,10 +11,15 @@ import type { PageVO } from '#/request/schema/record';
 
 import { pageQueryArgsTransform } from '#/request/schema';
 
-import { decodeVideo, encodeVideo, encodeVideoQuery } from './video.schema';
+import {
+  decodeVideo,
+  decodeVideoType,
+  encodeVideo,
+  encodeVideoQuery,
+} from './video.schema';
 
 /** 后端接口就绪后改为 false */
-export const USE_VIDEO_MOCK = true;
+export const USE_VIDEO_MOCK = false;
 
 type MethodLike<T> = PromiseLike<T> & {
   send?: (force?: boolean) => PromiseLike<T>;
@@ -28,62 +33,68 @@ const SEED_RECORDS: Omit<
   'createTime' | 'id' | 'personalStudioId' | 'updateTime'
 >[] = [
   {
-    videoName: '中医四诊技巧详解',
-    speakerName: '王教授',
-    category: 'clinical_teaching',
+    name: '中医四诊技巧详解',
+    explainer: '王教授',
+    type: '1',
     duration: '45:30',
-    viewCount: 3456,
-    status: true,
+    browseCount: 3456,
+    status: '0',
+    fileUrl: MOCK_VIDEO_URL,
     videoUrl: MOCK_VIDEO_URL,
     createBy: '王教授',
   },
   {
-    videoName: '针灸手法实操演示',
-    speakerName: '李主任',
-    category: 'clinical_skill',
+    name: '针灸手法实操演示',
+    explainer: '李主任',
+    type: '0',
     duration: '32:15',
-    viewCount: 2180,
-    status: true,
+    browseCount: 2180,
+    status: '0',
+    fileUrl: MOCK_VIDEO_URL,
     videoUrl: MOCK_VIDEO_URL,
     createBy: '李主任',
   },
   {
-    videoName: '经典方剂配伍规律解析',
-    speakerName: '张教授',
-    category: 'theory',
+    name: '经典方剂配伍规律解析',
+    explainer: '张教授',
+    type: '2',
     duration: '58:42',
-    viewCount: 1890,
-    status: true,
+    browseCount: 1890,
+    status: '0',
+    fileUrl: MOCK_VIDEO_URL,
     videoUrl: MOCK_VIDEO_URL,
     createBy: '张教授',
   },
   {
-    videoName: '推拿手法基础教学',
-    speakerName: '赵医师',
-    category: 'clinical_teaching',
+    name: '推拿手法基础教学',
+    explainer: '赵医师',
+    type: '1',
     duration: '28:05',
-    viewCount: 1567,
-    status: true,
+    browseCount: 1567,
+    status: '0',
+    fileUrl: MOCK_VIDEO_URL,
     videoUrl: MOCK_VIDEO_URL,
     createBy: '赵医师',
   },
   {
-    videoName: '中药炮制工艺要点',
-    speakerName: '陈教授',
-    category: 'clinical_skill',
+    name: '中药炮制工艺要点',
+    explainer: '陈教授',
+    type: '0',
     duration: '41:20',
-    viewCount: 980,
-    status: false,
+    browseCount: 980,
+    status: '1',
+    fileUrl: MOCK_VIDEO_URL,
     videoUrl: MOCK_VIDEO_URL,
     createBy: '陈教授',
   },
   {
-    videoName: '伤寒论六经辨证精讲',
-    speakerName: '刘教授',
-    category: 'theory',
+    name: '伤寒论六经辨证精讲',
+    explainer: '刘教授',
+    type: '2',
     duration: '1:12:08',
-    viewCount: 4200,
-    status: true,
+    browseCount: 4200,
+    status: '0',
+    fileUrl: MOCK_VIDEO_URL,
     videoUrl: MOCK_VIDEO_URL,
     createBy: '刘教授',
   },
@@ -121,7 +132,7 @@ function delay<T>(runner: () => Promise<T> | T, ms = 120): MethodLike<T> {
 
 function matchKeyword(record: VideoDTO, keyword?: string) {
   if (!keyword) return true;
-  const text = [record.videoName, record.speakerName, record.createBy]
+  const text = [record.name, record.explainer, record.createBy]
     .filter(Boolean)
     .join(' ');
   return text.includes(keyword);
@@ -134,13 +145,13 @@ function matchWorkroom(record: VideoDTO, workroomId?: string) {
 
 function matchCategory(record: VideoDTO, category?: VideoCategory) {
   if (!category) return true;
-  return record.category === category;
+  return decodeVideoType(record.type) === category;
 }
 
 function sortRecords(records: VideoDTO[], sortType?: VideoSortType) {
   const sorted = [...records];
   if (sortType === 'popular') {
-    sorted.sort((a, b) => (b.viewCount ?? 0) - (a.viewCount ?? 0));
+    sorted.sort((a, b) => (b.browseCount ?? 0) - (a.browseCount ?? 0));
     return sorted;
   }
   sorted.sort((a, b) => {
@@ -161,8 +172,10 @@ export function mockListVideoMethod(...args: PageQueryMethodArgs) {
   const pageSize = Number(params.pageSize ?? 12);
   const keyword = data.mixture;
   const workroomId = data.personalStudioId?.toString();
-  const category = data.category;
-  const sortType = data.sortType as undefined | VideoSortType;
+  const category = data.type ? decodeVideoType(data.type) : undefined;
+  const sortType: undefined | VideoSortType = data.isOrderByBrowseCount
+    ? 'popular'
+    : 'latest';
 
   const filtered = sortRecords(
     store.filter(
@@ -219,7 +232,7 @@ export function mockEditVideoMethod(vo: VideoSubmitVO) {
     store.unshift({
       ...dto,
       id,
-      viewCount: 0,
+      browseCount: 0,
       createBy: dto.createBy ?? '当前用户',
       createTime: now,
       updateTime: now,

+ 78 - 24
apps/wisdom-legacy/src/api/outcome/video.schema.ts

@@ -4,7 +4,12 @@ import type {
 } from '#/request/schema/audit-record';
 
 import { z } from '#/adapter/form';
-import { decodeList } from '#/request/schema';
+import {
+  decodeList,
+  decodeZeroFlag,
+  encodeZeroFlag,
+  encodeZeroFlagOptional,
+} from '#/request/schema';
 import { decodeAuditRecord } from '#/request/schema/audit-record';
 
 // ---------------------------------------------------------------------------
@@ -15,6 +20,21 @@ export type VideoCategory = 'clinical_skill' | 'clinical_teaching' | 'theory';
 
 export type VideoSortType = 'latest' | 'popular';
 
+/** 后端 `type` 字段,对应字典:智慧传承系统-视频类型 */
+export type VideoTypeDTO = '0' | '1' | '2';
+
+const VIDEO_TYPE_TO_DTO: Record<VideoCategory, VideoTypeDTO> = {
+  clinical_skill: '0',
+  clinical_teaching: '1',
+  theory: '2',
+};
+
+const VIDEO_TYPE_FROM_DTO: Record<VideoTypeDTO, VideoCategory> = {
+  '0': 'clinical_skill',
+  '1': 'clinical_teaching',
+  '2': 'theory',
+};
+
 export const VIDEO_CATEGORY_OPTIONS = [
   { label: '临床技能', value: 'clinical_skill' },
   { label: '临床教学', value: 'clinical_teaching' },
@@ -30,28 +50,49 @@ export function getVideoCategoryLabel(category?: VideoCategory) {
   );
 }
 
+export function decodeVideoType(type?: number | string): VideoCategory {
+  const normalized = type?.toString();
+  if (normalized === '0' || normalized === '1' || normalized === '2') {
+    return VIDEO_TYPE_FROM_DTO[normalized];
+  }
+  return 'clinical_skill';
+}
+
+function encodeVideoType(category: VideoCategory): VideoTypeDTO {
+  return VIDEO_TYPE_TO_DTO[category];
+}
+
 // ---------------------------------------------------------------------------
 // DTO
 // ---------------------------------------------------------------------------
 
+/** 视频讲解 DTO,对应 `OutcomeVideoExplainDetail` */
 export interface VideoDTO extends AuditRecordDTO {
   id?: number | string;
+  status?: string;
+  remark?: string;
   personalStudioId?: number | string;
-  videoName?: string;
-  speakerName?: string;
-  category?: VideoCategory;
+  fileUrl?: string;
+  downloadCount?: number;
+  browseCount?: number;
+  commentCount?: number;
+  praiseCount?: number;
+  name?: string;
+  explainer?: string;
+  /** 视频类型,详见字典:智慧传承系统-视频类型 */
+  type?: string | VideoTypeDTO;
+  outcomeMedicalCasesId?: number | string;
   videoUrl?: string;
   videoFirstUrl?: string;
   duration?: string;
-  viewCount?: number;
-  status?: boolean;
 }
 
 export interface VideoQueryDTO {
   mixture?: string;
   personalStudioId?: number | string;
-  category?: VideoCategory;
-  sortType?: VideoSortType;
+  type?: string | VideoTypeDTO;
+  status?: string;
+  isOrderByBrowseCount?: boolean;
   pageNum?: number;
   pageSize?: number;
 }
@@ -80,6 +121,7 @@ export interface VideoQueryVO {
   workroomId?: string;
   category?: VideoCategory;
   sortType?: VideoSortType;
+  status?: boolean;
 }
 
 // ---------------------------------------------------------------------------
@@ -106,37 +148,49 @@ export function decodeVideo(dto: VideoDTO): VideoVO {
     ...decodeAuditRecord(dto),
     id: dto.id?.toString(),
     workroomId: dto.personalStudioId?.toString() ?? '',
-    name: dto.videoName ?? '',
-    speaker: dto.speakerName,
-    category: dto.category ?? 'clinical_skill',
+    name: dto.name ?? '',
+    speaker: dto.explainer,
+    category: decodeVideoType(dto.type),
     videoUrl: dto.videoUrl,
     thumbnailUrl: dto.videoFirstUrl,
     duration: dto.duration,
-    viewCount: dto.viewCount ?? 0,
-    status: dto.status ?? true,
+    viewCount: dto.browseCount ?? 0,
+    status: decodeZeroFlag(dto.status),
   };
 }
 
-export function encodeVideoQuery(query: Partial<VideoQueryVO>): VideoQueryDTO {
+function encodeBrowseCountSort(sortType?: VideoSortType): boolean | undefined {
+  if (sortType === 'popular') return true;
+  if (sortType === 'latest') return false;
+  return undefined;
+}
+
+export function encodeVideoQuery(
+  query: Partial<Record<string, unknown> & VideoQueryVO>,
+): VideoQueryDTO {
   return {
-    mixture: query.keyword,
+    mixture: query.keyword || undefined,
     personalStudioId: query.workroomId,
-    category: query.category,
-    sortType: query.sortType,
+    type: query.category ? encodeVideoType(query.category) : undefined,
+    status: encodeZeroFlagOptional(query.status),
+    isOrderByBrowseCount: encodeBrowseCountSort(query.sortType),
   };
 }
 
 export function encodeVideo(vo: VideoSubmitVO): VideoDTO {
+  const videoUrl = vo.videoUrl ?? '';
+
   return {
     id: vo.id,
     personalStudioId: vo.workroomId,
-    videoName: vo.name,
-    speakerName: vo.speaker,
-    category: vo.category,
-    videoUrl: vo.videoUrl,
-    videoFirstUrl: vo.thumbnailUrl,
-    duration: vo.duration,
-    status: vo.status,
+    fileUrl: videoUrl,
+    name: vo.name,
+    explainer: vo.speaker,
+    type: encodeVideoType(vo.category),
+    videoUrl,
+    videoFirstUrl: vo.thumbnailUrl ?? '',
+    duration: vo.duration ?? '',
+    status: encodeZeroFlag(vo.status),
   };
 }
 

+ 11 - 2
apps/wisdom-legacy/src/views/outcome/VideoList.vue

@@ -7,6 +7,7 @@ import { Page } from '@vben/common-ui';
 import { Plus } from '@vben/icons';
 
 import { ClockCircleOutlined, FireOutlined } from '@ant-design/icons-vue';
+import { watchDebounced } from '@vueuse/core';
 import {
   Button,
   Empty,
@@ -143,11 +144,19 @@ async function handleDelete(row: VideoVO) {
   }
 }
 
-function onSearch() {
-  searchKeyword.value = keyword.value.trim();
+function applySearch(value: string) {
+  const next = value.trim();
+  if (next === searchKeyword.value) return;
+  searchKeyword.value = next;
   pageNum.value = 1;
 }
 
+watchDebounced(keyword, applySearch, { debounce: 300 });
+
+function onSearch() {
+  applySearch(keyword.value);
+}
+
 function onPageChange(page: number, size: number) {
   pageNum.value = page;
   pageSize.value = size;

+ 1 - 0
apps/wisdom-legacy/src/views/outcome/video.data.ts

@@ -48,6 +48,7 @@ export const videoForm = defineEditShell<VideoVO>({
       label: '视频分类',
       defaultValue: 'clinical_skill',
       componentProps: {
+        class: 'w-full',
         options: [...VIDEO_CATEGORY_OPTIONS],
         placeholder: '请选择视频分类',
         getPopupContainer,