index.vue 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875
  1. <template>
  2. <div class="p-2">
  3. <transition :enter-active-class="proxy?.animate.searchAnimate.enter"
  4. :leave-active-class="proxy?.animate.searchAnimate.leave">
  5. <div v-show="showSearch" class="mb-[10px]">
  6. <el-card shadow="hover" :style="{ height: '70px' }">
  7. <el-row :gutter="20" align="middle">
  8. <!-- 节目总数 -->
  9. <el-col :span="8" class="statistic-col">
  10. <el-statistic :value="totalNum">
  11. <template #title>
  12. <div class="stat-title">节目总数</div>
  13. </template>
  14. </el-statistic>
  15. </el-col>
  16. <!-- 轮播总数 -->
  17. <el-col :span="8" class="statistic-col">
  18. <el-statistic :value="lbNum">
  19. <template #title>
  20. <div class="stat-title">轮播总数</div>
  21. </template>
  22. </el-statistic>
  23. </el-col>
  24. <!-- 分屏总数 -->
  25. <el-col :span="8" class="statistic-col">
  26. <el-statistic :value="jmNum">
  27. <template #title>
  28. <div class="stat-title">分屏总数</div>
  29. </template>
  30. </el-statistic>
  31. </el-col>
  32. </el-row>
  33. </el-card>
  34. <el-card shadow="hover" :style="{ marginTop: '10px', height: '60px' }">
  35. <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="40px">
  36. <el-form-item label="名称" prop="itemName">
  37. <el-input v-model="queryParams.itemName" style="width: 150px" placeholder="请输入名称" clearable
  38. @keyup.enter="handleQuery" />
  39. </el-form-item>
  40. <!-- <el-form-item label="类型" prop="itemType">
  41. <el-select v-model="queryParams.itemType" style="width: 150px" placeholder="请选择类型" clearable>
  42. <el-option v-for="dict in smsb_item_type" :key="dict.value" :label="dict.label" :value="dict.value" />
  43. </el-select>
  44. </el-form-item>-->
  45. <el-form-item label="时间" style="width: 250px">
  46. <el-date-picker v-model="dateRangeCreateTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange"
  47. range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"
  48. :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"></el-date-picker>
  49. </el-form-item>
  50. <el-form-item>
  51. <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
  52. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  53. <el-button type="primary" plain icon="Plus" @click="handleAddL" v-hasPermi="['source:item:add']"> 新增轮播组
  54. </el-button>
  55. <!-- <el-button type="primary" plain icon="Plus" @click="handleAddJ" v-hasPermi="['source:item:add']"> 新增分屏组
  56. </el-button>-->
  57. <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()"
  58. v-hasPermi="['source:item:edit']">修改
  59. </el-button>
  60. <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()"
  61. v-hasPermi="['source:item:remove']">删除
  62. </el-button>
  63. <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['source:item:export']">
  64. 导出
  65. </el-button>
  66. </el-form-item>
  67. </el-form>
  68. </el-card>
  69. </div>
  70. </transition>
  71. <el-card shadow="never">
  72. <div class="table-content">
  73. <el-table v-loading="mainLoading" :data="itemList" @selection-change="handleSelectionChange">
  74. <el-table-column type="selection" width="55" align="center" />
  75. <el-table-column label="ID" align="left" prop="id" v-if="true" width="180" :show-overflow-tooltip="true" />
  76. <el-table-column label="名称" align="left" prop="itemName" />
  77. <el-table-column label="类型" align="center" prop="itemType" width="120">
  78. <template #default="scope">
  79. <dict-tag :options="smsb_item_type" :value="scope.row.itemType" />
  80. </template>
  81. </el-table-column>
  82. <!-- <el-table-column label="分屏" align="center" prop="splitScreen" width="120">
  83. <template #default="scope">
  84. <span v-if="scope.row.splitScreen == 0"> -&#45;&#45; </span>
  85. <dict-tag v-else :options="smsb_split_screen" :value="scope.row.splitScreen" />
  86. </template>
  87. </el-table-column>-->
  88. <el-table-column label="资源数量" align="center" prop="sourceNum" width="100" />
  89. <el-table-column label="创建人" align="left" prop="createUser" width="120" :show-overflow-tooltip="true" />
  90. <el-table-column label="创建时间" align="left" prop="createTime" width="160" />
  91. <el-table-column label="更新时间" align="left" prop="updateTime" width="160" />
  92. <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120">
  93. <template #default="scope">
  94. <el-tooltip content="修改" placement="top">
  95. <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
  96. v-hasPermi="['source:item:edit']"></el-button>
  97. </el-tooltip>
  98. <el-tooltip content="删除" placement="top">
  99. <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
  100. v-hasPermi="['source:item:remove']"></el-button>
  101. </el-tooltip>
  102. <el-tooltip content="编辑历史" placement="top">
  103. <el-button link type="primary" icon="Clock" @click="onShowEditHistory(scope.row)"></el-button>
  104. </el-tooltip>
  105. </template>
  106. </el-table-column>
  107. <!-- 编辑历史弹窗 -->
  108. <el-dialog title="编辑历史" v-model="editHistoryDialog.visible" width="800px" append-to-body>
  109. <div class="edit-history-container" v-html="editHistoryDialog.tableHtml"></div>
  110. </el-dialog>
  111. </el-table>
  112. </div>
  113. <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
  114. v-model:limit="queryParams.pageSize" @pagination="getList" />
  115. </el-card>
  116. <!-- 添加或修改轮播组对话框 -->
  117. <el-dialog :title="dialog.title" v-model="dialog.visible" width="1400px" append-to-body @close="onDialogClose">
  118. <div class="dialog-container">
  119. <!-- 左侧文件列表 -->
  120. <div class="table-container">
  121. <!-- 界面名称输入框 -->
  122. <el-input v-model="itemName" placeholder="请输入轮播组名称" class="interface-input"></el-input>
  123. <el-table v-loading="dialogLoading" ref="fileTable" :data="minioDataList" reserve-selection row-key="id"
  124. @selection-change="handleSelectionFile" @select="handleSelect" @select-all="handleSelectAll">
  125. <el-table-column type="selection" width="55" header-align="center" />
  126. <el-table-column label="类型" header-align="center" prop="type" width="80">
  127. <template #default="scope">
  128. <dict-tag :options="smsb_source_type" :value="scope.row.type" />
  129. </template>
  130. </el-table-column>
  131. <el-table-column label="原名" header-align="left" prop="originalName" width="150"
  132. :show-overflow-tooltip="true" />
  133. <el-table-column label="大小" header-align="center" prop="size" />
  134. <el-table-column label="时长" header-align="center" prop="duration" />
  135. <el-table-column label="截图" header-align="center" prop="screenshot">
  136. <template #default="scope">
  137. <image-preview :src="scope.row.screenshot" style="width: 40px; height: 40px; cursor: pointer" />
  138. </template>
  139. </el-table-column>
  140. </el-table>
  141. <pagination v-show="fileTotal > 0" :total="fileTotal" v-model:page="dialogQueryParams.pageNum"
  142. v-model:limit="dialogQueryParams.pageSize" @pagination="getFileList" />
  143. </div>
  144. <!-- 右侧选中文件列表 -->
  145. <div class="selected-container">
  146. <!-- 自定义表头 -->
  147. <div class="draggable-header"
  148. style="display: flex; align-items: center; background: #fafafa; border-bottom: 1px solid #eee; min-height: 48px; font-weight: bold">
  149. <span style="width: 80px; text-align: center">排序</span>
  150. <span style="flex: 1">文件名</span>
  151. <span style="width: 120px; margin-left: 8px">播放时长</span>
  152. </div>
  153. <!-- 拖拽列表体 -->
  154. <draggable v-model="selectedFiles" item-key="id" @end="onSelectedFilesDragEnd" :animation="200" tag="div">
  155. <template #item="{ element, index }">
  156. <div class="draggable-row" :draggable="true"
  157. style="display: flex; align-items: center; border-bottom: 1px solid #eee; min-height: 48px; cursor: move">
  158. <span class="order-number" style="width: 80px; text-align: center">{{ element.order }}</span>
  159. <el-tooltip effect="dark" :content="element.name" placement="top">
  160. <span style="
  161. flex: 1;
  162. max-width: 380px;
  163. overflow: hidden;
  164. text-overflow: ellipsis;
  165. white-space: nowrap;
  166. display: inline-block;
  167. vertical-align: middle;
  168. ">{{ element.name }}</span>
  169. </el-tooltip>
  170. <el-input-number :disabled="element.type !== 1" v-model="element.duration" :min="1" :max="300"
  171. style="width: 120px; margin-left: 8px" />
  172. </div>
  173. </template>
  174. </draggable>
  175. <!-- 空数据提示 -->
  176. <div v-if="selectedFiles.length === 0" style="text-align: center; color: #999; padding: 16px 0">暂无数据</div>
  177. </div>
  178. </div>
  179. <template #footer>
  180. <div class="dialog-footer">
  181. <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
  182. <el-button @click="cancel">取 消</el-button>
  183. </div>
  184. </template>
  185. </el-dialog>
  186. <!-- 添加或修改分屏组对话框 -->
  187. <el-dialog :title="splitDialog.title" v-model="splitDialog.visible" width="1200px" append-to-body>
  188. <div class="dialog-container">
  189. <!-- 左侧示意图 -->
  190. <div class="table-container">示意图</div>
  191. <!-- 右侧基础信息 -->
  192. <div class="selected-container">
  193. <el-form ref="itemFormRef" :model="form" :rules="rules" label-width="60px">
  194. <el-form-item label="名称" prop="itemName">
  195. <el-input v-model="form.itemName" placeholder="请输入分屏组名称" />
  196. </el-form-item>
  197. <el-row>
  198. <el-col :span="10">
  199. <el-form-item label="分辨率" prop="width">
  200. <el-input-number v-model="form.width" controls-position="right" placeholder="请输入宽度" :min="0"
  201. style="width: 180px" maxlength="6" />
  202. </el-form-item>
  203. </el-col>
  204. <el-col :span="10">
  205. <el-form-item label="" prop="height" width="10px">
  206. <el-input-number v-model="form.height" controls-position="right" placeholder="请输入高度" :min="0"
  207. style="width: 180px" maxlength="6" />
  208. </el-form-item>
  209. </el-col>
  210. </el-row>
  211. <el-form-item label="分屏" prop="splitScreen">
  212. <el-radio-group v-model="form.splitScreen">
  213. <el-radio v-for="dict in smsb_split_screen" :key="dict.value" :value="parseInt(dict.value)">
  214. {{ dict.label }}
  215. </el-radio>
  216. </el-radio-group>
  217. </el-form-item>
  218. <el-form-item label="坐标" prop="position">
  219. <el-row>
  220. <el-col :span="12">
  221. <el-input-number v-model="position1" controls-position="right" placeholder="请输入坐标1" :min="0"
  222. style="width: 180px" maxlength="6" />
  223. </el-col>
  224. <el-col :span="12">
  225. <el-input-number v-model="position2" controls-position="right" placeholder="请输入坐标2" :min="0"
  226. style="width: 180px; margin-left: 15px" maxlength="6" v-if="form.splitScreen !== 2" />
  227. </el-col>
  228. </el-row>
  229. </el-form-item>
  230. <el-form-item label="宽高" prop="position" v-if="form.splitScreen === 4">
  231. <el-row>
  232. <el-col :span="12">
  233. <el-input-number v-model="positionW" controls-position="right" placeholder="请输入坐标1" :min="0"
  234. style="width: 180px" maxlength="6" />
  235. </el-col>
  236. <el-col :span="12">
  237. <el-input-number v-model="positionH" controls-position="right" placeholder="请输入坐标2" :min="0"
  238. style="width: 180px; margin-left: 15px" maxlength="6" />
  239. </el-col>
  240. </el-row>
  241. </el-form-item>
  242. <el-row>
  243. <el-col :span="12">
  244. <el-form-item label="跑马灯" prop="hasPmd">
  245. <el-radio-group v-model="form.hasPmd">
  246. <el-radio v-for="dict in smsb_yes_no" :key="dict.value" :value="parseInt(dict.value)">{{ dict.label
  247. }}
  248. </el-radio>
  249. </el-radio-group>
  250. </el-form-item>
  251. </el-col>
  252. <el-col :span="12">
  253. <el-form-item label="位置" prop="positionPmd">
  254. <el-radio-group v-model="form.positionPmd">
  255. <el-radio-button label="上方" value="1" />
  256. <el-radio-button label="下方" value="2" />
  257. </el-radio-group>
  258. </el-form-item>
  259. </el-col>
  260. </el-row>
  261. <el-form-item label="内容" prop="contentPmd">
  262. <el-input v-model="form.contentPmd" type="textarea" placeholder="请输入跑马灯内容" />
  263. </el-form-item>
  264. <el-row>
  265. <el-col :span="12">
  266. <el-form-item label="天气" prop="hasWeather">
  267. <el-radio-group v-model="form.hasWeather">
  268. <el-radio v-for="dict in smsb_yes_no" :key="dict.value" :value="parseInt(dict.value)">{{ dict.label
  269. }}
  270. </el-radio>
  271. </el-radio-group>
  272. </el-form-item>
  273. </el-col>
  274. <el-col :span="12">
  275. <el-form-item label="时间" prop="hasTime">
  276. <el-radio-group v-model="form.hasTime">
  277. <el-radio v-for="dict in smsb_yes_no" :key="dict.value" :value="parseInt(dict.value)">{{ dict.label
  278. }}
  279. </el-radio>
  280. </el-radio-group>
  281. </el-form-item>
  282. </el-col>
  283. </el-row>
  284. </el-form>
  285. </div>
  286. </div>
  287. <template #footer>
  288. <div class="dialog-footer">
  289. <el-button :loading="buttonLoading" type="primary" @click="submitSplit">确 定</el-button>
  290. <el-button @click="cancel">取 消</el-button>
  291. </div>
  292. </template>
  293. </el-dialog>
  294. </div>
  295. </template>
  296. <script setup name="Item" lang="ts">
  297. import draggable from 'vuedraggable';
  298. import { onActivated, reactive } from 'vue';
  299. import { getEditHistory } from '@/api/smsb/source/item';
  300. import type { EditHistoryVo, ItemVO, ItemForm } from '@/api/smsb/source/item_type';
  301. // 编辑历史弹窗数据
  302. const editHistoryDialog = reactive({
  303. visible: false,
  304. list: [] as EditHistoryVo[],
  305. type: 1 as 1 | 2,
  306. tableHtml: ref('')
  307. });
  308. /**
  309. * 打开编辑历史弹窗
  310. * @param row 当前行数据
  311. */
  312. function onShowEditHistory(row: ItemVO) {
  313. editHistoryDialog.visible = true;
  314. editHistoryDialog.list = [];
  315. // type: 1=轮播组, 2=分屏组
  316. const type = row.itemType === 1 ? 1 : 2;
  317. editHistoryDialog.type = type;
  318. getEditHistory(row.id, type).then((res) => {
  319. console.log('编辑历史数据:', res.data);
  320. let list = res.data || [];
  321. if (type === 1) {
  322. // 轮播组过滤掉无selectedFiles的异常数据(第一条保留)
  323. list = list.filter((item: any, idx: number, arr: any[]) => {
  324. if (idx === 0) return true;
  325. try {
  326. const curr = JSON.parse(item.operParam || '{}');
  327. const prev = idx > 0 ? JSON.parse(arr[idx - 1].operParam || '{}') : {};
  328. // 没有selectedFiles字段直接过滤
  329. if (!Array.isArray(curr.selectedFiles)) return false;
  330. // diff结果为无变化也过滤
  331. const diff = diffSelectedFiles({ selectedFiles: prev.selectedFiles || [] }, { selectedFiles: curr.selectedFiles || [] }, 'selectedFiles');
  332. return diff && diff !== '无变化';
  333. } catch {
  334. return false;
  335. }
  336. });
  337. }
  338. editHistoryDialog.list = list;
  339. // 使用新的渲染函数生成表格HTML
  340. editHistoryDialog.tableHtml = renderOperHistoryTable(list, type);
  341. console.log('编辑历史数据处理后:', list);
  342. });
  343. }
  344. function getFileNameSet(selectedFiles?: any[]): Set<string> {
  345. if (!Array.isArray(selectedFiles)) return new Set();
  346. return new Set(selectedFiles.map((f) => f.name));
  347. }
  348. function diffSelectedFiles(prev: any, curr: any, key: string) {
  349. const prevSet = getFileNameSet(prev?.[key]);
  350. const currSet = getFileNameSet(curr?.[key]);
  351. const added = [...currSet].filter((x) => !prevSet.has(x));
  352. const removed = [...prevSet].filter((x) => !currSet.has(x));
  353. let result = '';
  354. if (added.length) result += `新增:${added.join(',')}`;
  355. if (removed.length) result += `${added.length ? ' ' : ''}减少:${removed.join(',')}`;
  356. return result || '无变化';
  357. }
  358. function renderOperParam(row: EditHistoryVo, index: number, list: EditHistoryVo[], type: 1 | 2) {
  359. // 轮播组和分屏组都采用diff方式展示
  360. let curr, prev;
  361. // 轮播组只处理selectedFiles字段,分屏组处理每一屏
  362. if (type === 1) {
  363. try {
  364. curr = JSON.parse(row.operParam || '{}');
  365. } catch {
  366. curr = {};
  367. }
  368. try {
  369. prev = index > 0 ? JSON.parse(list[index - 1].operParam || '{}') : {};
  370. } catch {
  371. prev = {};
  372. }
  373. // 只要没有selectedFiles字段就视为异常数据,不显示
  374. if (!Array.isArray(curr.selectedFiles)) {
  375. return '';
  376. }
  377. const currFiles = curr.selectedFiles || [];
  378. const prevFiles = prev.selectedFiles || [];
  379. if (index === 0) {
  380. // 第一条记录,全部按“创建”显示
  381. return `<span style='color:#409EFF;font-weight:bold;'>创建</span>`;
  382. }
  383. const diff = diffSelectedFiles({ selectedFiles: prevFiles }, { selectedFiles: currFiles }, 'selectedFiles');
  384. if (diff && diff !== '无变化') {
  385. let styled = diff
  386. .replace(/新增:/g, "<span style='color:#67C23A;font-weight:bold;'>新增:</span>")
  387. .replace(/减少:/g, "<span style='color:#F56C6C;font-weight:bold;'>减少:</span>");
  388. return `<span style='font-weight:bold;'>文件</span>:${styled}`;
  389. }
  390. return '';
  391. }
  392. // 分屏组diff逻辑修正,严格一一对应
  393. const screenFields = ['selectedFiles1', 'selectedFiles2', 'selectedFiles3', 'selectedFiles4'];
  394. const screenNames = ['第一屏', '第二屏', '第三屏', '第四屏'];
  395. try {
  396. curr = JSON.parse(row.operParam || '{}');
  397. } catch {
  398. curr = {};
  399. }
  400. try {
  401. prev = index > 0 ? JSON.parse(list[index - 1].operParam || '{}') : {};
  402. } catch {
  403. prev = {};
  404. }
  405. if (index === 0) {
  406. // 第一条记录,固定显示“创建”
  407. return `<span style='color:#409EFF;font-weight:bold;'>创建</span>`;
  408. }
  409. const diffs = screenFields.map((field, idx) => {
  410. const currFiles = curr[field] || [];
  411. const prevFiles = prev[field] || [];
  412. if (!Array.isArray(currFiles) && !Array.isArray(prevFiles)) return '';
  413. const diff = diffSelectedFiles({ selectedFiles: prevFiles }, { selectedFiles: currFiles }, 'selectedFiles');
  414. if (diff && diff !== '无变化') {
  415. let styled = diff
  416. .replace(/新增:/g, "<span style='color:#67C23A;font-weight:bold;'>新增:</span>")
  417. .replace(/减少:/g, "<span style='color:#F56C6C;font-weight:bold;'>减少:</span>");
  418. return `<span style='font-weight:bold;'>${screenNames[idx]}</span>:${styled}`;
  419. }
  420. return '';
  421. }).filter(Boolean);
  422. return diffs.length ? diffs.join('<br/>') : '';
  423. }
  424. import { listItem, getItem, delItem, addItem, updateItem, itemStatistics } from '@/api/smsb/source/item';
  425. import { MinioDataQuery, MinioDataVO } from '@/api/smsb/source/minioData_type';
  426. import { listMinioData } from '@/api/smsb/source/minioData';
  427. import { renderOperHistoryTable } from './renderOperHistoryTable';
  428. import { nextTick } from 'vue';
  429. import type { ElTable } from 'element-plus';
  430. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  431. const { smsb_item_type, smsb_split_screen, smsb_source_type, smsb_yes_no } = toRefs<any>(
  432. proxy?.useDict('smsb_item_type', 'smsb_split_screen', 'smsb_source_type', 'smsb_yes_no')
  433. );
  434. const itemList = ref<ItemVO[]>([]);
  435. const buttonLoading = ref(false);
  436. const mainLoading = ref(true); // 主表 loading
  437. const dialogLoading = ref(false); // dialog loading
  438. const showSearch = ref(true);
  439. const ids = ref<Array<string | number>>([]);
  440. const single = ref(true);
  441. const multiple = ref(true);
  442. const total = ref(0);
  443. const fileTotal = ref(0);
  444. const totalNum = ref(0);
  445. const lbNum = ref(0);
  446. const jmNum = ref(0);
  447. const position1 = ref(0);
  448. const position2 = ref(0);
  449. const positionW = ref(0);
  450. const positionH = ref(0);
  451. const itemName = ref<string>('');
  452. const queryFormRef = ref<ElFormInstance>();
  453. const itemFormRef = ref<ElFormInstance>();
  454. const fileTable = ref<InstanceType<typeof ElTable>>();
  455. const minioDataList = ref<MinioDataVO[]>([]);
  456. const dateRangeCreateTime = ref<[DateModelType, DateModelType]>(['', '']);
  457. // 选中的文件
  458. const selectedFiles = ref<{ id: number; name: string; duration: number; order: number; type: number }[]>([]);
  459. const dialog = reactive<DialogOption>({
  460. visible: false,
  461. title: ''
  462. });
  463. const splitDialog = reactive<DialogOption>({
  464. visible: false,
  465. title: ''
  466. });
  467. const initFormData: ItemForm = {
  468. itemName: undefined,
  469. itemType: undefined,
  470. splitScreen: 2,
  471. selectedFiles: undefined,
  472. width: undefined,
  473. height: undefined,
  474. background: undefined,
  475. position: undefined,
  476. hasPmd: 0,
  477. positionPmd: '1',
  478. contentPmd: undefined,
  479. hasWeather: 0,
  480. hasTime: 0
  481. };
  482. const queryParams = ref({
  483. pageNum: 1,
  484. pageSize: 10,
  485. itemName: undefined,
  486. itemType: 1,
  487. splitScreen: undefined,
  488. createUser: undefined,
  489. params: {}
  490. });
  491. const form = ref({ ...initFormData });
  492. const rules = {
  493. itemName: [{ required: true, message: '请输入名称', trigger: 'blur' }],
  494. itemType: [{ required: true, message: '请选择类型', trigger: 'change' }]
  495. // 可按需补充其它字段校验
  496. };
  497. const dialogQueryParams = ref({
  498. pageNum: 1,
  499. pageSize: 10,
  500. params: {}
  501. });
  502. /** 查询节目管理列表 */
  503. const getList = async () => {
  504. mainLoading.value = true;
  505. try {
  506. const res = await listItem(proxy?.addDateRange(queryParams.value, dateRangeCreateTime.value, 'CreateTime'));
  507. console.log('getList result:', res);
  508. itemList.value = res.rows;
  509. total.value = res.total;
  510. } catch (e: any) {
  511. proxy?.$modal.msgError(e?.message || '获取列表失败');
  512. } finally {
  513. mainLoading.value = false;
  514. }
  515. };
  516. /** 查询文件资源列表 */
  517. const getFileList = async () => {
  518. dialogLoading.value = true;
  519. try {
  520. // console.log('分页参数:', dialogQueryParams.value);
  521. const res = await listMinioData(dialogQueryParams.value);
  522. minioDataList.value = res.rows.map((data) => ({
  523. ...data,
  524. size: (parseFloat(data.size) / 1024).toFixed(3) + 'MB'
  525. }));
  526. fileTotal.value = res.total;
  527. // After data loads, restore selections
  528. await nextTick();
  529. if (selectedFiles.value?.length) {
  530. const selectedIds = new Set(selectedFiles.value.map((f) => f.id));
  531. minioDataList.value.forEach((row) => {
  532. if (selectedIds.has(row.id)) {
  533. fileTable.value?.toggleRowSelection(row, true);
  534. }
  535. });
  536. // console.log('本页高亮选中:', [...selectedIds]);
  537. }
  538. } finally {
  539. dialogLoading.value = false;
  540. }
  541. };
  542. /** 多选框选中文件数据 */
  543. // selection-change 只做新增
  544. const handleSelectionFile = (selection: MinioDataVO[]) => {
  545. selection.forEach((item) => {
  546. if (!selectedFiles.value.some((f) => String(f.id) === String(item.id))) {
  547. selectedFiles.value.push({
  548. id: item.id,
  549. name: item.originalName,
  550. type: item.type,
  551. duration: item.type === 1 ? 10 : item.duration,
  552. order: 0
  553. });
  554. }
  555. });
  556. // 重新排序
  557. selectedFiles.value = selectedFiles.value.map((f, idx) => ({ ...f, order: idx + 1 }));
  558. };
  559. // 取消单个选中
  560. const handleSelect = (selection: MinioDataVO[], row: MinioDataVO) => {
  561. if (!selection.some((item) => String(item.id) === String(row.id))) {
  562. selectedFiles.value = selectedFiles.value.filter((f) => String(f.id) !== String(row.id));
  563. selectedFiles.value = selectedFiles.value.map((f, idx) => ({ ...f, order: idx + 1 }));
  564. }
  565. };
  566. // 取消全选
  567. const handleSelectAll = (selection: MinioDataVO[]) => {
  568. const currentPageIds = new Set(minioDataList.value.map((item) => String(item.id)));
  569. const selectedIds = new Set(selection.map((item) => String(item.id)));
  570. selectedFiles.value = selectedFiles.value.filter((f) => !currentPageIds.has(String(f.id)) || selectedIds.has(String(f.id)));
  571. selectedFiles.value = selectedFiles.value.map((f, idx) => ({ ...f, order: idx + 1 }));
  572. };
  573. const cancel = () => {
  574. reset();
  575. dialog.visible = false;
  576. splitDialog.visible = false;
  577. onDialogClose();
  578. };
  579. /** 新建轮播组弹窗关闭时清空选中文件 */
  580. const onDialogClose = () => {
  581. selectedFiles.value = [];
  582. };
  583. /** 表单重置 */
  584. const reset = () => {
  585. form.value = { ...initFormData };
  586. itemName.value = '';
  587. itemFormRef.value?.resetFields();
  588. };
  589. /** 搜索按钮操作 */
  590. const handleQuery = () => {
  591. queryParams.value.pageNum = 1;
  592. getList();
  593. };
  594. /** 重置按钮操作 */
  595. const resetQuery = () => {
  596. queryFormRef.value?.resetFields();
  597. handleQuery();
  598. };
  599. /** 多选框选中数据 */
  600. const handleSelectionChange = (selection: ItemVO[]) => {
  601. ids.value = selection.map((item) => item.id);
  602. single.value = selection.length != 1;
  603. multiple.value = !selection.length;
  604. };
  605. /** 新增轮播按钮操作 */
  606. const handleAddL = () => {
  607. reset();
  608. dialog.visible = true;
  609. dialog.title = '新增轮播组';
  610. form.value.itemType = 1;
  611. getFileList();
  612. };
  613. /** 新增节目按钮操作 */
  614. const handleAddJ = () => {
  615. reset();
  616. splitDialog.visible = true;
  617. splitDialog.title = '新增分屏组';
  618. form.value.itemType = 2;
  619. };
  620. /** 修改按钮操作 */
  621. const handleUpdate = async (row?: ItemVO) => {
  622. reset();
  623. try {
  624. const _id = row?.id || ids.value[0];
  625. const res = await getItem(_id);
  626. if (1 === res.data.itemType) {
  627. Object.assign(form.value, res.data);
  628. itemName.value = res.data.itemName || '';
  629. dialog.visible = true;
  630. dialog.title = '修改节目管理';
  631. // 资源ID列表字段,改为fileIdList
  632. const fileIds = Array.isArray(res.data.fileIdList) ? res.data.fileIdList : [];
  633. if (fileIds.length > 0) {
  634. const fileRes = await listMinioData({ ids: fileIds });
  635. selectedFiles.value = fileIds
  636. .map((id: string | number, idx: number) => {
  637. const file = fileRes.rows.find((f: any) => String(f.id) === String(id));
  638. return file
  639. ? {
  640. id: file.id,
  641. name: file.originalName,
  642. type: file.type,
  643. duration: file.type === 1 ? 10 : Number(file.duration),
  644. order: idx + 1
  645. }
  646. : null;
  647. })
  648. .filter(Boolean);
  649. } else {
  650. selectedFiles.value = [];
  651. }
  652. // 打开弹窗后刷新分页列表
  653. await getFileList();
  654. } else {
  655. // 跳转页面进行数据关联
  656. proxy.$router.push('/source/split/edit/' + res.data.id);
  657. }
  658. } catch (e: any) {
  659. proxy?.$modal.msgError(e?.message || '获取详情失败');
  660. }
  661. };
  662. /** 提交按钮 */
  663. const submitForm = async () => {
  664. buttonLoading.value = true;
  665. try {
  666. if (form.value.id) {
  667. if (form.value.itemType === 1) {
  668. form.value.selectedFiles = selectedFiles.value;
  669. form.value.fileIdList = selectedFiles.value.map(f => f.id);
  670. form.value.itemName = itemName.value;
  671. }
  672. await updateItem(form.value);
  673. } else {
  674. if (form.value.itemType === 1) {
  675. form.value.selectedFiles = selectedFiles.value;
  676. form.value.fileIdList = selectedFiles.value.map(f => f.id);
  677. form.value.itemName = itemName.value;
  678. }
  679. await addItem(form.value);
  680. }
  681. proxy?.$modal.msgSuccess('操作成功');
  682. dialog.visible = false;
  683. await getList();
  684. } catch (e: any) {
  685. proxy?.$modal.msgError(e?.message || '提交失败');
  686. } finally {
  687. buttonLoading.value = false;
  688. }
  689. };
  690. const submitSplit = async () => {
  691. buttonLoading.value = true;
  692. try {
  693. // 根据分辨率判断横屏还是竖屏
  694. const isWidth = form.value.width > form.value.height;
  695. // 2分屏的坐标
  696. if (form.value.splitScreen === 2) {
  697. if (isWidth) {
  698. form.value.position = '(0,' + position1.value + '),(' + form.value.height + ',' + position1.value + ')';
  699. } else {
  700. form.value.position = '(0,' + position1.value + '),(' + form.value.width + ',' + position1.value + ')';
  701. }
  702. }
  703. // 3分屏的坐标
  704. if (form.value.splitScreen === 3) {
  705. if (isWidth) {
  706. const xy1 = '(0,' + position1.value + '),(' + form.value.height + ',' + position1.value + ')';
  707. const xy2 = '(0,' + position2.value + '),(' + form.value.height + ',' + position2.value + ')';
  708. form.value.position = xy1 + ',' + xy2;
  709. } else {
  710. const xy1 = '(0,' + position1.value + '),(' + form.value.width + ',' + position1.value + ')';
  711. const xy2 = '(0,' + position2.value + '),(' + form.value.width + ',' + position2.value + ')';
  712. form.value.position = xy1 + ',' + xy2;
  713. }
  714. }
  715. // 播放框
  716. if (form.value.splitScreen === 4) {
  717. const xy1 = '(' + (position1.value - positionW.value / 2) + ',' + (position2.value + positionH.value / 2) + ')';
  718. const xy2 = '(' + (position1.value + positionW.value / 2) + ',' + (position2.value + positionH.value / 2) + ')';
  719. const xy3 = '(' + (position1.value - positionW.value / 2) + ',' + (position2.value - positionH.value / 2) + ')';
  720. const xy4 = '(' + (position1.value + positionW.value / 2) + ',' + (position2.value - positionH.value / 2) + ')';
  721. form.value.position = xy1 + ',' + xy2 + ',' + xy3 + ',' + xy4;
  722. }
  723. const res = await addItem(form.value);
  724. proxy?.$modal.msgSuccess('操作成功');
  725. splitDialog.visible = false;
  726. const itemId = res.data;
  727. // 跳转页面进行数据关联
  728. proxy.$router.push('/source/split/edit/' + itemId);
  729. } catch (e: any) {
  730. proxy?.$modal.msgError(e?.message || '提交分屏失败');
  731. } finally {
  732. buttonLoading.value = false;
  733. }
  734. };
  735. /** 删除按钮操作 */
  736. const handleDelete = async (row?: ItemVO) => {
  737. loading.value = true;
  738. try {
  739. const _ids = row?.id || ids.value;
  740. await proxy?.$modal.confirm('是否确认删除节目管理编号为"' + _ids + '"的数据项?');
  741. await delItem(_ids);
  742. proxy?.$modal.msgSuccess('删除成功');
  743. await getList();
  744. } catch (e: any) {
  745. proxy?.$modal.msgError(e?.message || '删除失败');
  746. } finally {
  747. loading.value = false;
  748. }
  749. };
  750. /** 导出按钮操作 */
  751. const handleExport = () => {
  752. proxy?.download(
  753. 'source/item/export',
  754. {
  755. ...queryParams.value
  756. },
  757. `item_${new Date().getTime()}.xlsx`
  758. );
  759. };
  760. const getItemStatistics = async () => {
  761. try {
  762. const res = await itemStatistics();
  763. totalNum.value = res.data.totalNum;
  764. lbNum.value = res.data.lbNum;
  765. jmNum.value = res.data.jmNum;
  766. } catch (e: any) {
  767. proxy?.$modal.msgError(e?.message || '获取统计数据失败');
  768. }
  769. };
  770. /** 拖拽排序结束时,重排 order 字段 */
  771. const onSelectedFilesDragEnd = () => {
  772. selectedFiles.value = selectedFiles.value.map((f, idx) => ({ ...f, order: idx + 1 }));
  773. };
  774. onMounted(() => {
  775. getList();
  776. getItemStatistics();
  777. });
  778. onActivated(() => {
  779. getList();
  780. getItemStatistics();
  781. });
  782. </script>
  783. <style scoped>
  784. .dialog-container {
  785. display: flex;
  786. gap: 20px;
  787. }
  788. .table-container {
  789. flex: 1;
  790. }
  791. .selected-container {
  792. flex: 1;
  793. }
  794. .interface-input {
  795. margin-bottom: 10px;
  796. }
  797. .order-number {
  798. display: inline-block;
  799. width: 30px;
  800. text-align: center;
  801. font-weight: bold;
  802. }
  803. .stat-title {
  804. display: inline-flex;
  805. align-items: center;
  806. }
  807. .statistic-col {
  808. display: flex;
  809. justify-content: center;
  810. align-items: center;
  811. }
  812. .edit-history-param {
  813. line-height: 1.8;
  814. font-size: 14px;
  815. word-break: break-all;
  816. }
  817. .edit-history-user {
  818. font-size: 18px;
  819. color: #222;
  820. }
  821. .edit-history-time {
  822. font-size: 14px;
  823. color: #999;
  824. }
  825. </style>