bill 5 ماه پیش
والد
کامیت
36808a8834

+ 162 - 49
src/api/animation.ts

@@ -1,76 +1,189 @@
-import axios from './instance'
-import { params } from '@/env'
-import { 
+import axios from "./instance";
+import { params } from "@/env";
+import {
   AM_MODEL_LIST,
   INSERT_AM_MODEL,
   UPDATE_AM_MODEL,
   DELETE_AM_MODEL,
-} from './constant'
+} from "./constant";
 
-type ServiceAnimationModel = AnimationModel
-
-export interface AnimationAction  {
-  id: string,
-  title: string,
-  url: string,
+type ServiceAnimationModel = AnimationModel;
 
+export interface AnimationAction {
+  id: string;
+  title: string;
+  url: string;
+  
 }
+export type AnimationModelAction = {
+  amplitude: number;
+  speed: number;
+  time: number;
+  duration: number;
+  id: string;
+  name: string
+};
+export type AnimationModelSubtitle = {
+  content: string;
+  duration: number;
+  time: number;
+  id: string;
+  background: string;
+  name: string
+};
+export type AnimationModelFrame = {
+  time: number;
+  id: string;
+  name: string
+  duration?: number;
+};
+export type AnimationModelPath = {
+  reverse: boolean;
+  pathId: string;
+  time: number;
+  duration: number;
+  id: string;
+  name: string
+};
+
 export interface AnimationModel {
-  id: string,
-  title: string,
-  url: string,
-  showTitle: boolean
-  fontSize: number
-  globalVisibility: boolean
-  visibilityRange: number
+  id: string;
+  title: string;
+  url: string;
+  showTitle: boolean;
+  fontSize: number;
+  globalVisibility: boolean;
+  visibilityRange: number;
+  frames: AnimationModelFrame[];
+  actions: AnimationModelAction[];
+  subtitles: AnimationModelSubtitle[];
+  paths: AnimationModelPath[];
 }
 
-export type AnimationModels = AnimationModel[]
+export type AnimationModels = AnimationModel[];
 
 const serviceToLocal = (serviceAM: ServiceAnimationModel): AnimationModel => ({
   ...serviceAM,
-})
+});
 
 const localToService = (am: AnimationModel): ServiceAnimationModel => ({
   ...am,
-})
+});
 
 export const fetchAnimationModels = async () => {
   return [
-    { id: '1', title: '模型1', url: '' },
-    { id: '2', title: '模型2', url: '' },
-    { id: '3', title: '模型3', url: '' },
-  ] as AnimationModel[]
-  
-  
-  const ams = await axios.get<ServiceAnimationModel[]>(AM_MODEL_LIST, { params: { caseId: params.caseId } })
-  return ams.map(serviceToLocal)
-}
+    {
+      id: "1",
+      title: "模型1",
+      url: "",
+      frames: [
+        { time: 10, name: "帧1" },
+        { time: 20, name: "帧2" },
+        { time: 30, name: "帧3" },
+      ],
+      actions: [
+        { time: 10, duration: 3, name: "动作1" },
+        { time: 20, duration: 5, name: "动作2" },
+        { time: 30, duration: 3, name: "动作3" },
+      ],
+      subtitles: [
+        { time: 0, duration: 15, name: "字幕1" },
+        { time: 20, duration: 15, name: "字幕2" },
+        { time: 40, duration: 30, name: "字幕3" },
+      ],
+      paths: [
+        { time: 0, duration: 15, name: "路径1" },
+        { time: 20, duration: 15, name: "路径2" },
+        { time: 40, duration: 30, name: "路径3" },
+      ],
+    },
+    {
+      id: "2",
+      title: "模型2",
+      url: "",
+      frames: [
+        { time: 10, name: "帧1" },
+        { time: 20, name: "帧2" },
+        { time: 30, name: "帧3" },
+      ],
+      actions: [
+        { time: 10, duration: 3, name: "动作1" },
+        { time: 20, duration: 5, name: "动作2" },
+        { time: 30, duration: 3, name: "动作3" },
+      ],
+      subtitles: [
+        { time: 0, duration: 15, name: "字幕1" },
+        { time: 20, duration: 15, name: "字幕2" },
+        { time: 40, duration: 30, name: "字幕3" },
+      ],
+      paths: [
+        { time: 0, duration: 15, name: "路径1" },
+        { time: 20, duration: 15, name: "路径2" },
+        { time: 40, duration: 30, name: "路径3" },
+      ],
+    },
+    {
+      id: "3",
+      title: "模型3",
+      url: "",
+      frames: [
+        { time: 10, name: "帧1" },
+        { time: 20, name: "帧2" },
+        { time: 30, name: "帧3" },
+      ],
+      actions: [
+        { time: 10, duration: 3, name: "动作1" },
+        { time: 20, duration: 5, name: "动作2" },
+        { time: 30, duration: 3, name: "动作3" },
+      ],
+      subtitles: [
+        { time: 0, duration: 15, name: "字幕1" },
+        { time: 20, duration: 15, name: "字幕2" },
+        { time: 40, duration: 30, name: "字幕3" },
+      ],
+      paths: [
+        { time: 0, duration: 15, name: "路径1" },
+        { time: 20, duration: 15, name: "路径2" },
+        { time: 40, duration: 30, name: "路径3" },
+      ],
+    },
+  ] as unknown as AnimationModel[];
+
+  const ams = await axios.get<ServiceAnimationModel[]>(AM_MODEL_LIST, {
+    params: { caseId: params.caseId },
+  });
+  return ams.map(serviceToLocal);
+};
 
 export const fetchAnimationActions = async () => {
   return [
-    { id: '1', title: '模型1', url: '' },
-    { id: '2', title: '模型2', url: '' },
-    { id: '3', title: '模型3', url: '' },
-    { id: '2', title: '模型2', url: '' },
-    { id: '3', title: '模型3', url: '' },
-    { id: '2', title: '模型2', url: '' },
-    { id: '3', title: '模型3', url: '' },
-  ]
-}
+    { id: "1", title: "模型1", url: "" },
+    { id: "2", title: "模型2", url: "" },
+    { id: "3", title: "模型3", url: "" },
+    { id: "2", title: "模型2", url: "" },
+    { id: "3", title: "模型3", url: "" },
+    { id: "2", title: "模型2", url: "" },
+    { id: "3", title: "模型3", url: "" },
+  ];
+};
 
 export const postInsertAnimationModel = async (am: AnimationModel) => {
-  const addData = { ...localToService(am), caseId: params.caseId, id: undefined }
-   const serviceData = await axios.post<ServiceAnimationModel>(INSERT_AM_MODEL, addData)
-   return serviceToLocal(serviceData)
-}
+  const addData = {
+    ...localToService(am),
+    caseId: params.caseId,
+    id: undefined,
+  };
+  const serviceData = await axios.post<ServiceAnimationModel>(
+    INSERT_AM_MODEL,
+    addData
+  );
+  return serviceToLocal(serviceData);
+};
 
 export const postUpdateAnimationModel = async (guide: AnimationModel) => {
-  return axios.post<undefined>(UPDATE_AM_MODEL, { ...localToService(guide)})
-}
-
-export const postDeleteAnimationModel = (id: AnimationModel['id']) => {
-  return axios.post<undefined>(DELETE_AM_MODEL, { id: Number(id) })
-}
+  return axios.post<undefined>(UPDATE_AM_MODEL, { ...localToService(guide) });
+};
 
