Selaa lähdekoodia

初步给定画布

Shinohara Haruna 5 kuukautta sitten
vanhempi
sitoutus
d2abad14a6

+ 1 - 1
smsb-plus-ui/src/layout/components/Sidebar/index.vue

@@ -30,7 +30,7 @@ const topMenus = computed(() => permissionStore.getTopbarRoutes().filter((menu)
 const currentTopMenuPath = computed(() => {
   // console.log("[Sidebar] route.path", route.path);
   // 特例
-  if (route.path === '/source/push/approval' || route.path.startsWith('/source/split/edit')) {
+  if (route.path === '/source/push/approval' || route.path.startsWith('/source/split/edit') || route.path.startsWith('/smsb/itemProgram/edit')) {
     return '/source';
   }
   // 取当前路由的一级菜单 path

+ 426 - 22
smsb-plus-ui/src/views/smsb/itemProgram/EditProgram.vue

@@ -1,35 +1,439 @@
 <template>
-    <div class="edit-program-page">
-        <h2>编辑节目</h2>
-        <div v-if="id">
-            <el-form label-width="100px">
-                <el-form-item label="节目ID">
-                    <el-input v-model="id" disabled />
-                </el-form-item>
-                <!-- 这里可以添加更多表单项 -->
-            </el-form>
-        </div>
-        <div v-else>
-            <el-alert title="未获取到节目ID" type="error" />
-        </div>
+  <div class="edit-program-layout">
+    <!-- 左侧组件栏及返回按钮 -->
+    <div class="sidebar">
+      <el-button class="back-btn" type="default" @click="goBack" circle>
+        <el-icon>
+          <ArrowLeft />
+        </el-icon>
+      </el-button>
+      <div class="sidebar-title">组件</div>
+      <div class="sidebar-item component-item"
+        :class="{ 'selected': selectedComponent && selectedComponent.type === 'canvas' }"
+        @click="selectCanvasFromSidebar">
+        <div class="component-icon-text">画布</div>
+      </div>
     </div>
+
+    <!-- 中间编辑区 -->
+    <div class="main-editor">
+      <div class="editor-canvas" ref="editorCanvasRef">
+        <template v-for="item in editorContent.elements.sort((a, b) => a.depth - b.depth)"
+          :key="item.depth + '-' + item.type">
+          <CanvasBoard v-if="item.type === 'canvas'" :width="item.width" :height="item.height" :bg="item.bg"
+            :scale="canvasScale"
+            @click.stop="selectComponent(item)" :class="{ selected: selectedComponent === item }" />
+          <!-- 未来可扩展其他组件类型的渲染 -->
+        </template>
+      </div>
+      <el-button type="primary" class="save-btn" @click="handleSave">保存</el-button>
+    </div>
+
+    <!-- 右侧属性栏 -->
+    <div class="property-panel">
+      <div class="property-title">属性</div>
+      <div class="property-form-area">
+        <el-form label-width="80px">
+          <el-form-item label="节目ID">
+            <el-input v-model="id" disabled />
+          </el-form-item>
+          <!-- 动态显示选中组件的可编辑属性 -->
+          <template v-if="selectedComponent">
+            <div style="margin-bottom: 8px; font-weight: bold">组件属性</div>
+            <template v-for="[key, value] in Object.entries(selectedComponent || {})" :key="key">
+              <el-form-item v-if="showEditableProp(key)" :label="getPropLabel(key)">
+                <el-input v-model="selectedComponent[key]" />
+              </el-form-item>
+            </template>
+          </template>
+          <template v-else>
+            <div style="color: #bbb">请点击编辑区中的组件以编辑属性</div>
+          </template>
+        </el-form>
+      </div>
+      <hr class="property-divider" />
+      <div class="json-debug-title">当前JSON</div>
+      <el-input class="json-debug" type="textarea" :rows="8" :model-value="JSON.stringify(editorContent, null, 2)"
+        readonly />
+    </div>
+  </div>
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue';
-import { useRoute } from 'vue-router';
+import { ref, onMounted, computed, nextTick } from 'vue';
+import CanvasBoard from './component/CanvasBoard.vue';
+import { canvasPropNameMap } from './component/propNameMaps';
+import { useRoute, useRouter } from 'vue-router';
+import { ElMessage } from 'element-plus';
+import { ArrowLeft } from '@element-plus/icons-vue';
+import { getItemProgram } from '@/api/smsb/source/item_program';
 
 const route = useRoute();
+
+// 当前选中组件
+const selectedComponent = ref<any>(null);
+
+// 选中组件方法
+function selectComponent(item: any) {
+  selectedComponent.value = item;
+}
+
+// 左侧栏点击选中画布
+function selectCanvasFromSidebar() {
+  const canvas = editorContent.value.elements.find((el: any) => el.type === 'canvas');
+  if (canvas) {
+    selectedComponent.value = canvas;
+  }
+}
+
+// 属性栏显示哪些属性可编辑(可根据实际需求过滤)
+function showEditableProp(key: string) {
+  // 明确排除 type 字段,防止被编辑
+  return !['type', 'depth'].includes(key);
+}
+
+// 获取属性中文名
+function getPropLabel(key: string) {
+  if (selectedComponent.value?.type === 'canvas') {
+    return canvasPropNameMap[key] || key;
+  }
+  return key;
+}
+
+const router = useRouter();
 const id = ref(route.params.id);
+
+// 自动填充画布分辨率
+onMounted(async () => {
+  try {
+    const res = await getItemProgram(id.value);
+    const data = res.data;
+    let resolutionRatio = '';
+    if (data && data.resolutionRatio) {
+      resolutionRatio = data.resolutionRatio;
+    }
+    // 初始化 elements
+    let parsed = json_str ? JSON.parse(json_str) : { elements: [] };
+    editorContent.value = parsed;
+    // 只在拿到分辨率后调用一次 ensureCanvasAndDepth
+    ensureCanvasAndDepth(editorContent.value.elements, resolutionRatio);
+  } catch (e) {
+    // fallback: 初始化 elements 并插入默认画布
+    let parsed = json_str ? JSON.parse(json_str) : { elements: [] };
+    editorContent.value = parsed;
+    ensureCanvasAndDepth(editorContent.value.elements);
+  }
+});
+
+// 模拟 program 对象
+const program = ref<any>({});
+
+// 假设 json_str 为后端获取或 props 传入的 JSON 字符串
+let json_str = '';
+// TODO: 替换为实际获取方式
+// json_str = props.jsonStr || '';
+
+function ensureCanvasAndDepth(elements, resolutionRatio?: string) {
+  let width = 600,
+    height = 400;
+  if (resolutionRatio) {
+    const [w, h] = resolutionRatio.split('x').map(Number);
+    if (w && h) {
+      width = w;
+      height = h;
+      console.log('#136: ', width, height);
+    }
+  }
+  // 检查是否有 type: 'canvas' 的组件
+  let idx = elements.findIndex((el) => el.type === 'canvas');
+  if (idx === -1) {
+    console.log('#141: ', width, height);
+    elements.unshift({ type: 'canvas', width, height, bg: '#fff', depth: 0 });
+  } else {
+    let canvas = elements[idx];
+    let changed = false;
+    if (!canvas.width) {
+      canvas = { ...canvas, width };
+      changed = true;
+    }
+    if (!canvas.height) {
+      canvas = { ...canvas, height };
+      changed = true;
+    }
+    if (changed) {
+      elements[idx] = canvas; // 替换整个对象,确保响应式
+      console.log('#145: ', width, height, '响应式canvas:', canvas);
+    } else {
+      console.log('#145: ', width, height);
+    }
+  }
+  // 按 depth 排序,如果没有 depth 则补齐
+  elements.forEach((el, idx) => {
+    if (typeof el.depth !== 'number') {
+      el.depth = el.type === 'canvas' ? 0 : idx + 1;
+    }
+  });
+  elements.sort((a, b) => a.depth - b.depth);
+  return elements;
+}
+
+const editorContent = ref({ elements: [] });
+
+// editor-canvas 缩放逻辑
+const editorCanvasRef = ref<HTMLElement | null>(null);
+const containerSize = ref({ width: 0, height: 0 });
+
+function updateContainerSize() {
+  if (editorCanvasRef.value) {
+    containerSize.value.width = editorCanvasRef.value.clientWidth;
+    containerSize.value.height = editorCanvasRef.value.clientHeight;
+  }
+}
+
+onMounted(() => {
+  nextTick(updateContainerSize);
+  window.addEventListener('resize', updateContainerSize);
+});
+
+const canvas = computed(() =>
+  editorContent.value.elements.find((el: any) => el.type === 'canvas')
+);
+
+const canvasScale = computed(() => {
+  if (!canvas.value) return 1;
+  const cW = Number(canvas.value.width) || 600;
+  const cH = Number(canvas.value.height) || 400;
+  const boxW = containerSize.value.width;
+  const boxH = containerSize.value.height;
+  if (!boxW || !boxH) return 1;
+  return Math.min(boxW / cW, boxH / cH, 1);
+});
+
+try {
+  let parsed = json_str ? JSON.parse(json_str) : { elements: [] };
+  editorContent.value.elements = ensureCanvasAndDepth(parsed.elements || []);
+} catch (e) {
+  // fallback
+  editorContent.value.elements = ensureCanvasAndDepth([]);
+}
+
+const handleSave = () => {
+  program.value.content = JSON.stringify(editorContent.value);
+  ElMessage.success('保存成功,内容已写入 program');
+  // 可在此处调用 API 更新 program
+};
+
+const goBack = () => {
+  router.push('/source/program');
+};
 </script>
 
 <style scoped>
-.edit-program-page {
-    max-width: 600px;
-    margin: 40px auto;
-    padding: 24px;
-    background: #fff;
-    border-radius: 8px;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+.edit-program-layout {
+  display: flex;
+  flex-direction: row;
+  height: 100vh;
+  background: #f6f8fa;
+  min-width: 900px;
+}
+
+.sidebar {
+  width: 90px;
+  background: #232a36;
+  color: #fff;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 18px;
+  padding-bottom: 24px;
+  box-sizing: border-box;
+  height: 100vh;
+}
+
+.sidebar-title {
+  font-size: 16px;
+  margin-bottom: 18px;
+}
+
+.sidebar-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-bottom: 18px;
+  cursor: pointer;
+  width: 90%;
+}
+
+.sidebar-icon {
+  width: 34px;
+  height: 34px;
+  margin-bottom: 4px;
+}
+
+.sidebar-icon.text {
+  width: 34px;
+  height: 34px;
+  background: #fff;
+  color: #232a36;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-weight: bold;
+  font-size: 22px;
+  border-radius: 6px;
+  margin-bottom: 4px;
+}
+
+.main-editor {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  min-width: 0;
+  min-height: 0;
+  position: relative;
+}
+
+.editor-canvas {
+  width: 90%;
+  height: 75%;
+  background: #e9eef3;
+  border-radius: 10px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 24px;
+  box-shadow: 0 1px 8px rgba(0, 0, 0, 0.06);
+}
+
+.canvas-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.canvas-icon {
+  width: 120px;
+  height: 120px;
+  margin-bottom: 18px;
+}
+
+.canvas-text {
+  color: #222;
+  font-size: 22px;
+}
+
+.save-btn {
+  align-self: flex-end;
+  margin-right: 8vw;
+}
+
+.back-btn {
+  margin-bottom: 16px;
+  background: #fff;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
+  border: none;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.property-panel {
+  width: 260px;
+  background: #fff;
+  box-shadow: -2px 0 8px rgba(0, 0, 0, 0.03);
+  padding: 32px 18px 0 18px;
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  position: relative;
+  border-left: 1px solid #ececec;
+  box-sizing: border-box;
+  justify-content: flex-start;
+}
+
+.property-form-area {
+  flex: 0 0 auto;
+}
+
+.property-divider {
+  height: 1px;
+  background: #ececec;
+  margin: 18px 0 12px 0;
+  width: 100%;
+  border: none;
+}
+
+.json-debug-title {
+  margin-top: 30px;
+  font-size: 14px;
+  color: #888;
+  font-weight: bold;
+}
+
+.json-debug {
+  margin-top: 8px;
+  font-size: 13px;
+  background: #f6f8fa;
+  color: #222;
+  font-family: 'Fira Mono', 'Consolas', monospace;
+}
+
+.property-title {
+  font-size: 16px;
+  margin-bottom: 18px;
+}
+
+.canvas-default {
+  width: 600px;
+  height: 400px;
+  background: #fff;
+  margin: 0 auto;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 20px;
+  color: #aaa;
+  border-radius: 12px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+.component-item {
+  border: 2px solid #e3e3e3;
+  border-radius: 10px;
+  background: #fafbfc;
+  margin-bottom: 16px;
+  padding: 18px 0;
+  text-align: center;
+  cursor: pointer;
+  transition:
+    border-color 0.2s,
+    box-shadow 0.2s;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+  font-weight: 500;
+}
+
+.component-item.selected {
+  border-color: #409eff;
+  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.15);
+  background: #eaf6ff;
+}
+
+.component-icon-text {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  font-size: 18px;
+  color: #222;
+}
+
+.sidebar-item-disabled {
+  pointer-events: none;
+  opacity: 0.6;
 }
 </style>

+ 48 - 0
smsb-plus-ui/src/views/smsb/itemProgram/component/CanvasBoard.vue

@@ -0,0 +1,48 @@
+<template>
+  <div class="canvas-board-wrapper">
+    <div class="canvas-board" :style="canvasStyle">
+      <slot />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+interface Props {
+  width?: number | string;
+  height?: number | string;
+  bg?: string;
+  scale?: number;
+}
+const props = defineProps<Props>();
+const canvasStyle = computed(() => ({
+  width: typeof props.width === 'number' ? props.width + 'px' : props.width || '600px',
+  height: typeof props.height === 'number' ? props.height + 'px' : props.height || '400px',
+  background: props.bg || '#fff',
+  display: 'flex',
+  alignItems: 'center',
+  justifyContent: 'center',
+  fontSize: '20px',
+  color: '#aaa',
+  borderRadius: '12px',
+  boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
+  transform: props.scale && props.scale !== 1 ? `scale(${props.scale})` : undefined,
+  transformOrigin: 'center center'
+}));
+</script>
+
+<style scoped>
+.canvas-board-wrapper {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+  overflow: hidden;
+}
+
+.canvas-board {
+  transition: all 0.2s;
+}
+</style>

+ 5 - 0
smsb-plus-ui/src/views/smsb/itemProgram/component/propNameMaps.ts

@@ -0,0 +1,5 @@
+export const canvasPropNameMap = {
+  width: '宽度',
+  height: '高度',
+  bg: '背景'
+};

+ 191 - 196
smsb-plus-ui/src/views/smsb/itemProgram/index.vue

@@ -1,119 +1,114 @@
 <template>
-    <div class="p-2">
-        <transition :enter-active-class="proxy?.animate.searchAnimate.enter"
-            :leave-active-class="proxy?.animate.searchAnimate.leave">
-            <div v-show="showSearch" class="mb-[10px]">
-                <el-card shadow="hover">
-                    <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-                        <el-form-item label="节目ID" prop="programId">
-                            <el-input v-model="queryParams.programId" placeholder="请输入节目ID" clearable
-                                @keyup.enter="handleQuery" />
-                        </el-form-item>
-                        <el-form-item label="节目名称" prop="name">
-                            <el-input v-model="queryParams.name" placeholder="请输入节目名称" clearable
-                                @keyup.enter="handleQuery" />
-                        </el-form-item>
-                        <el-form-item label="分辨率" prop="resolutionRatio">
-                            <el-input v-model="queryParams.resolutionRatio" placeholder="请输入分辨率" clearable
-                                @keyup.enter="handleQuery" />
-                        </el-form-item>
-                        <el-form-item label="图片地址" prop="imgUrl">
-                            <el-input v-model="queryParams.imgUrl" placeholder="请输入图片地址" clearable
-                                @keyup.enter="handleQuery" />
-                        </el-form-item>
-                        <el-form-item label="节目时长" prop="duration">
-                            <el-input v-model="queryParams.duration" placeholder="请输入节目时长" clearable
-                                @keyup.enter="handleQuery" />
-                        </el-form-item>
-                        <el-form-item label="所属个人" prop="user">
-                            <el-input v-model="queryParams.user" placeholder="请输入所属个人" clearable
-                                @keyup.enter="handleQuery" />
-                        </el-form-item>
-                        <el-form-item>
-                            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-                            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-                        </el-form-item>
-                    </el-form>
-                </el-card>
-            </div>
-        </transition>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter"
+      :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="节目ID" prop="programId">
+              <el-input v-model="queryParams.programId" placeholder="请输入节目ID" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="节目名称" prop="name">
+              <el-input v-model="queryParams.name" placeholder="请输入节目名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="分辨率" prop="resolutionRatio">
+              <el-input v-model="queryParams.resolutionRatio" placeholder="请输入分辨率" clearable
+                @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="图片地址" prop="imgUrl">
+              <el-input v-model="queryParams.imgUrl" placeholder="请输入图片地址" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="节目时长" prop="duration">
+              <el-input v-model="queryParams.duration" placeholder="请输入节目时长" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="所属个人" prop="user">
+              <el-input v-model="queryParams.user" placeholder="请输入所属个人" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
 
-        <el-card shadow="never">
-            <template #header>
-                <el-row :gutter="10" class="mb8">
-                    <el-col :span="1.5">
-                        <el-button type="primary" plain icon="Plus" @click="handleAdd"
-                            v-hasPermi="['system:itemProgram:add']">新增</el-button>
-                    </el-col>
-                    <el-col :span="1.5">
-                        <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()"
-                            v-hasPermi="['system:itemProgram:edit']">修改</el-button>
-                    </el-col>
-                    <el-col :span="1.5">
-                        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()"
-                            v-hasPermi="['system:itemProgram:remove']">删除</el-button>
-                    </el-col>
-                    <el-col :span="1.5">
-                        <el-button type="warning" plain icon="Download" @click="handleExport"
-                            v-hasPermi="['system:itemProgram:export']">导出</el-button>
-                    </el-col>
-                    <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
-                </el-row>
-            </template>
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button type="primary" plain icon="Plus" @click="handleAdd"
+              v-hasPermi="['system:itemProgram:add']">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()"
+              v-hasPermi="['system:itemProgram:edit']">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()"
+              v-hasPermi="['system:itemProgram:remove']">删除</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="warning" plain icon="Download" @click="handleExport"
+              v-hasPermi="['system:itemProgram:export']">导出</el-button>
+          </el-col>
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
 
-            <el-table v-loading="loading" :data="itemProgramList" @selection-change="handleSelectionChange">
-                <el-table-column type="selection" width="55" align="center" />
-                <el-table-column label="节目ID" align="center" prop="programId" />
-                <el-table-column label="节目名称" align="center" prop="name" />
-                <el-table-column label="分辨率" align="center" prop="resolutionRatio" />
-                <el-table-column label="图片地址" align="center" prop="imgUrl" />
-                <el-table-column label="状态" align="center" prop="status" />
-                <el-table-column label="节目时长" align="center" prop="duration" />
-                <el-table-column label="所属个人" align="center" prop="user" />
-                <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
-                    <template #default="scope">
-                        <el-tooltip content="修改" placement="top">
-                            <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
-                                v-hasPermi="['system:itemProgram:edit']"></el-button>
-                        </el-tooltip>
-                        <el-tooltip content="删除" placement="top">
-                            <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
-                                v-hasPermi="['system:itemProgram:remove']"></el-button>
-                        </el-tooltip>
-                    </template>
-                </el-table-column>
-            </el-table>
+      <el-table v-loading="loading" :data="itemProgramList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="节目ID" align="center" prop="programId" />
+        <el-table-column label="节目名称" align="center" prop="name" />
+        <el-table-column label="分辨率" align="center" prop="resolutionRatio" />
+        <el-table-column label="图片地址" align="center" prop="imgUrl" />
+        <el-table-column label="状态" align="center" prop="status" />
+        <el-table-column label="节目时长" align="center" prop="duration" />
+        <el-table-column label="所属个人" align="center" prop="user" />
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
+                v-hasPermi="['system:itemProgram:edit']"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
+                v-hasPermi="['system:itemProgram:remove']"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
 
-            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
-                v-model:limit="queryParams.pageSize" @pagination="getList" />
-        </el-card>
-        <!-- 添加或修改节目信息对话框 -->
-        <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
-            <el-form ref="itemProgramFormRef" :model="form" :rules="rules" label-width="80px">
-                <el-form-item label="节目名称" prop="name">
-                    <el-input v-model="form.name" placeholder="请输入节目名称" />
-                </el-form-item>
-                <el-form-item label="分辨率" prop="resolutionRatio">
-                    <el-select v-model="form.resolutionRatio" placeholder="请选择分辨率或自定义" filterable allow-create
-                        :reserve-keyword="true">
-                        <el-option label="1920x1080" value="1920x1080" />
-                        <el-option label="1280x720" value="1280x720" />
-                        <el-option label="3840x2160" value="3840x2160" />
-                        <el-option label="1024x768" value="1024x768" />
-                    </el-select>
-                </el-form-item>
-                <el-form-item label="图片地址" prop="imgUrl">
-                    <el-input v-model="form.imgUrl" placeholder="请输入图片地址(可选)" />
-                </el-form-item>
-            </el-form>
-            <template #footer>
-                <div class="dialog-footer">
-                    <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
-                    <el-button @click="cancel">取 消</el-button>
-                </div>
-            </template>
-        </el-dialog>
-    </div>
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
+        v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+    <!-- 添加或修改节目信息对话框 -->
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+      <el-form ref="itemProgramFormRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="节目名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入节目名称" />
+        </el-form-item>
+        <el-form-item label="分辨率" prop="resolutionRatio">
+          <el-select v-model="form.resolutionRatio" placeholder="请选择分辨率或自定义" filterable allow-create
+            :reserve-keyword="true">
+            <el-option label="1920x1080" value="1920x1080" />
+            <el-option label="1280x720" value="1280x720" />
+            <el-option label="3840x2160" value="3840x2160" />
+            <el-option label="1024x768" value="1024x768" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="图片地址" prop="imgUrl">
+          <el-input v-model="form.imgUrl" placeholder="请输入图片地址(可选)" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
 <script setup name="ItemProgram" lang="ts">
@@ -136,146 +131,146 @@ const queryFormRef = ref<ElFormInstance>();
 const itemProgramFormRef = ref<ElFormInstance>();
 
 const dialog = reactive<DialogOption>({
-    visible: false,
-    title: ''
+  visible: false,
+  title: ''
 });
 
 const initFormData: ItemProgramForm = {
-    id: undefined,
+  id: undefined,
+  programId: undefined,
+  name: undefined,
+  resolutionRatio: undefined,
+  imgUrl: undefined
+  // 已移除 create_dept 字段
+};
+const data = reactive<PageData<ItemProgramForm, ItemProgramQuery>>({
+  form: { ...initFormData },
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
     programId: undefined,
     name: undefined,
     resolutionRatio: undefined,
-    imgUrl: undefined
-    // 已移除 create_dept 字段
-};
-const data = reactive<PageData<ItemProgramForm, ItemProgramQuery>>({
-    form: { ...initFormData },
-    queryParams: {
-        pageNum: 1,
-        pageSize: 10,
-        programId: undefined,
-        name: undefined,
-        resolutionRatio: undefined,
-        imgUrl: undefined,
-        params: {}
-    },
-    rules: {
-        name: [{ required: true, message: '节目名称不能为空', trigger: 'blur' }],
-        resolutionRatio: [
-            { required: true, message: '分辨率不能为空', trigger: 'blur' },
-            { pattern: /^\d+x\d+$/, message: '请输入如1920x1080的分辨率', trigger: 'blur' }
-        ]
-    }
+    imgUrl: undefined,
+    params: {}
+  },
+  rules: {
+    name: [{ required: true, message: '节目名称不能为空', trigger: 'blur' }],
+    resolutionRatio: [
+      { required: true, message: '分辨率不能为空', trigger: 'blur' },
+      { pattern: /^\d+x\d+$/, message: '请输入如1920x1080的分辨率', trigger: 'blur' }
+    ]
+  }
 });
 
 const { queryParams, form, rules } = toRefs(data);
 
 /** 查询节目信息列表 */
 const getList = async () => {
-    loading.value = true;
-    const res = await listItemProgram(queryParams.value);
-    itemProgramList.value = res.rows;
-    total.value = res.total;
-    loading.value = false;
+  loading.value = true;
+  const res = await listItemProgram(queryParams.value);
+  itemProgramList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
 };
 
 /** 取消按钮 */
 const cancel = () => {
-    reset();
-    dialog.visible = false;
+  reset();
+  dialog.visible = false;
 };
 
 /** 表单重置 */
 const userStore = useUserStore();
 const reset = () => {
-    form.value = { ...initFormData };
-    // 自动赋值当前用户和租户ID(字段名按实际接口)
-    form.value.user = String(userStore.userId);
-    form.value.tenantId = userStore.tenantId;
-    // 新增时自动生成节目ID
-    if (!form.value.programId) {
-        form.value.programId = Date.now().toString();
-    }
-    itemProgramFormRef.value?.resetFields();
+  form.value = { ...initFormData };
+  // 自动赋值当前用户和租户ID(字段名按实际接口)
+  form.value.user = String(userStore.userId);
+  form.value.tenantId = userStore.tenantId;
+  // 新增时自动生成节目ID
+  if (!form.value.programId) {
+    form.value.programId = Date.now().toString();
+  }
+  itemProgramFormRef.value?.resetFields();
 };
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
-    queryParams.value.pageNum = 1;
-    getList();
+  queryParams.value.pageNum = 1;
+  getList();
 };
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-    queryFormRef.value?.resetFields();
-    handleQuery();
+  queryFormRef.value?.resetFields();
+  handleQuery();
 };
 
 /** 多选框选中数据 */
 const handleSelectionChange = (selection: ItemProgramVO[]) => {
-    ids.value = selection.map((item) => item.id);
-    single.value = selection.length != 1;
-    multiple.value = !selection.length;
+  ids.value = selection.map((item) => item.id);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
 };
 
 /** 新增按钮操作 */
 const handleAdd = () => {
-    reset();
-    dialog.visible = true;
-    dialog.title = '添加节目信息';
+  reset();
+  dialog.visible = true;
+  dialog.title = '添加节目信息';
 };
 
 /** 修改按钮操作 */
 const handleUpdate = (row?: ItemProgramVO) => {
-    const _id = row?.id || ids.value[0];
-    if (_id) {
-        // 跳转到可视化编辑页面
-        window.$router ? window.$router.push(`/smsb/itemProgram/edit/${_id}`) : (proxy?.$router.push(`/smsb/itemProgram/edit/${_id}`));
-    }
+  const _id = row?.id || ids.value[0];
+  if (_id) {
+    // 跳转到可视化编辑页面
+    window.$router ? window.$router.push(`/smsb/itemProgram/edit/${_id}`) : proxy?.$router.push(`/smsb/itemProgram/edit/${_id}`);
+  }
 };
 
 /** 提交按钮 */
 const submitForm = () => {
-    // 新增时自动生成节目ID
-    if (!form.value.programId) {
-        form.value.programId = Date.now().toString();
+  // 新增时自动生成节目ID
+  if (!form.value.programId) {
+    form.value.programId = Date.now().toString();
+  }
+  itemProgramFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      if (form.value.id) {
+        await updateItemProgram(form.value).finally(() => (buttonLoading.value = false));
+      } else {
+        await addItemProgram(form.value).finally(() => (buttonLoading.value = false));
+      }
+      proxy?.$modal.msgSuccess('操作成功');
+      dialog.visible = false;
+      await getList();
     }
-    itemProgramFormRef.value?.validate(async (valid: boolean) => {
-        if (valid) {
-            buttonLoading.value = true;
-            if (form.value.id) {
-                await updateItemProgram(form.value).finally(() => (buttonLoading.value = false));
-            } else {
-                await addItemProgram(form.value).finally(() => (buttonLoading.value = false));
-            }
-            proxy?.$modal.msgSuccess('操作成功');
-            dialog.visible = false;
-            await getList();
-        }
-    });
+  });
 };
 
 /** 删除按钮操作 */
 const handleDelete = async (row?: ItemProgramVO) => {
-    const _ids = row?.id || ids.value;
-    await proxy?.$modal.confirm('是否确认删除节目信息编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
-    await delItemProgram(_ids);
-    proxy?.$modal.msgSuccess('删除成功');
-    await getList();
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除节目信息编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
+  await delItemProgram(_ids);
+  proxy?.$modal.msgSuccess('删除成功');
+  await getList();
 };
 
 /** 导出按钮操作 */
 const handleExport = () => {
-    proxy?.download(
-        'system/itemProgram/export',
-        {
-            ...queryParams.value
-        },
-        `itemProgram_${new Date().getTime()}.xlsx`
-    );
+  proxy?.download(
+    'system/itemProgram/export',
+    {
+      ...queryParams.value
+    },
+    `itemProgram_${new Date().getTime()}.xlsx`
+  );
 };
 
 onMounted(() => {
-    getList();
+  getList();
 });
 </script>