App.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. <template>
  2. <div class="mirror-setting" v-if="!isNotFound">
  3. <!-- 图片预览 -->
  4. <el-dialog v-model="dialogVisible">
  5. <img
  6. v-if="checkSourceIsImage(dialogImageUrl) && dialogVisible"
  7. style="width: 100%; height: 500px; object-fit: scale-down"
  8. w-full
  9. :src="dialogImageUrl"
  10. alt="Preview Image"
  11. />
  12. <video
  13. v-if="checkSourceIsVideo(dialogImageUrl) && dialogVisible"
  14. style="width: 100%"
  15. w-full
  16. controls
  17. :src="dialogImageUrl"
  18. />
  19. <audio
  20. v-if="checkSourceIsAudio(dialogImageUrl) && dialogVisible"
  21. style="width: 100%"
  22. w-full
  23. controls
  24. :src="dialogImageUrl"
  25. />
  26. </el-dialog>
  27. <!-- 分镜配置 -->
  28. <div class="project-title">
  29. <el-input
  30. class="title"
  31. type="textarea"
  32. :autosize="{ minRows: 1, maxRows: 4 }"
  33. v-model="project.title"
  34. />
  35. <el-button type="primary" @click="saveProject">保存</el-button>
  36. </div>
  37. <div class="content">
  38. <el-table
  39. :key="data.list.length"
  40. class="main-table"
  41. key="id"
  42. border
  43. v-dragable="dragOptions"
  44. :data="data.list"
  45. header-row-class-name="t-head"
  46. header-cell-class-name="t-cell"
  47. >
  48. <!-- <template v-for="item in columns" :key="item.prop">
  49. <el-table-column :prop="item.prop" :label="item.label" />
  50. 大纲
  51. </template>
  52. :on-preview="handlePictureCardPreview" :on-remove="handleRemove"
  53. -->
  54. <el-table-column prop="name" label="大纲">
  55. <template v-slot="{ row }">
  56. <el-input
  57. type="textarea"
  58. :autosize="{ minRows: 3 }"
  59. v-model="row.name"
  60. :row="3"
  61. placeholder="概括拍摄内容"
  62. />
  63. </template>
  64. </el-table-column>
  65. <el-table-column prop="desc" label="分镜描述">
  66. <template v-slot="{ row }">
  67. <el-input
  68. class="gray"
  69. type="textarea"
  70. :autosize="{ minRows: 3 }"
  71. v-model="row.desc"
  72. :row="3"
  73. placeholder="详细描述分镜"
  74. />
  75. </template>
  76. </el-table-column>
  77. <!-- show-overflow-tooltip -->
  78. <el-table-column prop="clip" label="已拍摄片段">
  79. <template v-slot="{ row }">
  80. <el-upload
  81. ref="upload"
  82. v-model:file-list="row.fileList"
  83. class="list-upload-style"
  84. :accept="DrawFormats.toString()"
  85. :class="{
  86. activefileList: row.fileList && row.fileList.length == 1,
  87. }"
  88. :before-upload="beforeUpload"
  89. list-type="picture-card"
  90. :action="uploadFileUrl"
  91. :on-success="handleUploadSuccess"
  92. :limit="1"
  93. >
  94. <div
  95. class="uploadImg"
  96. v-if="row.fileList && row.fileList.length == 0"
  97. >
  98. <el-icon><Plus /></el-icon>
  99. </div>
  100. <template #file="{ file }">
  101. <div style="width: 100%">
  102. <img
  103. v-if="file.cover"
  104. class="el-upload-list__item-thumbnail"
  105. :src="getCoverUrl(file.cover)"
  106. alt=""
  107. />
  108. <span class="el-upload-list__item-actions">
  109. <span
  110. class="el-upload-list__item-preview"
  111. @click="handlePictureCardPreview(file)"
  112. >
  113. <el-icon><zoom-in /></el-icon>
  114. </span>
  115. <span
  116. class="el-upload-list__item-delete"
  117. @click="handleRemove(row)"
  118. >
  119. <el-icon><Delete /></el-icon>
  120. </span>
  121. </span>
  122. </div>
  123. </template>
  124. </el-upload>
  125. </template>
  126. </el-table-column>
  127. <el-table-column prop="words" label="台词文案">
  128. <template v-slot="{ row }">
  129. <el-input
  130. class="gray"
  131. type="textarea"
  132. :autosize="{ minRows: 3 }"
  133. v-model="row.words"
  134. placeholder="点击输入台词"
  135. />
  136. </template>
  137. </el-table-column>
  138. <el-table-column prop="marks" label="备注">
  139. <template v-slot="{ row, $index }">
  140. <div class="marksDiv">
  141. <el-input
  142. class="gray"
  143. type="textarea"
  144. :autosize="{ minRows: 3 }"
  145. v-model="row.marks"
  146. placeholder="点击输入内容"
  147. />
  148. <span
  149. class="table-delete"
  150. @click="handleTableRemove($index, row)"
  151. >
  152. <el-icon><Delete /></el-icon>
  153. </span>
  154. </div>
  155. </template>
  156. </el-table-column>
  157. </el-table>
  158. </div>
  159. <div class="add-handle">
  160. <el-button type="primary" @click="handleAdd">
  161. <el-icon class="el-icon--right">
  162. <Plus />
  163. </el-icon>
  164. 添加
  165. <el-input class="add-line" type="text" v-model="addLine" size="small">
  166. </el-input>
  167. </el-button>
  168. </div>
  169. </div>
  170. <noCase :show-btn="false" v-else></noCase>
  171. </template>
  172. <script lang="ts" setup>
  173. import { vDragable } from "./dragable";
  174. import { ElMessage } from "element-plus";
  175. import { reactive, ref, onMounted, computed } from "vue";
  176. import type { UploadFile } from "element-plus";
  177. import { uploadFile as uploadFileUrl } from "@/request";
  178. import {
  179. getCaseScriptInfo,
  180. CaseScriptSaveOrUpdate,
  181. CaseScriptGetCover,
  182. } from "@/app/mirror/store/script";
  183. import linkIco from "@/assets/image/fire.ico";
  184. import musicHeadphones from "@/assets/image/music.png";
  185. import { getCaseInfo } from "@/store/case";
  186. import noCase from "@/view/case/no-case.vue";
  187. const link = document.querySelector<HTMLLinkElement>("#app-icon")!;
  188. link.setAttribute("href", linkIco);
  189. const caseId = ref(null);
  190. const project = reactive({
  191. title: "",
  192. });
  193. const DrawFormats = [".jpg", ".jpeg", ".png",".mp4",".m4v",".mp3",".aac", ".wav"]
  194. const isNotFound = ref(false);
  195. const dialogImageUrl = ref("");
  196. const dialogVisible = ref(false);
  197. const disabled = ref(false);
  198. const addLine = ref(1);
  199. const active = ref(1);
  200. const dragOptions = [
  201. // {
  202. // selector: "thead tr", // add drag support for column
  203. // option: {
  204. // // sortablejs's option
  205. // animation: 150,
  206. // onEnd: (evt) => {
  207. // let oldCol: any = {};
  208. // Object.assign(oldCol, columns.value[evt.oldIndex]);
  209. // columns.value.splice(evt.oldIndex, 1); // 因为新增了数据,所以要移除原来的列的index要在原来的基础上
  210. // setTimeout(() => {
  211. // columns.value.splice(evt.newIndex, 0, oldCol); // 把原来的列数据添加到新的位置,然后再从原位置移除它,触发table的重绘
  212. // }, 30);
  213. // console.log(evt.oldIndex, evt.newIndex);
  214. // },
  215. // },
  216. // },
  217. {
  218. selector: "tbody", // add drag support for row
  219. option: {
  220. // sortablejs's option
  221. animation: 150,
  222. onEnd: (evt: any) => {
  223. // let oldItem = sortList.value[evt.oldIndex];
  224. // let sortLists = sortList.value.filter(
  225. // (_, index) => index !== evt.oldIndex
  226. // );
  227. // sortLists.splice(evt.newIndex, 0, oldItem);
  228. // sortList.value = sortLists;
  229. let list = JSON.parse(JSON.stringify(data.newSortList));
  230. const target = list.splice(evt.oldIndex, 1);
  231. list.splice(evt.newIndex, 0, target[0]);
  232. data.newSortList = list;
  233. console.log(evt.oldIndex, evt.newIndex, data.newSortList, data.list);
  234. },
  235. },
  236. },
  237. ];
  238. const columns = ref([
  239. // { prop: "id", label: "ID", hidden: true, },
  240. { prop: "name", label: "大纲" },
  241. { prop: "desc", label: "分镜描述" },
  242. { prop: "clip", label: "已拍摄片段" },
  243. { prop: "words", label: "台词文案" },
  244. { prop: "marks", label: "备注" },
  245. ]);
  246. const beforeUpload = async (file: File) => {
  247. const fileType = file.name
  248. .substring(file.name.lastIndexOf("."))
  249. .toUpperCase();
  250. if (!DrawFormats.some((type) => type.toUpperCase() === fileType)) {
  251. ElMessage.error(`请上传${DrawFormats}格式的文件`);
  252. return false;
  253. } else {
  254. return true;
  255. }
  256. };
  257. const checkSourceIsVideo = computed(() => (url: string) => {
  258. return url.includes(".mp4") || url.includes(".m4v");
  259. });
  260. const checkSourceIsAudio = computed(() => (url: string) => {
  261. return url.includes(".mp3") || url.includes(".aac") || url.includes(".wav");
  262. });
  263. const checkSourceIsImage = computed(() => (url: string) => {
  264. return url.includes(".jpg") || url.includes(".png") || url.includes(".jpeg") || url.includes(".gif");
  265. });
  266. const getCoverUrl = computed(() => (url: string) => {
  267. switch (true) {
  268. // case url.includes(".mp4"):
  269. // return (
  270. // url + "?x-oss-process=video/snapshot,t_0,f_jpg,w_0,h_0,m_fast,ar_auto"
  271. // );
  272. case url.includes(".mp3") || url.includes(".aac") || url.includes(".wmv") || url.includes(".wav"):
  273. return musicHeadphones;
  274. default:
  275. return url;
  276. }
  277. });
  278. const data = reactive({
  279. list: [{ id: 1, name: "", desc: "", fileList: [] }],
  280. newSortList: [],
  281. });
  282. const sortList = ref([0]);
  283. onMounted(async () => {
  284. caseId.value = GetRequest("caseId");
  285. try {
  286. const caseInfo = await getCaseInfo(caseId.value!);
  287. if (caseInfo && caseId.value) {
  288. document.title = caseInfo.caseTitle + " | 分镜配置";
  289. } else {
  290. isNotFound.value = true;
  291. }
  292. } catch (error) {
  293. isNotFound.value = true;
  294. }
  295. getCaseScriptList();
  296. console.log("caseId", caseId); //query传参
  297. });
  298. function getCaseScriptList() {
  299. getCaseScriptInfo(caseId.value)
  300. .then((res) => {
  301. project.title = res.name || '我的脚本';
  302. data.list = res.content || [];
  303. data.newSortList = res.content || [];
  304. const idList = data.list.map((ele) => ele.id);
  305. active.value = idList.length == 0 ? 0 : Math.max.apply(null, idList) || 1;
  306. sortList.value = data.list.map((_, index) => index);
  307. console.log("getCaseScriptList", idList, active.value);
  308. Array.from(data.list).forEach((item) => {
  309. item.fileList.forEach(async (file: File, index) => {
  310. if ((file as any).url.includes(".mp4") || (file as any).url.includes(".m4v")) {
  311. const res = await CaseScriptGetCover((file as any).url);
  312. (item.fileList[index] as any).cover = res;
  313. } else {
  314. (item.fileList[index] as any).cover = (file as any).url;
  315. }
  316. });
  317. });
  318. })
  319. .catch((err) => {
  320. console.log(err);
  321. });
  322. }
  323. function handleAdd() {
  324. // let content = sortList.value.map((index) => data.list[index]);
  325. // data.list.length = 0;
  326. // Object.assign(data.list, content);
  327. console.log("add", data.newSortList);
  328. for (var i = 1; i <= addLine.value; i++) {
  329. console.log(i);
  330. data.newSortList.push({
  331. id: active.value + 1,
  332. name: "",
  333. desc: "",
  334. words: "",
  335. marks: "",
  336. fileList: [],
  337. });
  338. }
  339. active.value++;
  340. data.list = data.newSortList;
  341. sortList.value = data.list.map((_, index) => index);
  342. }
  343. const handleRemove = (data) => {
  344. data.fileList = [];
  345. };
  346. const handleTableRemove = (index, datas) => {
  347. data.newSortList = data.newSortList.filter((ele) => ele.id !== datas.id);
  348. console.log("saveProject", data.newSortList);
  349. data.list = data.newSortList;
  350. // let content = sortList.value.map((index) => data.list[index]);
  351. // data.list.length = 0;
  352. // content.splice(index, 1);
  353. // Object.assign(data.list, content);
  354. // sortList.value = content.map((_, index) => index);
  355. ElMessage.success("删除成功");
  356. console.log("saveProject", index, datas, data.list);
  357. };
  358. const handlePictureCardPreview = (file: UploadFile) => {
  359. dialogImageUrl.value = file.url!;
  360. dialogVisible.value = true;
  361. };
  362. const saveProject = () => {
  363. // let content = sortList.value.map((index) => data.list[index]);
  364. let apiDataList = data.newSortList.map((item) => {
  365. let asData = data.list.find(ele => ele.id === item.id) || {};
  366. return {
  367. ...item,
  368. ...asData,
  369. }
  370. });
  371. console.log("saveProject", data.list, data.newSortList);
  372. CaseScriptSaveOrUpdate({
  373. caseId: caseId.value,
  374. name: project.title,
  375. content: apiDataList,
  376. }).then((res) => {
  377. console.log("saveProject");
  378. ElMessage.success("保存成功");
  379. });
  380. };
  381. async function handleUploadSuccess(response: any, uploadFile: UploadFile) {
  382. uploadFile.url = response.data;
  383. console.log("handleUploadSuccess", uploadFile.url);
  384. if (uploadFile.url!.includes(".mp4")) {
  385. const res = await CaseScriptGetCover(uploadFile.url!);
  386. (uploadFile as any).cover = res;
  387. } else {
  388. (uploadFile as any).cover = uploadFile.url;
  389. }
  390. }
  391. function GetRequest(value) {
  392. var url = decodeURI(window.location.search); //?id="123456"&name="www";
  393. var object = {};
  394. if (url.indexOf("?") != -1) {
  395. //url中存在问号,也就说有参数。
  396. var str = url.substr(1); //得到?后面的字符串
  397. var strs = str.split("&"); //将得到的参数分隔成数组[id="123456",name="www"];
  398. for (var i = 0; i < strs.length; i++) {
  399. object[strs[i].split("=")[0]] = strs[i].split("=")[1]; //得到{id:'123456',name:'www'}
  400. }
  401. }
  402. return object[value];
  403. }
  404. </script>
  405. <style lang="scss" scoped></style>
  406. <style lang="scss">
  407. body,
  408. #app {
  409. margin: 0;
  410. padding: 0;
  411. }
  412. .mirror-setting {
  413. width: 100%;
  414. min-height: 100%;
  415. padding-top: 80px;
  416. min-height: calc(100vh - 80px);
  417. margin: 0 auto;
  418. background: #eee;
  419. .content {
  420. margin: 0 auto;
  421. display: flex;
  422. padding: 0 40px;
  423. }
  424. .t-head {
  425. border: 1px solid #ddd;
  426. /* padding: 10px; */
  427. /* display: flex; */
  428. position: relative;
  429. background-color: #eee;
  430. }
  431. tbody {
  432. /* border-top: 20px solid transparent; */
  433. }
  434. .t-head th {
  435. margin-bottom: 20px;
  436. }
  437. .project-title {
  438. display: flex;
  439. padding: 0 40px;
  440. align-items: center;
  441. /* justify-content: center; */
  442. }
  443. .project-title .title {
  444. font-size: 28px;
  445. min-height: 0;
  446. height: auto;
  447. background-color: transparent !important;
  448. /* width: 300px; */
  449. margin: 30px 0;
  450. }
  451. .el-textarea__inner {
  452. background-color: transparent;
  453. box-shadow: none;
  454. resize: none;
  455. }
  456. .gray .el-textarea__inner {
  457. background: rgba(227, 225, 225, 0.2);
  458. }
  459. .add-handle {
  460. padding: 30px 0;
  461. display: flex;
  462. justify-content: center;
  463. }
  464. .add-line {
  465. margin: 0 10px;
  466. width: 30px;
  467. }
  468. .add-line .el-input__wrapper {
  469. box-shadow: none;
  470. background: rgba(23, 41, 46, 0.2);
  471. }
  472. .add-line input {
  473. color: white;
  474. text-align: center;
  475. }
  476. .activefileList {
  477. .el-upload-list--picture-card {
  478. }
  479. .el-upload--picture-card {
  480. display: none;
  481. }
  482. }
  483. .list-upload-style {
  484. width: 100%;
  485. text-align: center;
  486. .el-upload-list,
  487. .el-upload--text {
  488. width: 100%;
  489. }
  490. .el-upload-list__item-thumbnail,
  491. .el-upload--picture-card {
  492. min-height: 73px;
  493. height: 73px;
  494. width: 100%;
  495. }
  496. .uploadImg,
  497. .el-upload-list__item {
  498. width: 100%;
  499. min-height: 73px;
  500. height: 73px;
  501. line-height: 73px;
  502. .el-upload-list__item-thumbnail {
  503. width: 100%;
  504. max-width: 100px;
  505. object-fit: cover;
  506. }
  507. }
  508. }
  509. .marksDiv {
  510. position: relative;
  511. .table-delete {
  512. position: absolute;
  513. right: -10px;
  514. top: -3px;
  515. }
  516. }
  517. }
  518. </style>