-  
+export const postDeleteAnimationModel = (id: AnimationModel["id"]) => {
+  return axios.post<undefined>(DELETE_AM_MODEL, { id: Number(id) });
+};

+ 1 - 1
src/components/bill-ui/assets/scss/editor/_toolbar.scss

@@ -14,5 +14,5 @@
     left: calc(var(--editor-menu-left) + var(--editor-menu-width));
     z-index: 2;
     backdrop-filter: blur(4px);
-    transition: all .3s ease;
+    // transition: all .3s ease;
 }

+ 6 - 2
src/components/drawing-time-line/action.vue

@@ -1,6 +1,9 @@
 <template>
   <template v-for="(rect, i) in rects">
-    <v-rect :config="rect" :ref="(s: any) => actionShapes[i] = s" />
+    <v-rect
+      :config="{ ...rect, fill: activeNdx === i ? '#00c8af' : '#fff' }"
+      :ref="(s: any) => actionShapes[i] = s"
+    />
     <v-text :config="getTextConfig(i)" />
   </template>
 </template>
@@ -21,6 +24,7 @@ const { misPixel } = useGlobalVar();
 
 const props = defineProps<{
   items: { time: number; duration: number; name: string }[];
+  activeNdx?: number;
   top: number;
 }>();
 
@@ -52,7 +56,7 @@ const getTextConfig = (ndx: number) => {
     ...dec,
     width: rect.width / invConfig.value.scaleX,
     height: rect.height / invConfig.value.scaleY,
-    fill: "#fff",
+    fill: ndx === props.activeNdx ? "#00c8af" : "#fff",
     fontSize: 14,
     listening: false,
     align: "center",

+ 80 - 0
src/components/drawing-time-line/check.ts

@@ -0,0 +1,80 @@
+export type TLItem = { time: number; duration?: number };
+
+export const getAddTLItemAttr = (
+  items: TLItem[],
+  cur: number,
+  maxDur: number,
+  minDur: number
+) => {
+  items = [...items].sort((a, b) => a.time - b.time);
+
+  let last: TLItem | null = null;
+  let start: TLItem | null = null;
+  for (let i = 0; i < items.length; i++) {
+    if (
+      cur >= items[i].time &&
+      cur <= items[i].time + (items[i].duration || 0)
+    ) {
+      return null;
+    }
+    if (items[i].time > cur) {
+      last = items[i];
+      break;
+    }
+    if (items[i].time + (items[i].duration || 0) < cur) {
+      start = items[i];
+    }
+  }
+
+  let dur: number = 0;
+  if (!last) {
+    return { time: cur, duration: maxDur };
+  } else {
+    dur = Math.min(maxDur, last.time - cur);
+    if (dur < minDur) {
+      return null;
+    } else {
+      return { time: cur, duration: dur };
+    }
+  }
+};
+
+export const checkTLItem = (items: TLItem[], cur: TLItem, i?: number) => {
+  const exRects = items
+    .filter((_, ndx) => ndx !== i)
+    .sort((a, b) => a.time - b.time)
+    .map((item) => ({
+      x: item.time,
+      xe: item.time + (item.duration || 0.001),
+    }));
+
+  const curRect = { x: cur.time, xe: cur.time + (cur.duration || 0.001) };
+  for (let i = 0; i < exRects.length; i++) {
+    const exRect = exRects[i];
+    if (
+      (curRect.x > exRect.x && curRect.x < exRect.xe) ||
+      (curRect.xe > exRect.x && curRect.xe < exRect.xe) ||
+      (exRect.x > curRect.x && exRect.x < curRect.xe) ||
+      (exRect.xe > curRect.x && exRect.xe < curRect.xe)
+    ) {
+      return false;
+    }
+  }
+  return true;
+};
+
+export const getAddTLItemTime = (items: TLItem[], refNdx = 0, dur = 0.01) => {
+  const ref = items[refNdx];
+  items = [...items].sort((a, b) => a.time - b.time);
+  refNdx = items.indexOf(ref);
+
+  let time = 0;
+  for (let i = refNdx; i < items.length; i++) {
+    time = items[i].time + (items[i].duration || 0.01);
+    if (checkTLItem([...items].splice(i), { time, duration: dur })) {
+      break;
+    }
+  }
+
+  return time;
+};

+ 9 - 2
src/components/drawing-time-line/frame.vue

@@ -1,6 +1,9 @@
 <template>
   <template v-for="(l, i) in lines">
-    <v-line :config="l" :ref="(s: any) => frameShapes[i] = s" />
+    <v-line
+      :config="{ ...l, fill: activeNdx === i ? '#00c8af' : '#fff' }"
+      :ref="(s: any) => frameShapes[i] = s"
+    />
   </template>
 </template>
 
@@ -17,7 +20,11 @@ import { DC } from "../drawing/dec";
 
 const { misPixel } = useGlobalVar();
 
-const props = defineProps<{ items: { time: number }[], top: number }>();
+const props = defineProps<{
+  items: { time: number }[];
+  top: number;
+  activeNdx?: number;
+}>();
 const s = 6;
 const invConfig = useViewerInvertTransformConfig();
 const lines = computed(() => {

+ 25 - 20
src/components/drawing-time-line/index.vue

@@ -13,6 +13,7 @@
     :is="itemsRenderer"
     :items="items"
     :top="top"
+    :activeNdx="active ? items.indexOf(active) : -1"
     :ref="({ shapes }: any) => itemShapes = shapes"
   />
   <template v-for="(itemShape, i) in itemShapes">
@@ -44,18 +45,21 @@ import {
 import { Transform } from "konva/lib/Util";
 import { DC, EntityShape } from "../drawing/dec";
 import Operate from "../drawing/operate.vue";
+import { checkTLItem, getAddTLItemTime, TLItem } from "./check";
 
 const { misPixel } = useGlobalVar();
 const { size } = useGlobalResize();
 
 const props = defineProps<{
-  items: { time: number; duration?: number }[];
+  items: TLItem[];
   itemsRenderer: any;
   background?: string;
   height: number;
   top: number;
+  active?: TLItem;
 }>();
 const emit = defineEmits<{
+  (e: "update:active", data: TLItem | undefined): void;
   (e: "update", data: { ndx: number; time: number }): void;
   (e: "add", data: any): void;
   (e: "del", ndx: number): void;
@@ -78,24 +82,14 @@ watch(drag, (drag) => {
     return;
   }
   const cur = props.items[drag.ndx];
-  const curX = cur.time * misPixel + total.x + drag.x;
-  const exRects = props.items
-    .filter((_, ndx) => ndx !== drag.ndx)
-    .map((item) => ({
-      x: item.time * misPixel,
-      xe: (item.time + (item.duration || 0.001)) * misPixel,
-    }));
-
-  const curRect = { x: curX, xe: curX + (cur.duration || 0.001) * misPixel };
-  const joinNdx = exRects.findIndex(
-    (exRect) =>
-      (curRect.x > exRect.x && curRect.x < exRect.xe) ||
-      (curRect.xe > exRect.x && curRect.xe < exRect.xe) ||
-      (exRect.x > curRect.x && exRect.x < curRect.xe) ||
-      (exRect.xe > curRect.x && exRect.xe < curRect.xe)
-  );
-
-  if (!~joinNdx) {
+  if (
+    checkTLItem(
+      props.items,
+      { ...cur, time: cur.time + (total.x + drag.x) / misPixel },
+      drag.ndx
+    )
+  ) {
+    const curX = cur.time * misPixel + total.x + drag.x;
     emit("update", { ndx: drag.ndx, time: curX / misPixel });
     total = { x: 0, y: 0 };
   } else {
@@ -104,10 +98,21 @@ watch(drag, (drag) => {
   }
 });
 
+watchEffect((onCleanup) => {
+  for (let i = 0; i < itemShapes.value.length; i++) {
+    const $shape = itemShapes.value[i]?.getNode();
+    if (!$shape) continue;
+    $shape.on("click.uactive", () => {
+      emit("update:active", props.active === props.items[i] ? undefined : props.items[i]);
+    });
+    onCleanup(() => $shape.off("click.uactive"));
+  }
+});
+
 const copyHandler = (ndx: number) => {
   const newFrame = {
     ...props.items[ndx],
-    time: props.items[ndx].time + 3,
+    time: getAddTLItemTime(props.items, ndx, props.items[ndx].duration),
   };
   emit("add", newFrame);
 };

+ 2 - 3
src/components/drawing/hook.ts

@@ -13,7 +13,7 @@ import {
   WatchOptions,
   WatchSource,
 } from "vue";
-import { v4 as uuid } from "uuid";
+import { v4 as uuidRaw } from "uuid";
 import { DC, EntityShape, Pos, Size } from "./dec";
 import { Stage } from "konva/lib/Stage";
 import { Transform } from "konva/lib/Util";
@@ -23,7 +23,7 @@ import { KonvaEventObject } from "konva/lib/Node";
 
 export const rendererName = "renderer";
 export const rendererMap = new WeakMap<any, { unmounteds: (() => void)[] }>();
-
+export const uuid = uuidRaw
 export const useRendererInstance = () => {
   let instance = getCurrentInstance()!;
   while (instance.type.__name !== rendererName) {
@@ -160,7 +160,6 @@ export const useGlobalResize = installGlobalVar(() => {
   const size = ref<Size>();
   const setSize = () => {
     if (fix.value) return;
-    console.error(stage.value);
     const container = stage.value?.getStage().container();
     if (container) {
       container.style.setProperty("display", "none");

+ 59 - 36
src/store/animation.ts

@@ -1,57 +1,80 @@
-
-import { ref } from 'vue'
-import { autoSetModeCallback, createTemploraryID } from './sys'
-import { 
+import { ref } from "vue";
+import { autoSetModeCallback, createTemploraryID } from "./sys";
+import {
   fetchAnimationActions,
   fetchAnimationModels,
-  postDeleteAnimationModel, 
+  postDeleteAnimationModel,
   postInsertAnimationModel,
   postUpdateAnimationModel,
-} from '@/api'
-import { 
+} from "@/api";
+import {
   togetherCallback,
-  deleteStoreItem, 
-  addStoreItem, 
-  updateStoreItem, 
+  deleteStoreItem,
+  addStoreItem,
+  updateStoreItem,
   saveStoreItems,
-  recoverStoreItems
-} from '@/utils'
+  recoverStoreItems,
+} from "@/utils";
 
-import type { AnimationAction, AnimationModel, AnimationModels } from '@/api'
-export type { AnimationModel, AnimationModels }
+import type {
+  AnimationAction,
+  AnimationModel,
+  AnimationModels,
+} from "@/api";
+export type {
+  AnimationModelAction,
+  AnimationModelFrame,
+  AnimationModelPath,
+  AnimationModelSubtitle,
+} from '@/api'
+export type { AnimationModel, AnimationModels };
 
-export const ams = ref<AnimationModels>([])
-export const amActions = ref<AnimationAction[]>([])
+export const ams = ref<AnimationModels>([]);
+export const amActions = ref<AnimationAction[]>([]);
 
 export const initAnimationActions = async () => {
-  amActions.value = await fetchAnimationActions()
-}
+  amActions.value = await fetchAnimationActions();
+};
 
-export const createAnimationModel = (am: Partial<AnimationModel> = {}): AnimationModel => ({
+export const createAnimationModel = (
+  am: Partial<AnimationModel> = {}
+): AnimationModel => ({
   id: createTemploraryID(),
   title: `模型`,
-  url: '',
+  url: "",
   showTitle: true,
   fontSize: 12,
   globalVisibility: true,
   visibilityRange: 12,
-  ...am
-})
+  subtitles: [],
+  actions: [],
+  frames: [],
+  paths: [],
+  ...am,
+});
 
-let bcAms: AnimationModels = []
-export const getBackupAnimationModels = () => bcAms
+let bcAms: AnimationModels = [];
+export const getBackupAnimationModels = () => bcAms;
 export const backupAnimationModels = () => {
-  bcAms = ams.value.map(am => ({...am }))
-}
-export const addAnimationModel = addStoreItem(ams, postInsertAnimationModel)
-export const updateAnimationModels = updateStoreItem(ams, postUpdateAnimationModel)
-export const deleteAnimationModel = deleteStoreItem(ams, ({id}) => postDeleteAnimationModel(id))
+  bcAms = ams.value.map((am) => ({ ...am }));
+};
+export const addAnimationModel = addStoreItem(ams, postInsertAnimationModel);
+export const updateAnimationModels = updateStoreItem(
+  ams,
+  postUpdateAnimationModel
+);
+export const deleteAnimationModel = deleteStoreItem(ams, ({ id }) =>
+  postDeleteAnimationModel(id)
+);
 export const initialAnimationModels = async () => {
-  ams.value = await fetchAnimationModels()
-  backupAnimationModels()
-}
+  ams.value = await fetchAnimationModels();
+  backupAnimationModels();
+};
 
-export const recoverAnimationModels = recoverStoreItems(ams, getBackupAnimationModels)
+export const recoverAnimationModels = recoverStoreItems(
+  ams,
+  getBackupAnimationModels
+);
 export const saveAnimationModels = saveStoreItems(
   ams,
   getBackupAnimationModels,
@@ -60,11 +83,11 @@ export const saveAnimationModels = saveStoreItems(
     update: updateAnimationModels,
     delete: deleteAnimationModel,
   }
-)
+);
 export const autoSaveAnimationModel = autoSetModeCallback([ams], {
   backup: togetherCallback([backupAnimationModels]),
   recovery: togetherCallback([recoverAnimationModels]),
   save: async () => {
-    await saveAnimationModels()
+    await saveAnimationModels();
   },
-})
+});

+ 4 - 0
src/style.scss

@@ -151,4 +151,8 @@ input::-ms-clear,input::-ms-reveal {
 
 .ant-modal-mask {
   background-color: rgba(0, 0, 0, 0.6) !important;
+}
+
+.nameInput.ui-input .text.suffix input {
+  padding-right: 70px;
 }

+ 33 - 57
src/views/animation/bottom/bottom.vue

@@ -20,47 +20,24 @@
     <div class="oper-bar" :class="{ disabled: play }">
       <Renderer v-model:scale="scale">
         <TimeLine
-          background="#000"
-          :items="frames"
-          :height="30"
-          :top="24"
-          :itemsRenderer="TimeLineFrame"
-          @update="({ ndx, time }) => (frames[ndx].time = time)"
-          @add="(item) => frames.push(item)"
-          @del="(ndx) => frames.splice(ndx, 1)"
+          v-for="prop in tlProps"
+          :items="am[prop.attr]"
+          :height="prop.height"
+          :top="prop.top"
+          :itemsRenderer="prop.component"
+          @update="({ ndx, time }) => (am[prop.attr][ndx].time = time)"
+          @add="(item) => am[prop.attr].push(item)"
+          @del="(ndx) => am[prop.attr].splice(ndx, 1)"
+          :active="prop.attr === active?.key ? am[prop.attr][active.ndx] : undefined"
+          @update:active="(active: any) => $emit('update:active', active && { key: prop.attr, ndx: am[prop.attr].indexOf(active) })"
         />
 
-        <TimeLine
-          :items="actions"
-          :height="30"
-          :top="65"
-          :itemsRenderer="TimeLineAction"
-          @update="({ ndx, time }) => (actions[ndx].time = time)"
-          @add="(item) => actions.push(item)"
-          @del="(ndx) => actions.splice(ndx, 1)"
-        />
-
-        <TimeLine
-          :items="subtitles"
-          :height="30"
-          :top="105"
-          :itemsRenderer="TimeLineAction"
-          @update="({ ndx, time }) => (subtitles[ndx].time = time)"
-          @add="(item) => subtitles.push(item)"
-          @del="(ndx) => subtitles.splice(ndx, 1)"
-        />
-        <TimeLine
-          :items="paths"
-          :height="30"
-          :top="140"
-          :itemsRenderer="TimeLineAction"
-          @update="({ ndx, time }) => (paths[ndx].time = time)"
-          @add="(item) => paths.push(item)"
-          @del="(ndx) => paths.splice(ndx, 1)"
-        />
-
-        <Time @update-current-time="(time) => (currentTime = time)">
-          <TimeCurrent v-model:currentTime="currentTime" :follow="play" />
+        <Time @update-current-time="(time) => $emit('update:currentTime', time)">
+          <TimeCurrent
+            :currentTime="currentTime"
+            @update:current-time="(time) => $emit('update:currentTime', time)"
+            :follow="play"
+          />
         </Time>
       </Renderer>
     </div>
@@ -70,32 +47,31 @@
 <script lang="ts" setup>
 import { Slider } from "ant-design-vue";
 import { ref, watch } from "vue";
+import { AnimationModel } from "@/store/animation";
 import Renderer from "@/components/drawing/renderer.vue";
 import Time from "@/components/drawing-time/time.vue";
 import TimeCurrent from "@/components/drawing-time/current.vue";
 import TimeLine from "@/components/drawing-time-line/index.vue";
 import TimeLineFrame from "@/components/drawing-time-line/frame.vue";
 import TimeLineAction from "@/components/drawing-time-line/action.vue";
+import { Active } from "./type";
+
+const props = defineProps<{ am: AnimationModel; active?: Active; currentTime: number }>();
+const emit = defineEmits<{
+  (e: "update:active", data: Active | undefined): void;
+  (e: "update:currentTime", v: number): void;
+}>();
 
 const scale = ref(1);
-const currentTime = ref(30);
+
+const tlProps = [
+  { attr: "frames", component: TimeLineFrame, height: 30, top: 24 },
+  { attr: "actions", component: TimeLineAction, height: 30, top: 65 },
+  { attr: "subtitles", component: TimeLineAction, height: 30, top: 105 },
+  { attr: "paths", component: TimeLineAction, height: 30, top: 140 },
+] as const;
+
 const play = ref(false);
-const frames = ref([{ time: 10 }, { time: 20 }, { time: 30 }]);
-const actions = ref([
-  { time: 10, duration: 3, name: "动作1" },
-  { time: 20, duration: 5, name: "动作2" },
-  { time: 30, duration: 3, name: "动作3" },
-]);
-const subtitles = ref([
-  { time: 0, duration: 15, name: "字幕1" },
-  { time: 20, duration: 15, name: "字幕2" },
-  { time: 40, duration: 30, name: "字幕3" },
-]);
-const paths = ref([
-  { time: 0, duration: 15, name: "路径1" },
-  { time: 20, duration: 15, name: "路径2" },
-  { time: 40, duration: 30, name: "路径3" },
-]);
 
 watch(play, (_a, _b, onCleanup) => {
   let isDes = false;
@@ -103,7 +79,7 @@ watch(play, (_a, _b, onCleanup) => {
   const animation = () => {
     if (play.value && !isDes) {
       const curNow = Date.now();
-      currentTime.value += (curNow - prevNow) * 0.001;
+      emit("update:currentTime", props.currentTime + (curNow - prevNow) * 0.001);
       prevNow = curNow;
       requestAnimationFrame(animation);
     }

+ 0 - 0
src/views/animation/bottom/pano.vue


+ 59 - 6
src/views/animation/index.vue

@@ -1,15 +1,28 @@
 <template>
   <div :class="{ focusAM: focusAM }" class="animation-layout">
-    <Left v-model:focus="focusAM" class="animation-left" />
-    <Right v-if="focusAM" :am="focusAM" class="animation-right" />
-    <Bottom v-if="focusAM" :am="focusAM" class="animation-toolbar" />
+    <Left :focus="focusAM" @update:focus="updateFocus" class="animation-left" />
+    <Right
+      v-if="focusAM"
+      :am="focusAM"
+      class="animation-right"
+      v-model:activeAttrib="activeAttrib"
+      @add-frame="addFrame"
+      @add-path="addPath"
+    />
+    <Bottom
+      v-if="focusAM"
+      :am="focusAM"
+      v-model:current-time="currentTime"
+      class="animation-toolbar"
+      v-model:active="activeAttrib"
+    />
   </div>
 </template>
 
 <script lang="ts" setup>
 import Left from "./left.vue";
-import Right from "./right.vue";
-import Bottom from "./bottom/bottom.vue";
+import Right from "./right/index.vue";
+import Bottom from "./bottom.vue";
 import router from "@/router";
 import { enterEdit } from "@/store";
 import { useViewStack } from "@/hook";
@@ -19,7 +32,11 @@ import {
   initAnimationActions,
   initialAnimationModels,
 } from "@/store/animation";
-import { ref } from "vue";
+import { ref, watchEffect } from "vue";
+import { Active } from "./type";
+import { getAddTLItemAttr } from "@/components/drawing-time-line/check";
+import { Message } from "bill/expose-common";
+import { uuid } from "@/components/drawing/hook";
 
 enterEdit(() => router.back());
 initialAnimationModels();
@@ -27,11 +44,47 @@ initAnimationActions();
 useViewStack(autoSaveAnimationModel);
 
 const focusAM = ref<AnimationModel>();
+const activeAttrib = ref<Active>();
+const currentTime = ref(0);
+
+const updateFocus = (am?: AnimationModel) => {
+  activeAttrib.value = undefined;
+  focusAM.value = am;
+  console.error(am);
+};
+
+const addFrame = () => {
+  const attr = getAddTLItemAttr(focusAM.value!.frames, currentTime.value, 10, 1);
+  if (!attr) {
+    Message.error("当前时间已存在其他帧");
+  } else {
+    focusAM.value!.frames.push({
+      id: uuid(),
+      name: "帧",
+      ...attr,
+      duration: 0,
+    });
+  }
+};
+
+const addPath = () => {
+  const attr = getAddTLItemAttr(focusAM.value!.paths, currentTime.value, 10, 1);
+  if (!attr) {
+    Message.error("当前时间已存在其他路径");
+  } else {
+    focusAM.value!.frames.push({
+      id: uuid(),
+      name: "路径",
+      ...attr,
+    });
+  }
+};
 </script>
 
 <style lang="scss" scoped>
 .animation-layout {
   --bottom-height: 0px;
+
   &.focusAM {
     --bottom-height: 225px;
   }

+ 0 - 1
src/views/animation/left.vue

@@ -99,7 +99,6 @@ const selectModel = async () => {
 </script>
 
 <style scoped lang="scss">
-
 .animation-left-header {
   width: 100%;
 }

+ 0 - 168
src/views/animation/right.vue

@@ -1,168 +0,0 @@
-<template>
-  <RightFillPano class="animation-right">
-    <Tabs v-model:activeKey="activeKey" width="100%">
-      <TabPane key="setting" tab="设置">
-        <ui-group borderBottom>
-          <ui-group-option class="item">
-            <span class="label">名称</span>
-            <span class="oper">
-              <ui-input
-                width="100%"
-                type="text"
-                ref="nameInput"
-                class="nameInput"
-                placeholder="请输入名称"
-                v-model="am.title"
-                :maxlength="100"
-              />
-            </span>
-          </ui-group-option>
-          <ui-group-option class="item">
-            <span class="label">显示名称</span>
-            <span class="oper"> <Switch v-model:checked="am.showTitle" /> </span>
-          </ui-group-option>
-        </ui-group>
-
-        <ui-group borderBottom>
-          <ui-group-option>
-            <SignItem
-              label="文字大小"
-              @apply-global="$emit('applyGlobal', 'visibilityRange')"
-            >
-              <Slider v-model:value="am.fontSize" :min="12" :max="60" :step="0.1" />
-            </SignItem>
-          </ui-group-option>
-        </ui-group>
-        <ui-group borderBottom>
-          <ui-group-option>
-            <SignItem
-              label="可见范围"
-              v-if="!am.globalVisibility"
-              @apply-global="$emit('applyGlobal', 'visibilityRange')"
-            >
-              <Slider
-                v-model:value="am.visibilityRange"
-                :min="1"
-                :max="1000"
-                :step="0.1"
-              />
-            </SignItem>
-          </ui-group-option>
-          <ui-group-option>
-            <ui-input
-              type="checkbox"
-              label="全部范围可视"
-              :modelValue="!!am.globalVisibility"
-              @update:modelValue="(v: boolean) => am.globalVisibility = v"
-            />
-          </ui-group-option>
-        </ui-group>
-      </TabPane>
-      <TabPane key="animation" tab="动画">
-        <ui-group borderBottom>
-          <ui-group-option class="item">
-            <span class="label">加帧</span>
-            <span class="oper"> <ui-icon type="add" ctrl /> </span>
-          </ui-group-option>
-          <ui-group-option class="item">
-            <span class="label">路径</span>
-            <span class="oper"> <ui-icon type="add" ctrl /> </span>
-          </ui-group-option>
-          <ui-group-option class="item">
-            <span class="label">字幕</span>
-            <span class="oper"> <ui-icon type="add" ctrl /> </span>
-          </ui-group-option>
-        </ui-group>
-        <ui-group borderBottom>
-          <ui-group-option class="item">
-            <span class="label">动作库</span>
-          </ui-group-option>
-          <ui-group-option class="actions">
-            <span v-for="action in amActions">
-              <img :src="action.url" />
-            </span>
-          </ui-group-option>
-        </ui-group>
-      </TabPane>
-    </Tabs>
-    <div></div>
-  </RightFillPano>
-</template>
-
-<script lang="ts" setup>
-import { Switch, Slider, TabPane, Tabs } from "ant-design-vue";
-import { AnimationModel } from "@/api";
-import { RightFillPano } from "@/layout";
-import SignItem from "@/views/tagging-position/sign-item.vue";
-import { ref } from "vue";
-import { amActions } from "@/store/animation";
-
-defineProps<{ am: AnimationModel }>();
-const activeKey = ref("setting");
-</script>
-
-<style scoped lang="scss">
-
-.edit-path-point {
-  position: absolute;
-  right: 0;
-  height: 100%;
-  top: 0;
-  --editor-menu-right: 0px;
-}
-
-.item {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-}
-.pin-position {
-  position: absolute;
-  left: 50%;
-  transform: translate(-50%);
-  width: 64px;
-  height: 64px;
-  background: rgba(27, 27, 28, 0.8);
-  border-radius: 50%;
-  bottom: 20px;
-  z-index: 9;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.actions {
-  display: flex;
-  flex-wrap: wrap;
-
-  span {
-    width: calc((100% - 28px) / 3);
-    background: #fff;
-    margin-top: 7px;
-    margin-bottom: 7px;
-    height: 80px;
-    cursor: pointer;
-
-    &:nth-child(3n - 1) {
-      margin-left: 14px;
-      margin-right: 14px;
-    }
-
-    img {
-      width: 100%;
-      height: 100%;
-      object-fit: cover;
-    }
-  }
-}
-</style>
-
-<style lang="scss">
-.animation-right .ui-editor-toolbox {
-  padding-top: 0;
-  .ant-tabs-tab {
-    font-size: 16px;
-    padding: 16px 10px;
-  }
-}
-</style>

+ 63 - 0
src/views/animation/right/action.vue

@@ -0,0 +1,63 @@
+<template>
+  <Tabs activeKey="t" width="100%">
+    <TabPane key="t" tab="设置动画">
+      <ui-group>
+        <ui-group-option>
+          <SignItem label="名称" not-apply>
+            <ui-input
+              width="100%"
+              type="text"
+              ref="nameInput"
+              class="nameInput"
+              placeholder="请输入名称"
+              v-model="data.name"
+              :maxlength="100"
+            />
+          </SignItem>
+        </ui-group-option>
+        <ui-group-option>
+          <SignItem label="幅度" not-apply>
+            <Slider v-model:value="data.amplitude" :min="12" :max="60" :step="0.1" />
+          </SignItem>
+        </ui-group-option>
+        <ui-group-option>
+          <SignItem label="速度" not-apply>
+            <Slider v-model:value="data.speed" :min="12" :max="60" :step="0.1" />
+          </SignItem>
+        </ui-group-option>
+
+        <ui-group-option class="item">
+          <span class="label">持续时间</span>
+          <span class="oper">
+            <ui-input
+              width="75px"
+              type="number"
+              ref="nameInput"
+              placeholder="请输入"
+              :modelValue="data.duration"
+              @update:modelValue="(dur: number) => $emit('updateDuration', dur)"
+            />
+            S
+          </span>
+        </ui-group-option>
+      </ui-group>
+    </TabPane>
+  </Tabs>
+</template>
+
+<script lang="ts" setup>
+import { Slider, TabPane, Tabs } from "ant-design-vue";
+import { AnimationModelAction } from "@/api";
+import SignItem from "@/views/tagging-position/sign-item.vue";
+
+defineProps<{ data: AnimationModelAction }>();
+defineEmits<{ (e: 'updateDuration', dur: number): void }>()
+</script>
+
+<style scoped lang="scss">
+.item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+</style>

+ 156 - 0
src/views/animation/right/am.vue

@@ -0,0 +1,156 @@
+<template>
+  <Tabs v-model:activeKey="activeKey" width="100%">
+    <TabPane key="setting" tab="设置">
+      <ui-group borderBottom>
+        <ui-group-option>
+          <SignItem label="名称" not-apply>
+            <ui-input
+              width="100%"
+              type="text"
+              ref="nameInput"
+              class="nameInput"
+              placeholder="请输入名称"
+              v-model="am.title"
+              :maxlength="100"
+            />
+          </SignItem>
+        </ui-group-option>
+        <ui-group-option class="item">
+          <span class="label">显示名称</span>
+          <span class="oper"> <Switch v-model:checked="am.showTitle" /> </span>
+        </ui-group-option>
+      </ui-group>
+
+      <ui-group borderBottom>
+        <ui-group-option>
+          <SignItem label="文字大小" @apply-global="$emit('applyGlobal', 'fontSize')">
+            <Slider v-model:value="am.fontSize" :min="12" :max="60" :step="0.1" />
+          </SignItem>
+        </ui-group-option>
+      </ui-group>
+      <ui-group borderBottom>
+        <ui-group-option>
+          <SignItem
+            label="可见范围"
+            v-if="!am.globalVisibility"
+            @apply-global="$emit('applyGlobal', 'globalVisibility')"
+          >
+            <Slider v-model:value="am.visibilityRange" :min="1" :max="1000" :step="0.1" />
+          </SignItem>
+        </ui-group-option>
+        <ui-group-option>
+          <ui-input
+            type="checkbox"
+            label="全部范围可视"
+            :modelValue="!!am.globalVisibility"
+            @update:modelValue="(v: boolean) => am.globalVisibility = v"
+          />
+        </ui-group-option>
+      </ui-group>
+    </TabPane>
+    <TabPane key="animation" tab="动画">
+      <ui-group borderBottom>
+        <ui-group-option class="item">
+          <span class="label">加帧</span>
+          <span class="oper" @click="$emit('addFrame')">
+            <ui-icon type="add" ctrl />
+          </span>
+        </ui-group-option>
+        <ui-group-option class="item">
+          <span class="label">路径</span>
+          <span class="oper">
+            <ui-icon @click="$emit('addPath')" type="add" ctrl />
+          </span>
+        </ui-group-option>
+        <ui-group-option class="item">
+          <span class="label">字幕</span>
+          <span class="oper">
+            <ui-icon @click="$emit('addSubtitle')" type="add" ctrl />
+          </span>
+        </ui-group-option>
+      </ui-group>
+      <ui-group borderBottom>
+        <ui-group-option class="item">
+          <span class="label">动作库</span>
+        </ui-group-option>
+        <ui-group-option class="actions">
+          <span v-for="action in amActions">
+            <img :src="action.url" />
+          </span>
+        </ui-group-option>
+      </ui-group>
+    </TabPane>
+  </Tabs>
+</template>
+
+<script lang="ts" setup>
+import { Switch, Slider, TabPane, Tabs } from "ant-design-vue";
+import { AnimationModel } from "@/api";
+import SignItem from "@/views/tagging-position/sign-item.vue";
+import { ref } from "vue";
+import { amActions } from "@/store/animation";
+
+defineProps<{ am: AnimationModel }>();
+defineEmits<{
+  (e: "addFrame" | "addPath" | "addSubtitle"): void;
+  (e: "applyGlobal", d: keyof AnimationModel): void;
+}>();
+const activeKey = ref("setting");
+</script>
+
+<style scoped lang="scss">
+.item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.pin-position {
+  position: absolute;
+  left: 50%;
+  transform: translate(-50%);
+  width: 64px;
+  height: 64px;
+  background: rgba(27, 27, 28, 0.8);
+  border-radius: 50%;
+  bottom: 20px;
+  z-index: 9;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.actions {
+  display: flex;
+  flex-wrap: wrap;
+
+  span {
+    width: calc((100% - 28px) / 3);
+    background: #fff;
+    margin-top: 7px;
+    margin-bottom: 7px;
+    height: 80px;
+    cursor: pointer;
+
+    &:nth-child(3n - 1) {
+      margin-left: 14px;
+      margin-right: 14px;
+    }
+
+    img {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+.animation-right .ui-editor-toolbox {
+  padding-top: 0;
+  .ant-tabs-tab {
+    font-size: 16px;
+    padding: 16px 10px;
+  }
+}
+</style>

+ 69 - 0
src/views/animation/right/frame.vue

@@ -0,0 +1,69 @@
+<template>
+  <div>
+    <div class="am-ctrl-pano strengthen">
+      <span
+        v-for="action in actions"
+        ctrl
+        :class="{ active: frameAction === action.key }"
+        @click="
+          $emit('changeFrameAction', {
+            action: frameAction === action.key ? undefined : action.key,
+            frame: data,
+          })
+        "
+        class="fun-ctrl"
+      >
+        <ui-icon :type="action.icon" />
+      </span>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { AnimationModelFrame } from "@/api";
+import { ref } from "vue";
+
+const actions = [
+  { key: "translate", icon: "close" },
+  { key: "rotate", icon: "close" },
+  { key: "scale", icon: "close" },
+  { key: "originTranslate", icon: "close" },
+];
+
+defineProps<{ data: AnimationModelFrame; frameAction?: string }>();
+defineEmits<{
+  (e: "changeFrameAction", d: { action?: string; frame: AnimationModelFrame }): void;
+}>();
+</script>
+
+<style lang="scss" scoped>
+.am-ctrl-pano {
+  z-index: 99;
+  position: absolute;
+  top: calc(var(--editor-head-height) + var(--header-top));
+  margin: 20px;
+  right: 0;
+  border-radius: 4px;
+  height: 40px;
+  background: rgba(27, 27, 28, 0.8);
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 10px;
+
+  span {
+    width: 32px;
+    height: 32px;
+    font-size: 16px;
+    display: flex;
+    border-radius: 4px;
+    align-items: center;
+    cursor: pointer;
+    justify-content: center;
+
+    &.active {
+      background: rgba(0, 200, 175, 0.3);
+    }
+  }
+}
+</style>

+ 96 - 0
src/views/animation/right/index.vue

@@ -0,0 +1,96 @@
+<template>
+  <RightFillPano class="animation-right" v-if="activeAttrib?.key !== 'frames'">
+    <AM
+      v-if="!activeAttrib || !~activeAttrib.ndx"
+      :am="am"
+      @add-frame="emit('addFrame')"
+      @add-path="emit('addPath')"
+      @add-subtitle="emit('addSubtitle')"
+      @apply-global="(k) => emit('applyGlobal', k)"
+    />
+    <template v-else>
+      <ui-icon
+        style="font-size: 16px"
+        type="close"
+        class="close"
+        ctrl
+        @click="emit('update:activeAttrib', undefined)"
+      />
+      <component
+        @updateDuration="setDuration"
+        :is="(comps[activeAttrib.key] as any)"
+        :data="am[activeAttrib.key][activeAttrib.ndx]"
+      />
+    </template>
+  </RightFillPano>
+  <Frame
+    :data="am[activeAttrib.key][activeAttrib.ndx]"
+    :frame-action="frameAction"
+    @change-frame-action="(d) => emit('changeFrameAction', d)"
+    v-else
+  />
+</template>
+
+<script lang="ts" setup>
+import { RightFillPano } from "@/layout";
+import { Active } from "../type";
+import AM from "./am.vue";
+import Action from "./action.vue";
+import Frame from "./frame.vue";
+import Path from "./path.vue";
+import Subtitle from "./subtitle.vue";
+import { checkTLItem } from "@/components/drawing-time-line/check";
+import { Message } from "bill/expose-common";
+import { AnimationModel, AnimationModelFrame } from "@/store/animation";
+
+const props = defineProps<{
+  am: AnimationModel;
+  activeAttrib?: Active;
+  frameAction?: string;
+}>();
+const emit = defineEmits<{
+  (e: "update:activeAttrib", d: undefined): void;
+  (e: "addFrame" | "addPath" | "addSubtitle"): void;
+  (e: "applyGlobal", d: keyof AnimationModel): void;
+  (e: "changeFrameAction", d: { action?: string; frame: AnimationModelFrame }): void;
+}>();
+
+const comps = {
+  actions: Action,
+  paths: Path,
+  subtitles: Subtitle,
+};
+const title = {
+  actions: "动作",
+  paths: "路径",
+  subtitles: "字幕",
+  frames: "",
+};
+const setDuration = (dur: number) => {
+  const ndx = props.activeAttrib!.ndx;
+  const items = props.am[props.activeAttrib!.key];
+  const cur = items[ndx];
+
+  if (!checkTLItem(items, { ...cur, duration: dur }, ndx)) {
+    Message.error("当前时间已存在其他" + title[props.activeAttrib!.key]);
+  } else {
+    cur.duration = dur;
+  }
+};
+</script>
+
+<style lang="scss">
+.close {
+  position: absolute;
+  right: 21px;
+  top: 26px;
+  z-index: 9;
+}
+.animation-right .ui-editor-toolbox {
+  padding-top: 0;
+  .ant-tabs-tab {
+    font-size: 16px;
+    padding: 16px 10px;
+  }
+}
+</style>

+ 72 - 0
src/views/animation/right/path.vue

@@ -0,0 +1,72 @@
+<template>
+  <Tabs activeKey="t" width="100%">
+    <TabPane key="t" tab="设置路径">
+      <ui-group>
+        <ui-group-option>
+          <SignItem label="名称" not-apply>
+            <ui-input
+              width="100%"
+              type="text"
+              ref="nameInput"
+              class="nameInput"
+              placeholder="请输入名称"
+              v-model="data.name"
+              :maxlength="100"
+            />
+          </SignItem>
+        </ui-group-option>
+        <ui-group-option>
+          <SignItem label="路径" not-apply>
+            <ui-input
+              width="100%"
+              type="select"
+              :options="options"
+              placeholder="请选择路径"
+              v-model="data.id"
+            />
+          </SignItem>
+        </ui-group-option>
+        <ui-group-option class="item">
+          <span class="label">终点反向</span>
+          <span class="oper"> <Switch v-model:checked="data.reverse" /> </span>
+        </ui-group-option>
+        <ui-group-option class="item">
+          <span class="label">持续时间</span>
+          <span class="oper">
+            <ui-input
+              width="75px"
+              type="number"
+              placeholder="请输入"
+              :modelValue="data.duration"
+              @update:modelValue="(dur: number) => $emit('updateDuration', dur)"
+            />
+            S
+          </span>
+        </ui-group-option>
+      </ui-group>
+    </TabPane>
+  </Tabs>
+</template>
+
+<script lang="ts" setup>
+import { TabPane, Tabs, Switch } from "ant-design-vue";
+import { AnimationModelPath } from "@/api";
+import { paths } from "@/store";
+import { computed } from "vue";
+import SignItem from "@/views/tagging-position/sign-item.vue";
+
+const options = computed(() =>
+  paths.value.map((item) => ({ label: item.name, value: item.id }))
+);
+
+defineProps<{ data: AnimationModelPath }>();
+defineEmits<{ (e: "updateDuration", dur: number): void }>();
+</script>
+
+<style scoped lang="scss">
+.item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+</style>

+ 78 - 0
src/views/animation/right/subtitle.vue

@@ -0,0 +1,78 @@
+<template>
+  <Tabs activeKey="t" width="100%">
+    <TabPane key="t" tab="设置旁白">
+      <ui-group>
+        <ui-group-option>
+          <SignItem label="名称" not-apply>
+            <ui-input
+              width="100%"
+              type="text"
+              ref="nameInput"
+              class="nameInput"
+              placeholder="请输入名称"
+              v-model="data.name"
+              :maxlength="100"
+            />
+          </SignItem>
+        </ui-group-option>
+
+        <ui-group-option>
+          <SignItem label="旁白" not-apply>
+            <ui-input
+              class="input"
+              width="100%"
+              height="158px"
+              type="richtext"
+              placeholder="请输入旁白"
+              v-model="data.content"
+              :maxlength="200"
+            />
+          </SignItem>
+        </ui-group-option>
+
+        <ui-group-option class="item">
+          <span class="label">画面停留</span>
+          <span class="oper">
+            <ui-input
+              width="75px"
+              type="number"
+              placeholder="请输入"
+              :modelValue="data.duration"
+              @update:modelValue="(dur: number) => $emit('updateDuration', dur)"
+            />
+            S
+          </span>
+        </ui-group-option>
+
+        <ui-group-option class="item">
+          <span class="label">背景颜色</span>
+          <span class="oper">
+            <ui-input
+              width="50px"
+              type="color"
+              placeholder="请输入名称"
+              v-model="data.background"
+            />
+          </span>
+        </ui-group-option>
+      </ui-group>
+    </TabPane>
+  </Tabs>
+</template>
+
+<script lang="ts" setup>
+import { TabPane, Tabs } from "ant-design-vue";
+import { AnimationModelSubtitle } from "@/api";
+import SignItem from "@/views/tagging-position/sign-item.vue";
+
+defineProps<{ data: AnimationModelSubtitle }>();
+defineEmits<{ (e: "updateDuration", dur: number): void }>();
+</script>
+
+<style scoped lang="scss">
+.item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+</style>

+ 4 - 0
src/views/animation/type.ts

@@ -0,0 +1,4 @@
+export type Active = {
+  key: "frames" | "actions" | "subtitles" | "paths";
+  ndx: number;
+};

+ 2 - 2
src/views/tagging-position/sign-item.vue

@@ -7,14 +7,14 @@
           {{ label }}
         </template>
       </span>
-      <span @click="$emit('applyGlobal')" class="apply">应用到全部</span>
+      <span v-if="!notApply" @click="$emit('applyGlobal')" class="apply">应用到全部</span>
     </div>
     <div class="item-content" v-if="$slots.default"><slot /></div>
   </div>
 </template>
 
 <script lang="ts" setup>
-defineProps<{ label?: string }>();
+defineProps<{ label?: string, notApply?: boolean }>();
 defineEmits<{ (e: "applyGlobal"): void }>();
 </script>