ソースを参照

Merge branch 'dev' of http://124.70.149.18:10880/lianghj/queenshow into dev

bianjiang 1 年間 前
コミット
560aed55ae
36 ファイル変更1217 行追加110 行削除
  1. 1 0
      .browserslistrc
  2. 2 0
      package.json
  3. 3 0
      src/assets/icons/components/IconCut.tsx
  4. 3 0
      src/assets/icons/components/IconPalette.tsx
  5. 3 0
      src/assets/icons/components/IconPicker.tsx
  6. 3 0
      src/assets/icons/components/IconRelate.tsx
  7. 4 0
      src/assets/icons/index.ts
  8. 1 0
      src/assets/icons/svg/cut.svg
  9. 1 0
      src/assets/icons/svg/palette.svg
  10. 1 0
      src/assets/icons/svg/picker.svg
  11. 1 0
      src/assets/icons/svg/relate.svg
  12. 1 1
      src/modules/editor/components/CompUI/basicUI/Page/component.tsx
  13. 1 1
      src/modules/editor/components/CompUI/basicUI/Web3D/index.ts
  14. 1 4
      src/modules/editor/components/CompUI/defines/createAttrsForm.tsx
  15. 6 6
      src/modules/editor/components/CompUI/defines/formOpts/createColorOpts.ts
  16. 133 0
      src/modules/editor/components/CompUI/formItems/NewColorPicker/ColorLib.tsx
  17. 136 0
      src/modules/editor/components/CompUI/formItems/NewColorPicker/ColorList.tsx
  18. 197 0
      src/modules/editor/components/CompUI/formItems/NewColorPicker/Panel.tsx
  19. 143 0
      src/modules/editor/components/CompUI/formItems/NewColorPicker/Picker.tsx
  20. 114 0
      src/modules/editor/components/CompUI/formItems/NewColorPicker/index.tsx
  21. 129 55
      src/modules/editor/components/CompUI/formItems/Size.tsx
  22. 4 2
      src/modules/editor/components/CompUI/formItems/Slider.tsx
  23. 7 5
      src/modules/editor/components/TipIcons/index.ts
  24. 217 0
      src/modules/editor/components/Viewport/Slider/SliderLeft/Sources/EditVideoModal.tsx
  25. 13 10
      src/modules/editor/components/Viewport/Slider/SliderLeft/Sources/List.tsx
  26. 19 2
      src/modules/editor/components/Viewport/Slider/SliderLeft/Sources/SourceItem.tsx
  27. 1 1
      src/modules/editor/components/Viewport/Slider/SliderLeft/index.tsx
  28. 2 2
      src/modules/editor/components/Viewport/Toolbar/index.tsx
  29. 0 2
      src/modules/editor/components/index.ts
  30. 6 3
      src/modules/editor/objects/DesignTemp/creates/createCompStyle.ts
  31. 3 2
      src/modules/editor/typings.ts
  32. 7 5
      src/modules/resource/actions/material.ts
  33. 22 8
      src/modules/resource/http.ts
  34. 2 1
      src/pages/editor/EditPage/index.tsx
  35. 7 0
      src/typings/pro.d.ts
  36. 23 0
      yarn.lock

+ 1 - 0
.browserslistrc

@@ -2,3 +2,4 @@
 last 2 versions
 not dead
 not ie 11
+ios >= 9

+ 2 - 0
package.json

@@ -28,6 +28,7 @@
     "@ckeditor/ckeditor5-paragraph": "^38.0.0",
     "@ckeditor/ckeditor5-theme-lark": "^38.0.0",
     "@ckeditor/ckeditor5-vue": "^5.1.0",
+    "@jaames/iro": "^5.5.2",
     "@linaria/core": "^4.1.1",
     "@queenjs-modules/auth": "^0.0.20",
     "@queenjs-modules/queditor": "0.0.13",
@@ -76,6 +77,7 @@
     "three": "^0.146.0",
     "vconsole": "^3.15.1",
     "vue": "^3.2.45",
+    "vue-color-kit": "^1.0.6",
     "vue-dndrop": "^1.3.1",
     "vue-router": "^4.0.3",
     "vue-types": "^4.2.1"

+ 3 - 0
src/assets/icons/components/IconCut.tsx

@@ -0,0 +1,3 @@
+
+import { createIcon } from '@queenjs/icons';
+export const IconCut = createIcon(<svg viewBox="0 0 20 20"><g transform="translate(-191 -296)"><g transform="translate(26)"><rect fill="none" opacity="0.9" width="20" height="20" rx="3" transform="translate(165 296)"/></g><g transform="translate(189.975 293.609)"><path fill="#262626" opacity="0.3" d="M36.854,9v9.514H34V15.977h2.854v-4.44H34V9Z" transform="translate(-19.803 -1.366)"/><path fill="#262626" opacity="0.3" d="M5,11.537V9H7.854v2.537H5v4.44H7.854v2.537H5V11.537Z" transform="translate(0 -1.366)"/><path fill="none" stroke="#262626" stroke-linecap="round" stroke-width="0.8px" stroke-linejoin="round" d="M17.051,11.537V9H14.2m2.854,2.537v4.44m0-4.44H14.2m-9.2,0V9H7.854M5,11.537v4.44m0-4.44H7.854M5,15.977v2.537H7.854M5,15.977H7.854m9.2,0v2.537H14.2m2.854-2.537H14.2M14.2,9v2.537M14.2,9H12.928M14.2,18.514V15.977m0,2.537H12.928M7.854,9v2.537M7.854,9H9.123M7.854,18.514V15.977m0,2.537H9.123M7.854,11.537H9.123m5.074,0H12.928m1.269,4.44H12.928m-5.074,0H9.123" transform="translate(0 -1.366)"/><path fill="none" stroke="#262626" stroke-linecap="round" stroke-width="0.8px" d="M24,7V8.269" transform="translate(-12.975)"/><path fill="none" stroke="#262626" stroke-linecap="round" stroke-width="0.8px" d="M24,17v1.269" transform="translate(-12.975 -6.829)"/><path fill="none" stroke="#262626" stroke-linecap="round" stroke-width="0.8px" d="M24,27v1.269" transform="translate(-12.975 -13.657)"/><path fill="none" stroke="#262626" stroke-linecap="round" stroke-width="0.8px" d="M24,37v1.269" transform="translate(-12.975 -20.486)"/></g></g></svg>)

+ 3 - 0
src/assets/icons/components/IconPalette.tsx

@@ -0,0 +1,3 @@
+
+import { createIcon } from '@queenjs/icons';
+export const IconPalette = createIcon(<svg viewBox="0 0 30 30"><g transform="translate(-1872 -333)"><rect fill="none" width="30" height="30" rx="4" transform="translate(1872 333)"/><g opacity="0.9" transform="translate(1872.471 218.464)"><path fill="currentColor" d="M140.693,549.657m-.454,0a.454.454,0,1,0,.454-.454A.454.454,0,0,0,140.239,549.657Z" transform="translate(-131.626 -419.329)"/><path fill="currentColor" d="M185.074,388.8m-.712,0a.712.712,0,1,0,.712-.712.712.712,0,0,0-.712.712Z" transform="translate(-174.841 -261.003)"/><path fill="currentColor" d="M351.334,258.042m-.841,0a.841.841,0,1,0,.841-.841A.841.841,0,0,0,350.493,258.042Z" transform="translate(-338.472 -131.874)"/><path fill="currentColor" d="M570.893,243.271m-.919,0a.919.919,0,1,0,.919-.919A.919.919,0,0,0,569.974,243.271Z" transform="translate(-554.319 -117.104)"/><path fill="currentColor" d="M547.758,237.414l-.012-.011a.406.406,0,0,0-.572.04l-6.116,7.042-.773,1.52,1.387-.966,6.127-7.055A.405.405,0,0,0,547.758,237.414Z" transform="translate(-525.334 -112.37)"/><path fill="currentColor" d="M21.375,128.96a.414.414,0,0,0-.412.381h0A7.823,7.823,0,0,1,17.891,134c-2.695,1.75-5.78.962-6.353-.049a9.818,9.818,0,0,0-1.068-1.719c-.733-.61-2.587.235-2.976-.689-.724-1.718.984-6.172,5.217-7.275a7.679,7.679,0,0,1,6.749,1.214h0a.4.4,0,1,0,.5-.63,8.087,8.087,0,0,0-8.587-.909c-5.126,2.3-5.45,7.1-4.379,8.3s2.637.251,2.984.649c.222.254.51.929.889,1.541.549.887,3.293,2.749,7.48.265,2.512-1.49,3.412-4.923,3.444-5.3h0c0-.011,0-.021,0-.032A.414.414,0,0,0,21.375,128.96Z" transform="translate(0)"/></g></g></svg>)

+ 3 - 0
src/assets/icons/components/IconPicker.tsx

@@ -0,0 +1,3 @@
+
+import { createIcon } from '@queenjs/icons';
+export const IconPicker = createIcon(<svg viewBox="0 0 36 36"><g transform="translate(-8901 -1508)"><rect fill="none" width="36" height="36" rx="4" transform="translate(8901 1508)"/><g transform="translate(14.236 -72.08)"><path fill="#fff" opacity="0.7" d="M8897.759,1607.859s4.09.868,5.912-.411-2.936,3.535-2.936,3.535l-3.76.659Z" transform="translate(0.91 -6.628)"/><path fill="none" stroke="#fff" stroke-linecap="round" d="M8884.98,1589.079l6.822,6.821" transform="translate(18.755 3.213)"/><path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" d="M8886.931,1586.8l-10.344,10.345-.994,4.211,4.074-.828,10.268-10.828Z" transform="translate(22 4)"/></g></g></svg>)

+ 3 - 0
src/assets/icons/components/IconRelate.tsx

@@ -0,0 +1,3 @@
+
+import { createIcon } from '@queenjs/icons';
+export const IconRelate = createIcon(<svg viewBox="0 0 22 22"><g transform="translate(-2783 -4497)"><rect fill="none" width="22" height="22" transform="translate(2783 4497)"/><g transform="translate(2800.435 4511.481) rotate(180)"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" d="M-189.919,68.962h-2.6A3.491,3.491,0,0,1-196,65.481h0A3.491,3.491,0,0,1-192.519,62h2.6" transform="translate(194 -62)"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" d="M-196,68.962h2.6a3.491,3.491,0,0,0,3.481-3.481h0A3.491,3.491,0,0,0-193.4,62H-196" transform="translate(204.354 -62)"/></g></g></svg>)

+ 4 - 0
src/assets/icons/index.ts

@@ -20,6 +20,7 @@ export * from "./components/IconCombination";
 export * from "./components/IconCroperr";
 export * from "./components/IconCross";
 export * from "./components/IconCube";
+export * from "./components/IconCut";
 export * from "./components/IconDelete";
 export * from "./components/IconEmpty";
 export * from "./components/IconFloatOff";
@@ -34,11 +35,14 @@ export * from "./components/IconLock";
 export * from "./components/IconLocked";
 export * from "./components/IconMove";
 export * from "./components/IconMusic";
+export * from "./components/IconPalette";
+export * from "./components/IconPicker";
 export * from "./components/IconPlay";
 export * from "./components/IconPlay2";
 export * from "./components/IconPreview";
 export * from "./components/IconProfile";
 export * from "./components/IconQueen";
+export * from "./components/IconRelate";
 export * from "./components/IconResizeY";
 export * from "./components/IconRight";
 export * from "./components/IconRotate";

+ 1 - 0
src/assets/icons/svg/cut.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 20 20"><g transform="translate(-191 -296)"><g transform="translate(26)"><rect fill="none" opacity="0.9" width="20" height="20" rx="3" transform="translate(165 296)"/></g><g transform="translate(189.975 293.609)"><path fill="#262626" opacity="0.3" d="M36.854,9v9.514H34V15.977h2.854v-4.44H34V9Z" transform="translate(-19.803 -1.366)"/><path fill="#262626" opacity="0.3" d="M5,11.537V9H7.854v2.537H5v4.44H7.854v2.537H5V11.537Z" transform="translate(0 -1.366)"/><path fill="none" stroke="#262626" stroke-linecap="round" stroke-width="0.8px" stroke-linejoin="round" d="M17.051,11.537V9H14.2m2.854,2.537v4.44m0-4.44H14.2m-9.2,0V9H7.854M5,11.537v4.44m0-4.44H7.854M5,15.977v2.537H7.854M5,15.977H7.854m9.2,0v2.537H14.2m2.854-2.537H14.2M14.2,9v2.537M14.2,9H12.928M14.2,18.514V15.977m0,2.537H12.928M7.854,9v2.537M7.854,9H9.123M7.854,18.514V15.977m0,2.537H9.123M7.854,11.537H9.123m5.074,0H12.928m1.269,4.44H12.928m-5.074,0H9.123" transform="translate(0 -1.366)"/><path fill="none" stroke="#262626" stroke-linecap="round" stroke-width="0.8px" d="M24,7V8.269" transform="translate(-12.975)"/><path fill="none" stroke="#262626" stroke-linecap="round" stroke-width="0.8px" d="M24,17v1.269" transform="translate(-12.975 -6.829)"/><path fill="none" stroke="#262626" stroke-linecap="round" stroke-width="0.8px" d="M24,27v1.269" transform="translate(-12.975 -13.657)"/><path fill="none" stroke="#262626" stroke-linecap="round" stroke-width="0.8px" d="M24,37v1.269" transform="translate(-12.975 -20.486)"/></g></g></svg>

+ 1 - 0
src/assets/icons/svg/palette.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 30 30"><g transform="translate(-1872 -333)"><rect fill="none" width="30" height="30" rx="4" transform="translate(1872 333)"/><g opacity="0.9" transform="translate(1872.471 218.464)"><path fill="currentColor" d="M140.693,549.657m-.454,0a.454.454,0,1,0,.454-.454A.454.454,0,0,0,140.239,549.657Z" transform="translate(-131.626 -419.329)"/><path fill="currentColor" d="M185.074,388.8m-.712,0a.712.712,0,1,0,.712-.712.712.712,0,0,0-.712.712Z" transform="translate(-174.841 -261.003)"/><path fill="currentColor" d="M351.334,258.042m-.841,0a.841.841,0,1,0,.841-.841A.841.841,0,0,0,350.493,258.042Z" transform="translate(-338.472 -131.874)"/><path fill="currentColor" d="M570.893,243.271m-.919,0a.919.919,0,1,0,.919-.919A.919.919,0,0,0,569.974,243.271Z" transform="translate(-554.319 -117.104)"/><path fill="currentColor" d="M547.758,237.414l-.012-.011a.406.406,0,0,0-.572.04l-6.116,7.042-.773,1.52,1.387-.966,6.127-7.055A.405.405,0,0,0,547.758,237.414Z" transform="translate(-525.334 -112.37)"/><path fill="currentColor" d="M21.375,128.96a.414.414,0,0,0-.412.381h0A7.823,7.823,0,0,1,17.891,134c-2.695,1.75-5.78.962-6.353-.049a9.818,9.818,0,0,0-1.068-1.719c-.733-.61-2.587.235-2.976-.689-.724-1.718.984-6.172,5.217-7.275a7.679,7.679,0,0,1,6.749,1.214h0a.4.4,0,1,0,.5-.63,8.087,8.087,0,0,0-8.587-.909c-5.126,2.3-5.45,7.1-4.379,8.3s2.637.251,2.984.649c.222.254.51.929.889,1.541.549.887,3.293,2.749,7.48.265,2.512-1.49,3.412-4.923,3.444-5.3h0c0-.011,0-.021,0-.032A.414.414,0,0,0,21.375,128.96Z" transform="translate(0)"/></g></g></svg>

+ 1 - 0
src/assets/icons/svg/picker.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 36 36"><g transform="translate(-8901 -1508)"><rect fill="none" width="36" height="36" rx="4" transform="translate(8901 1508)"/><g transform="translate(14.236 -72.08)"><path fill="#fff" opacity="0.7" d="M8897.759,1607.859s4.09.868,5.912-.411-2.936,3.535-2.936,3.535l-3.76.659Z" transform="translate(0.91 -6.628)"/><path fill="none" stroke="#fff" stroke-linecap="round" d="M8884.98,1589.079l6.822,6.821" transform="translate(18.755 3.213)"/><path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" d="M8886.931,1586.8l-10.344,10.345-.994,4.211,4.074-.828,10.268-10.828Z" transform="translate(22 4)"/></g></g></svg>

+ 1 - 0
src/assets/icons/svg/relate.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 22 22"><g transform="translate(-2783 -4497)"><rect fill="none" width="22" height="22" transform="translate(2783 4497)"/><g transform="translate(2800.435 4511.481) rotate(180)"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" d="M-189.919,68.962h-2.6A3.491,3.491,0,0,1-196,65.481h0A3.491,3.491,0,0,1-192.519,62h2.6" transform="translate(194 -62)"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" d="M-196,68.962h2.6a3.491,3.491,0,0,0,3.481-3.481h0A3.491,3.491,0,0,0-193.4,62H-196" transform="translate(204.354 -62)"/></g></g></svg>

+ 1 - 1
src/modules/editor/components/CompUI/basicUI/Page/component.tsx

@@ -44,7 +44,7 @@ export const Component = defineComponent({
           style={style}
           class={[comp.value.pageMode != "short" && "!h-auto", editor.store.isEditMode ? pageEditStyle : ""]}
         >
-          <div class="relative z-1000" style={contextStyle}>
+          <div class="relative z-999" style={contextStyle}>
             {slots.Container?.(
               children.default.map((compId) => {
                 const comp = helper.findComp(compId);

+ 1 - 1
src/modules/editor/components/CompUI/basicUI/Web3D/index.ts

@@ -48,7 +48,7 @@ export const Form = createAttrsForm([
     },
   },
   {
-    label: "是否内嵌3D",
+    label: "内嵌3D",
     dataIndex: "value.inline",
     component: "Switch",
   },

+ 1 - 4
src/modules/editor/components/CompUI/defines/createAttrsForm.tsx

@@ -12,12 +12,9 @@ import { css } from "@linaria/core";
 
 export const layoutColumns: ColumnItem[] = [
   {
-    // label: "尺寸",
+    label: "尺寸",
     dataIndex: "layout.size",
     component: Size,
-    props: {
-      labels: ["宽度", "高度"],
-    },
   },
   {
     label: "对齐",

+ 6 - 6
src/modules/editor/components/CompUI/defines/formOpts/createColorOpts.ts

@@ -1,8 +1,8 @@
 import { ColumnItem } from "@queenjs/components/FormUI";
-import BaseColorPicker from "../../formItems/BaseColorPicker";
 import { colorToHex, hexToColor } from "@queenjs/utils";
+import NewColorPicker from "../../formItems/NewColorPicker";
 
-export function createColorOpts(): Pick<
+export function createColorOpts1(): Pick<
   ColumnItem,
   "getValue" | "changeExtra" | "component" | "itemProps"
 > {
@@ -22,14 +22,14 @@ export function createColorOpts(): Pick<
   };
 }
 
-export function createColorOpts2(): Pick<
+export function createColorOpts(): Pick<
   ColumnItem,
-  "getValue" | "changeExtra" | "component" | "itemProps"
+  "getValue" | "changeExtra" | "component" | "itemProps" | "props"
 > {
   return {
-    component: BaseColorPicker,
+    component: NewColorPicker,
     itemProps: {
-      style: { alignItems: "flex-start" },
+      style: { justifyContent: "space-between" },
     },
     getValue(v) {
       return v || "#ffffff";

+ 133 - 0
src/modules/editor/components/CompUI/formItems/NewColorPicker/ColorLib.tsx

@@ -0,0 +1,133 @@
+import { Divider } from "ant-design-vue";
+import { defineComponent, onMounted, reactive } from "vue";
+import { any, bool } from "vue-types";
+import ColorList, { gradientColorType } from "./ColorList";
+
+const pureColors = [
+  "#111111",
+  "#545454",
+  "#737373",
+  "#A6A6A6",
+  "#FFFFFF",
+  "#EC635D",
+  "#F5BF6A",
+  "#93D668",
+  "#5970F6",
+  "#8354F5",
+];
+
+const gradientColors: gradientColorType[] = [
+  {
+    deg: 90,
+    type: "linear",
+    colors: ["#212121", "#656565"],
+  },
+  {
+    deg: 90,
+    type: "linear",
+    colors: ["#AEAEAE", "#F0F0F0"],
+  },
+  {
+    deg: 90,
+    type: "linear",
+    colors: ["#FCEFBB", "#F4B8EA"],
+  },
+  {
+    deg: 90,
+    type: "linear",
+    colors: ["#CCF1E0", "#A5C3F4"],
+  },
+  {
+    deg: 90,
+    type: "linear",
+    colors: ["#EB995A", "#F8D86E"],
+  },
+  {
+    deg: 90,
+    type: "linear",
+    colors: ["#EB4F40", "#EF8E58"],
+  },
+  {
+    deg: 90,
+    type: "linear",
+    colors: ["#85C6BD", "#F0DD75"],
+  },
+  {
+    deg: 90,
+    type: "linear",
+    colors: ["#8367F3", "#82CFE6"],
+  },
+  {
+    deg: 90,
+    type: "linear",
+    colors: ["#ED7FB1", "#F8D378"],
+  },
+  {
+    deg: 90,
+    type: "linear",
+    colors: ["#935EDF", "#E69069"],
+  },
+];
+
+export default defineComponent({
+  props: {
+    value: any<string | gradientColorType>().isRequired,
+    showGradient: bool().def(true),
+  },
+  emits: ["change"],
+  setup(props, { emit }) {
+    const state = reactive({
+      latestColors: [],
+    }) as any;
+
+    const selectColor = (data: any) => {
+      emit("change", data);
+    };
+
+    onMounted(() => {
+      const list = localStorage.getItem("latestColors");
+      let colors = list ? JSON.parse(list) : ["#fff"];
+      if (!props.showGradient) {
+        colors = colors.filter((d: any) => typeof d == "string");
+      }
+      colors.splice(10);
+      state.latestColors = colors;
+    });
+
+    return () => {
+      const { showGradient } = props;
+      return (
+        <div class="w-280px px-5px py-10px rounded">
+          <div class="flex items-center justify-between">
+            <span class="text-16px">色彩库</span>
+            {/* <IconClose class="text-18px" /> */}
+          </div>
+          <div class="mt-15px mb-10px">近期使用</div>
+          <ColorList
+            value={props.value}
+            data={state.latestColors}
+            onChange={(data) => emit("change", data)}
+          />
+          <Divider />
+          <div class="mb-10px">纯色库</div>
+          <ColorList
+            value={props.value}
+            data={pureColors}
+            onChange={selectColor}
+          />
+          {showGradient && (
+            <>
+              <Divider />
+              <div class="mb-10px">渐变</div>
+              <ColorList
+                value={props.value}
+                data={gradientColors}
+                onChange={selectColor}
+              />
+            </>
+          )}
+        </div>
+      );
+    };
+  },
+});

+ 136 - 0
src/modules/editor/components/CompUI/formItems/NewColorPicker/ColorList.tsx

@@ -0,0 +1,136 @@
+import { css, cx } from "@linaria/core";
+import { IconClose } from "@queenjs/icons";
+import { defineComponent } from "vue";
+import { any, array, bool, number, string } from "vue-types";
+
+// radial-gradient(circle at 50% 50%, rgb(255, 255, 255), rgb(0, 253, 3) 100%);
+// radial-gradient(red 10px, yellow 30%, #1e90ff 50%);
+// linear-gradient(70deg, blue, pink);
+
+export type gradientColorType = {
+  deg?: number;
+  type: "radial" | "linear";
+  colors: string[];
+};
+
+export function formatColor(item: string | gradientColorType) {
+  if (typeof item == "string") {
+    return item;
+  } else if (item.type == "radial") {
+    return `radial-gradient( ${item.colors.join(",")}`;
+  } else if (item.type == "linear") {
+    return `linear-gradient(${item.deg}deg, ${item.colors?.join(",")})`;
+  } else return "#ffffff";
+}
+
+export default defineComponent({
+  props: {
+    value: any<string | gradientColorType>().isRequired,
+    data: array<string | gradientColorType>().isRequired,
+    columns: number().def(5),
+    showDelete: bool().def(false),
+    min: number().def(2),
+  },
+  emits: ["change", "delete", "add"],
+  setup(props, { emit }) {
+    return () => {
+      const { columns, value, showDelete, min } = props;
+      const list = props.data;
+      const propsColor = formatColor(value);
+      return (
+        <div class={cx("grid gap-16px", `grid-cols-${columns}`)}>
+          {list.map((item: string | gradientColorType, index: number) => {
+            const thisColor = formatColor(item);
+            const active = thisColor === propsColor ? true : false;
+            return (
+              <ColorItem
+                key={index}
+                color={thisColor}
+                columns={columns}
+                active={active}
+                onDelete={() => emit("delete", index)}
+                showDelete={showDelete && list.length > min}
+                onChange={() => emit("change", item)}
+              />
+            );
+          })}
+        </div>
+      );
+    };
+  },
+});
+
+export const ColorItem = defineComponent({
+  props: {
+    color: string(),
+    active: bool(),
+    columns: number().def(5),
+    showDelete: bool().def(false),
+    min: number().def(2),
+  },
+  emits: ["change", "delete"],
+  setup(props, { emit }) {
+    return () => {
+      const { active, color, columns, showDelete } = props;
+      return (
+        <div class="relative group">
+          <div
+            class={cx(
+              "h-38px rounded-2px cursor-pointer overflow-hidden",
+              itemStyles,
+              active && "active",
+              columns == 4 ? "w-52px" : "w-38px"
+            )}
+            onClick={() => emit("change")}
+          >
+            <div class="h-full w-full" style={{ background: color }}></div>
+          </div>
+          {showDelete && (
+            <IconClose
+              class="absolute z-10 -right-8px -top-8px p-2px bg-dark-50 bg-opacity-80 rounded-1/2 text-light-50 cursor-pointer transition opacity-0 group-hover:opacity-100"
+              onClick={() => emit("delete")}
+            />
+          )}
+        </div>
+      );
+    };
+  },
+});
+
+const itemStyles = css`
+  position: relative;
+  background-color: #fff;
+  background-image: linear-gradient(
+      -45deg,
+      rgba(57, 76, 96, 0.15) 25%,
+      transparent 25%,
+      transparent 75%,
+      rgba(57, 76, 96, 0.15) 75%
+    ),
+    linear-gradient(
+      -45deg,
+      rgba(57, 76, 96, 0.15) 25%,
+      transparent 25%,
+      transparent 75%,
+      rgba(57, 76, 96, 0.15) 75%
+    );
+
+  background-position: 0 0, 6px 6px;
+  background-size: 12px 12px;
+  &:before {
+    border-radius: 4px;
+    bottom: 0;
+    content: "";
+    left: 0;
+    position: absolute;
+    right: 0;
+    top: 0;
+    transition: box-shadow 0.2s ease-in-out;
+  }
+  &:hover:before {
+    box-shadow: #fff 0px 0px 0px 1px inset, #000 0px 0px 0px 3px inset;
+  }
+  &.active:before {
+    box-shadow: #ea9e40 0px 0px 0px 1px inset, #000 0px 0px 0px 3px inset;
+  }
+`;

+ 197 - 0
src/modules/editor/components/CompUI/formItems/NewColorPicker/Panel.tsx

@@ -0,0 +1,197 @@
+import { css } from "@linaria/core";
+import { IconAddLine } from "@queenjs/icons";
+import { Divider, Popover, Tabs } from "ant-design-vue";
+import { computed, defineComponent, onMounted, reactive, watch } from "vue";
+import { any, bool } from "vue-types";
+import ColorList, { ColorItem, gradientColorType } from "./ColorList";
+import Picker from "./Picker";
+
+export default defineComponent({
+  props: {
+    value: any<string | gradientColorType>().isRequired,
+    showGradient: bool().def(true),
+  },
+  emits: ["change"],
+  setup(props, { emit }) {
+    const state = reactive({
+      key: typeof props.value == "string" ? "0" : "1",
+    });
+
+    return () => {
+      const { showGradient } = props;
+      if (!showGradient) {
+        return (
+          <PureColor
+            value={props.value}
+            onChange={(data) => emit("change", data)}
+          />
+        );
+      }
+      return (
+        <div class="w-280px px-5px pb-10px ">
+          <Tabs
+            centered
+            destroyInactiveTabPane
+            activeKey={state.key}
+            class={tabStyles}
+            onChange={(v: any) => (state.key = v)}
+          >
+            <Tabs.TabPane tab="纯色" key={"0"}>
+              <PureColor
+                value={props.value}
+                onChange={(data) => emit("change", data)}
+              />
+            </Tabs.TabPane>
+            <Tabs.TabPane tab="渐变" key={"1"}>
+              <MutiColor
+                value={props.value}
+                onChange={(data) => emit("change", data)}
+              />
+            </Tabs.TabPane>
+          </Tabs>
+        </div>
+      );
+    };
+  },
+});
+
+const PureColor = defineComponent({
+  props: {
+    value: any<string | gradientColorType>().isRequired,
+  },
+  emits: ["change"],
+  setup(props, { emit }) {
+    onMounted(() => {
+      if (typeof props.value !== "string") {
+        emit("change", props.value.colors[0]);
+      }
+    });
+    return () => {
+      const color =
+        typeof props.value == "string" ? props.value : props.value.colors[0];
+      return <Picker value={color} onChange={(data) => emit("change", data)} />;
+    };
+  },
+});
+
+const MutiColor = defineComponent({
+  props: {
+    value: any<string | gradientColorType>().isRequired,
+  },
+  emits: ["change"],
+  setup(props, { emit }) {
+    const state = reactive({
+      colors: getColors(),
+      gradientColors: computed(() => {
+        const list: any = [
+          { type: "linear", deg: 90 },
+          { type: "linear", deg: 180 },
+          { type: "linear", deg: 135 },
+          { type: "radial" },
+        ];
+        return list.map((d: any) => ({ ...d, colors: state.colors }));
+      }),
+    });
+
+    function getColors() {
+      let colors: string[] = [];
+      if (typeof props.value == "string") {
+        colors = [props.value, "#fff"];
+      } else {
+        colors = props.value.colors;
+      }
+      return colors;
+    }
+
+    const deleteColor = (index: number) => {
+      state.colors.splice(index, 1);
+    };
+
+    const addColor = () => {
+      state.colors.push(state.colors[0]);
+      setTimeout(() => {
+        const elements = document.querySelectorAll(".jsColorItem");
+        const element = elements[elements.length - 1];
+        // @ts-ignore
+        element.click();
+      }, 0);
+    };
+
+    const changeColor = (value: any, index: number) => {
+      state.colors[index] = value;
+    };
+
+    onMounted(() => {
+      if (typeof props.value == "string") {
+        emit("change", state.gradientColors[0]);
+      }
+    });
+
+    watch(
+      () => props.value,
+      () => (state.colors = getColors())
+    );
+
+    return () => {
+      return (
+        <div>
+          <div class="mb-10px">渐变色</div>
+          <div class="grid gap-16px grid-cols-5 mr-5px">
+            {state.colors.map((d, i) => {
+              return (
+                <Popover
+                  key={i}
+                  content={
+                    <Picker
+                      value={d}
+                      onChange={(value) => changeColor(value, i)}
+                    />
+                  }
+                  placement="bottomRight"
+                  // visible={libVisible}
+                  trigger="click"
+                >
+                  <ColorItem
+                    class="jsColorItem"
+                    color={d}
+                    showDelete={state.colors.length > 2}
+                    onDelete={() => deleteColor(i)}
+                  />
+                </Popover>
+              );
+            })}
+            {state.colors.length <= 9 && (
+              <div
+                class="w-38px h-38px rounded cursor-pointer bg-[#3b3b3b] flex items-center justify-center transition hover:opacity-70"
+                onClick={addColor}
+              >
+                <IconAddLine class="text-22px text-dark-300" />
+              </div>
+            )}
+          </div>
+          <Divider />
+          <div class="mb-10px">风格</div>
+          <ColorList
+            value={props.value}
+            data={state.gradientColors}
+            columns={4}
+            onChange={(data) => emit("change", data)}
+          />
+        </div>
+      );
+    };
+  },
+});
+
+const tabStyles = css`
+  .ant-tabs-nav-list {
+    flex: 1;
+  }
+  .ant-tabs-tab {
+    justify-content: center;
+    flex: 1;
+  }
+  .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
+    color: #fff;
+  }
+`;

+ 143 - 0
src/modules/editor/components/CompUI/formItems/NewColorPicker/Picker.tsx

@@ -0,0 +1,143 @@
+import { IconPicker } from "@/assets/icons";
+import iro from "@jaames/iro";
+import { css } from "@linaria/core";
+import { Input } from "ant-design-vue";
+import Color from "color";
+import { nanoid } from "nanoid";
+import { defineComponent, nextTick, onMounted, reactive, ref } from "vue";
+import { string } from "vue-types";
+
+declare global {
+  interface Window {
+    EyeDropper?: any;
+  }
+}
+
+export default defineComponent({
+  props: {
+    value: string().def("#fff"),
+  },
+  emits: ["change"],
+  setup(props, { emit }) {
+    const containerId = `id_${nanoid(5)}`;
+    const pickr = ref<any>(null);
+
+    const state = reactive({
+      showPicker: false,
+    });
+
+    function initPicker() {
+      if (window.EyeDropper) state.showPicker = true;
+      const element = document.querySelector(`.${containerId}`);
+      if (!element) return;
+      // @ts-ignore
+      pickr.value = new iro.ColorPicker(element, {
+        width: 270,
+        boxHeight: 150,
+        color: props.value,
+        sliderSize: 16,
+        layout: [
+          {
+            component: iro.ui.Box,
+          },
+          {
+            component: iro.ui.Slider,
+            options: {
+              sliderType: "hue",
+              id: "hue-slider",
+              handleSvg: "#handle",
+              handleProps: { x: -3, y: -3 },
+              handleRadius: 5,
+            },
+          },
+          {
+            component: iro.ui.Slider,
+            options: {
+              sliderType: "alpha",
+              handleSvg: "#handle",
+              handleProps: { x: -3, y: -3 },
+              handleRadius: 5,
+            },
+          },
+        ],
+      });
+
+      pickr.value.on("color:change", function (color: any) {
+        if (color.alpha == 1) {
+          emit("change", color.hexString);
+          return;
+        }
+        emit("change", color.hex8String);
+      });
+    }
+
+    onMounted(() => {
+      nextTick(() => {
+        initPicker();
+      });
+    });
+
+    function pickColor() {
+      if (!window.EyeDropper) {
+        return;
+      }
+      const eyeDropper = new window.EyeDropper();
+      eyeDropper
+        .open()
+        .then((result: any) => {
+          emit("change", result);
+        })
+        .catch((e: any) => {
+          console.log("e: ", e);
+        });
+    }
+
+    return () => {
+      const colorObj = Color(props.value);
+      const opacity = (colorObj.alpha() * 100).toFixed(0) + "%";
+
+      return (
+        <div class={styles}>
+          <div class={containerId}></div>
+          <div class="mt-15px flex items-center justify-between">
+            <Input
+              readonly
+              value={colorObj.hex()}
+              class="flex-1 bg-dark-300 h-36px hover:bg-dark-300 text-center"
+              bordered={false}
+            />
+            <Input
+              readonly
+              value={opacity}
+              class="w-60px ml-10px bg-dark-300 h-36px hover:bg-dark-300 text-center"
+              bordered={false}
+            />
+            {state.showPicker && (
+              <IconPicker
+                class="ml-10px text-36px bg-dark-300 rounded-2px transition cursor-pointer !hidden hover:opacity-60"
+                onClick={pickColor}
+              />
+            )}
+          </div>
+          <svg style="display:none;">
+            <circle
+              fill="#fff"
+              id="handle"
+              cx="8"
+              cy="8"
+              r="6"
+              // stroke-width="1"
+              // stroke="#fff"
+            ></circle>
+          </svg>
+        </div>
+      );
+    };
+  },
+});
+
+const styles = css`
+  .IroBox {
+    border-radius: 6px !important;
+  }
+`;

+ 114 - 0
src/modules/editor/components/CompUI/formItems/NewColorPicker/index.tsx

@@ -0,0 +1,114 @@
+import { IconPalette } from "@/assets/icons";
+import { css } from "@linaria/core";
+import { Popover } from "ant-design-vue";
+import { defineComponent, reactive } from "vue";
+import { any, bool } from "vue-types";
+import ColorLib from "./ColorLib";
+import { formatColor, gradientColorType } from "./ColorList";
+import Panel from "./Panel";
+
+export default defineComponent({
+  props: {
+    value: any<string | gradientColorType>(),
+    showGradient: bool().def(true),
+  },
+  emits: ["change"],
+  setup(props, { emit }) {
+    // @ts-ignore
+    const state = reactive({
+      color: props.value || "#ffffff",
+    });
+
+    function changeValue(data: any) {
+      state.color = data;
+      emit("change", data);
+    }
+
+    function storeColors() {
+      const list = localStorage.getItem("latestColors");
+      let colors = list ? JSON.parse(list) : ["#fff"];
+      const index = colors.findIndex(
+        (d: any) => formatColor(props.value || "") == formatColor(d)
+      );
+      if (index !== -1) return;
+      colors.unshift(props.value);
+      colors.splice(10);
+      localStorage.setItem("latestColors", JSON.stringify(colors));
+    }
+
+    return () => {
+      const { color } = state;
+
+      return (
+        <div class="space-x-10px flex">
+          <Popover
+            // visible={true}
+            color="#303030"
+            destroyTooltipOnHide
+            trigger="click"
+            placement="bottomRight"
+            content={
+              <ColorLib
+                showGradient={props.showGradient}
+                value={color}
+                onChange={changeValue}
+              />
+            }
+            onVisibleChange={(v) => {
+              if (!v) storeColors();
+            }}
+          >
+            <div class={colorItem}>
+              <div
+                class="w-30px h-30px cursor-pointer"
+                style={{ background: formatColor(color) }}
+              ></div>
+            </div>
+          </Popover>
+          <Popover
+            // visible={true}
+            color="#303030"
+            destroyTooltipOnHide
+            trigger="click"
+            placement="bottomRight"
+            content={
+              <Panel
+                showGradient={props.showGradient}
+                onChange={changeValue}
+                value={color}
+              />
+            }
+            onVisibleChange={(v) => {
+              if (!v) storeColors();
+            }}
+          >
+            <IconPalette class="rounded bg-[#303030] text-30px text-light-50 cursor-pointer" />
+          </Popover>
+        </div>
+      );
+    };
+  },
+});
+
+const colorItem = css`
+  background-color: #fff;
+  background-image: linear-gradient(
+      -45deg,
+      rgba(57, 76, 96, 0.15) 25%,
+      transparent 25%,
+      transparent 75%,
+      rgba(57, 76, 96, 0.15) 75%
+    ),
+    linear-gradient(
+      -45deg,
+      rgba(57, 76, 96, 0.15) 25%,
+      transparent 25%,
+      transparent 75%,
+      rgba(57, 76, 96, 0.15) 75%
+    );
+
+  background-position: 0 0, 6px 6px;
+  background-size: 12px 12px;
+  border-radius: 2px;
+  overflow: hidden;
+`;

+ 129 - 55
src/modules/editor/components/CompUI/formItems/Size.tsx

@@ -1,92 +1,166 @@
+import { IconHeight, IconWidth } from "@/assets/icons";
 import { CompSizeOpts } from "@/modules/editor/objects/DesignTemp/creates/CompSize";
+import { Layout } from "@/modules/editor/typings";
+import { css } from "@linaria/core";
 import { InputNumber, Select } from "ant-design-vue";
-import { defineComponent } from "vue";
+import { AnyFun } from "queenjs/typing";
+import { defineComponent, reactive } from "vue";
 import { any } from "vue-types";
+import { TipIcons } from "../../TipIcons";
 
 const selectOptions = [
-  { value: "px", label: "像素", options: { step: 1 } },
-  { value: "%", label: "比例", options: { step: 0.1, max: 100, min: 0 } },
+  { value: "px", label: "PX", options: { step: 1 } },
+  { value: "%", label: "%", options: { step: 0.1 } },
 ];
 
 export const Size = defineComponent({
   props: {
-    value: any<[number, number, string?, string?]>().def([0, 0]),
+    value: any<Layout["size"]>().def([0, 0]),
   },
   emits: ["change"],
   setup(props, { emit }) {
     const CompSizeOptsList = [CompSizeOpts.w, CompSizeOpts.h];
+    const state = reactive({
+      relateRatio: 0,
+    });
 
-    function changeVal(unit: any, index: number, v: any) {
+    function changeVal(index: number, v: any) {
       const { value } = props;
+      const unit = value[2]?.unit;
       value[index] = parseFloat(v) || 0;
+
       if (unit === "%") {
         value[index] = CompSizeOptsList[index].getPxSize(
           value[index] as number
         );
       }
+
+      if (state.relateRatio) {
+        const isChangeW = index === 0;
+        if (isChangeW) {
+          value[1] = value[0] / state.relateRatio;
+        } else {
+          value[0] = value[1] * state.relateRatio;
+        }
+      }
+
       emit("change", value);
     }
 
-    function changeUnit(index: number, unit: any) {
+    function changeUnit(unit: any) {
       const { value } = props;
-      value[index + 2] = unit;
+      if (!value[2]?.unit) {
+        value[2] = { unit };
+      } else {
+        value[2].unit = unit;
+      }
       emit("change", value);
     }
 
-    function fmtVal(val: number, unit: string, index: number) {
+    function changeRelate() {
+      state.relateRatio = !state.relateRatio
+        ? props.value[0] / props.value[1]
+        : 0;
+    }
+
+    function fmtVal(index: number) {
+      const unit = props.value[2]?.unit;
+      const val = props.value[index] as number;
       switch (unit) {
-        case "px":
-          return val.toFixed(0);
         case "%":
-          return CompSizeOptsList[index].getVSize(val).toFixed(1);
+          return CompSizeOptsList[index].getVSize(val).toFixed(0);
         default:
-          return "";
+          return val.toFixed(0);
       }
     }
 
-    return () => {
-      const values = props.value.slice(0, 2) as number[];
-      const units = props.value.slice(2) as string[];
-
-      return (
-        <div class="flex space-x-10px">
-          {values.map((value, i) => {
-            const unit = units[i] || selectOptions[0].value;
-            const inputOpts =
-              selectOptions.find((d) => d.value === unit)?.options || {};
-            return (
-              <InputNumber
-                key={i + unit}
-                value={fmtVal(value as number, unit, i)}
-                onChange={changeVal.bind(null, unit, i)}
-                {...inputOpts}
-              >
-                {{
-                  addonBefore() {
-                    return (
-                      <span class="-mx-8px">
-                        {CompSizeOptsList[i].label}
-                        <Select
-                          value={unit}
-                          onChange={changeUnit.bind(null, i)}
-                        >
-                          {selectOptions.map((d) => {
-                            return (
-                              <Select.Option key={d.value}>
-                                {d.value === "%" ? CompSizeOptsList[i].vUnit : d.value}
-                              </Select.Option>
-                            );
-                          })}
-                        </Select>
-                      </span>
-                    );
-                  },
-                }}
-              </InputNumber>
-            );
-          })}
+    return () => (
+      <div class="space-y-10px flex-1">
+        <div class="flex justify-between">
+          <SizeInput
+            label="X"
+            icon={IconWidth}
+            value={fmtVal(0)}
+            onChange={(v) => changeVal(0, v)}
+          />
+          <TipIcons.Relate
+            class={[relateIconCls, state.relateRatio && "active"]}
+            onClick={changeRelate}
+          />
         </div>
-      );
-    };
+        <div class="flex justify-between">
+          <SizeInput
+            label="Y"
+            icon={IconHeight}
+            value={fmtVal(1)}
+            onChange={(v) => changeVal(1, v)}
+          />
+          <Select
+            class={selUnitCls}
+            bordered={false}
+            value={props.value[2]?.unit || selectOptions[0].value}
+            onChange={(v) => changeUnit(v)}
+          >
+            {selectOptions.map((d) => {
+              return <Select.Option key={d.value}>{d.label}</Select.Option>;
+            })}
+          </Select>
+        </div>
+      </div>
+    );
   },
 });
+
+const SizeInput = (props: {
+  label: string;
+  icon: any;
+  value: string;
+  onChange: AnyFun;
+}) => {
+  const { label, icon, ...inputProps } = props;
+  return (
+    <div class={numberInputCls}>
+      <span class="pl-14px pr-1px text-14px">{label}</span>
+      <InputNumber class="flex-1" bordered={false} {...inputProps} />
+      <icon class="tipIcon" />
+    </div>
+  );
+};
+
+const numberInputCls = css`
+  @apply inline-flex items-center flex-1 w-0 mr-12px;
+  background-color: #303030;
+  border-radius: 4px;
+  overflow: hidden;
+
+  .tipIcon {
+    font-size: 24px;
+    padding: 3px 5px;
+    background-color: #373737;
+  }
+`;
+
+const relateIconCls = css`
+  padding: 3px 17px;
+  font-size: 24px;
+  border-radius: 4px;
+  background-color: #303030;
+  cursor: pointer;
+
+  &.active {
+    background-color: rgba(234, 158, 64, 0.2);
+    color: #ea9e40;
+  }
+`;
+
+const selUnitCls = css`
+  width: 58px;
+  height: 30px;
+  border-radius: 4px;
+  background-color: #303030;
+  cursor: pointer;
+
+  .ant-select-selector {
+    padding: 0 8px;
+  }
+`;

+ 4 - 2
src/modules/editor/components/CompUI/formItems/Slider.tsx

@@ -1,7 +1,7 @@
 import { css } from "@linaria/core";
 import { InputNumber, Slider } from "ant-design-vue";
 import { defineComponent, reactive, watchEffect } from "vue";
-import { bool, number } from "vue-types";
+import { bool, func, number } from "vue-types";
 
 export default defineComponent({
   props: {
@@ -11,6 +11,7 @@ export default defineComponent({
     min: number(),
     max: number(),
     step: number(),
+    formatter: func<(...args: any[]) => string>(),
   },
   emits: ["change"],
   setup(props, { emit }) {
@@ -30,7 +31,7 @@ export default defineComponent({
     });
 
     return () => {
-      const { defaultValue, disabled, min, max, step } = props;
+      const { formatter, defaultValue, disabled, min, max, step } = props;
       const attr = {
         defaultValue,
         disabled: disabled,
@@ -54,6 +55,7 @@ export default defineComponent({
           <InputNumber
             class="item_input"
             {...attr}
+            formatter={formatter}
             onPressEnter={() => changeVal()}
             // onBlur={() => {
             //   if (state.value !== props.value) changeVal();

+ 7 - 5
src/modules/editor/components/TipIcons/index.ts

@@ -26,6 +26,7 @@ import {
   IconWidth,
   IconHeight,
   IconDelete,
+  IconRelate,
 } from "@/assets/icons";
 import {
   IconCamera,
@@ -41,9 +42,6 @@ import {
 import { createTipIcon } from "./create";
 import { FontSize } from "./TipIcon";
 
-
-
-
 export const TipIcons = {
   Rename: createTipIcon({
     icons: [IconEdit],
@@ -69,7 +67,7 @@ export const TipIcons = {
     icons: [IconEdit],
     tips: ["编辑"],
   }),
-  
+
   Position: createTipIcon({
     icons: [IconFloatOff, IconFloatOn],
     tips: ["开启浮动", "关闭浮动"],
@@ -143,7 +141,7 @@ export const TipIcons = {
     tips: ["裁剪"],
   }),
   AlignXLeft: createTipIcon({
-    icons: [FontSize(IconAlignXLeft,26)],
+    icons: [FontSize(IconAlignXLeft, 28)],
     tips: ["左对齐"],
   }),
   AlignXCenter: createTipIcon({
@@ -182,4 +180,8 @@ export const TipIcons = {
     icons: [FontSize(IconHeight, 28)],
     tips: ["相同高度"],
   }),
+  Relate: createTipIcon({
+    icons: [IconRelate],
+    tips: ["锁定宽高比"],
+  }),
 };

+ 217 - 0
src/modules/editor/components/Viewport/Slider/SliderLeft/Sources/EditVideoModal.tsx

@@ -0,0 +1,217 @@
+import Empty from "@/components/Empty";
+import { TimeController } from "@/controllers/TimeController";
+import Slider from "@/modules/editor/components/CompUI/formItems/Slider";
+import { useResource } from "@/modules/resource";
+import { Button } from "ant-design-vue";
+import dayjs from "dayjs";
+import { queenApi } from "queenjs";
+import Modal from "queenjs/adapter/vue/components/modal";
+import { defineComponent, reactive, ref } from "vue";
+import { object } from "vue-types";
+
+export default defineComponent({
+  props: {
+    record: object().isRequired,
+  },
+  setup(props) {
+    const resource = useResource();
+    const videoRef = ref();
+    const previewRef = ref();
+
+    const state = reactive({
+      duration: 0,
+      startTime: 0,
+      endTime: 0,
+      previewObj: {} as any,
+    });
+
+    const editVideo = async () => {
+      if (state.endTime - state.startTime <= 0) {
+        queenApi.messageError("截取时长错误");
+        return;
+      }
+      queenApi.showLoading("正在处理……");
+      const data = {
+        start: state.startTime,
+        end: state.endTime,
+        id: props.record._id,
+      };
+      const res = await resource.https.cutVideo(data);
+      const { url, jobId } = res.result;
+
+      const videoStatusCtrl = new TimeController({
+        delayTime: 1000,
+        durationTime: 3000,
+      });
+      videoStatusCtrl
+        .setLoop(async () => {
+          const { result } = await resource.https.queryVideoStatus(jobId);
+          if (result === "SUCCEED") {
+            videoStatusCtrl.stop();
+            queenApi.hideLoading();
+            const souceObj: createSourceType = {
+              file: { url },
+              fileType: "video",
+              from: "cut",
+            };
+            state.previewObj = souceObj;
+          } else if (result === "FAILED") {
+            videoStatusCtrl.stop();
+            queenApi.hideLoading();
+            queenApi.messageError("失败,重新提交!");
+          }
+        })
+        .start();
+    };
+
+    const createNew = async () => {
+      queenApi.showLoading("正在创建……");
+      await resource.https.createResource(state.previewObj);
+      setTimeout(async () => {
+        await resource.controls.custVideoListCtrl.loadPage(1);
+        Modal.clear();
+        queenApi.hideLoading();
+      }, 800);
+    };
+
+    const replaceVideo = async () => {
+      queenApi.showLoading("正在替换……");
+      const res = await resource.https.updateVideoUrl({
+        url: state.previewObj.file.url,
+        id: props.record._id,
+      });
+      setTimeout(async () => {
+        // eslint-disable-next-line vue/no-mutating-props
+        props.record.thumbnail = res.result.thumbUrl;
+        // eslint-disable-next-line vue/no-mutating-props
+        props.record.file.url = state.previewObj.file.url;
+        Modal.clear();
+        queenApi.hideLoading();
+      }, 800);
+    };
+
+    function changeStart(v: number) {
+      videoRef.value.currentTime = v;
+      state.startTime = v;
+      if (state.endTime < state.startTime) {
+        state.endTime = state.startTime + 1;
+      }
+    }
+
+    function changeEnd(v: number) {
+      // videoRef.value.currentTime = v;
+      state.endTime = v;
+    }
+
+    function formateTime(time: number) {
+      // @ts-ignore
+      const h = parseInt(time / 3600);
+      // @ts-ignore
+      const minute = parseInt((time / 60) % 60);
+      const second = Math.ceil(time % 60);
+
+      const hours = h < 10 ? "0" + h : h;
+      const formatSecond = second > 59 ? 59 : second;
+      // @ts-ignore
+      return `${hours > 0 ? `${hours}:` : ""}${
+        minute < 10 ? "0" + minute : minute
+      }:${formatSecond < 10 ? "0" + formatSecond : formatSecond}`;
+    }
+
+    return () => {
+      const { record } = props;
+      const { duration, startTime, endTime, previewObj } = state;
+      const submitDisabled = previewObj.file ? false : true;
+
+      return (
+        <div>
+          <div class="flex items-center justify-center space-x-30px overflow-hidden">
+            <div>
+              <div class="mb-20px">原视频</div>
+              <div class="flex items-center justify-center w-510px h-400px bg-[#1B1B1B] rounded">
+                <video
+                  controls
+                  ref={videoRef}
+                  src={record.file?.url}
+                  class="w-full"
+                  onLoadeddata={() => {
+                    const time = videoRef.value.duration;
+                    state.duration = Math.floor(time);
+                    videoRef.value.volume = videoRef.value.volume / 2;
+                  }}
+                />
+              </div>
+            </div>
+            <div>
+              <div class="mb-20px">剪辑后预览</div>
+              <div class="flex items-center justify-center w-510px h-400px bg-[#1B1B1B] rounded">
+                {previewObj.file?.url ? (
+                  <video
+                    controls
+                    ref={previewRef}
+                    src={previewObj.file?.url}
+                    class="w-full"
+                    onLoadeddata={() => {
+                      previewRef.value.volume = previewRef.value.volume / 2;
+                    }}
+                  />
+                ) : (
+                  <Empty />
+                )}
+              </div>
+            </div>
+          </div>
+          <div class="mt-50px font-bold text-14px">剪辑</div>
+          <div class="flex items-center justify-around h-60px mt-20px rounded bg-[#1B1B1B]">
+            <div class="flex items-center justify-between space-x-20px">
+              <span class="text-12px">启始时间</span>
+              <Slider
+                min={0}
+                max={duration}
+                value={startTime}
+                class="w-300px"
+                onChange={changeStart}
+                formatter={(v) => formateTime(v)}
+              />
+            </div>
+            <div class="flex items-center justify-between space-x-20px">
+              <span class="text-12px">结束时间</span>
+              <Slider
+                min={0}
+                max={duration}
+                value={endTime}
+                class="w-300px"
+                onChange={changeEnd}
+                formatter={(v) => formateTime(v)}
+              />
+            </div>
+            <Button type="primary" class="w-100px" onClick={editVideo}>
+              生成视频
+            </Button>
+          </div>
+          <div class="mt-60px text-center space-x-30px">
+            <Button
+              ghost
+              type="primary"
+              size="large"
+              class="w-300px"
+              disabled={submitDisabled}
+              onClick={replaceVideo}
+            >
+              替换原视频
+            </Button>
+            <Button
+              type="primary"
+              size="large"
+              class="w-300px"
+              disabled={submitDisabled}
+              onClick={createNew}
+            >
+              生成新视频
+            </Button>
+          </div>
+        </div>
+      );
+    };
+  },
+});

+ 13 - 10
src/modules/editor/components/Viewport/Slider/SliderLeft/Sources.tsx → src/modules/editor/components/Viewport/Slider/SliderLeft/Sources/List.tsx

@@ -23,7 +23,9 @@ export const Sources = defineComponent({
       // sysImageListCtrl sysVideoListCtrl custImageListCtrl custVideoListCtrl
       if (props.sourceFrom == "user")
         return resource.controls[
-          props.sourceType === "Image" ? "custImageListCtrl" : "custVideoListCtrl"
+          props.sourceType === "Image"
+            ? "custImageListCtrl"
+            : "custVideoListCtrl"
         ];
       return resource.controls[
         props.sourceType === "Image" ? "sysImageListCtrl" : "sysVideoListCtrl"
@@ -58,22 +60,23 @@ export const Sources = defineComponent({
       const { sourceType } = props;
 
       return (
-        <div class={[ props.sourceFrom == "user" && "flex flex-col !px-0"]}>
-          {
-            props.sourceFrom == "user" &&   <Button
-                class="mt-12px mb-12px"
+        <div>
+          {props.sourceFrom == "user" && (
+            <div class="sticky top-0 pb-12px z-11 bg-[#262626]">
+              <Button
+                class="mt-5px"
                 block
                 type="primary"
                 onClick={async () => {
-                  await resource.actions.createMaterial()
+                  await resource.actions.createMaterial();
                   control.fresh();
                 }}
               >
                 上传素材
               </Button>
-          }
-
-          <div class={[ props.sourceFrom == "user" && "flex-grow overflow-x-hidden overflow-y-auto scrollbar"]}>
+            </div>
+          )}
+          <div>
             <Container
               class="grid grid-cols-2 gap-10px"
               behaviour="copy"
@@ -87,7 +90,7 @@ export const Sources = defineComponent({
               }}
             >
               {dataSource.map((item: any) => (
-                <Draggable>
+                <Draggable key={item._id}>
                   <SourceItem
                     class="draggable-item cursor-pointer"
                     record={item}

+ 19 - 2
src/modules/editor/components/Viewport/Slider/SliderLeft/SourceItem.tsx → src/modules/editor/components/Viewport/Slider/SliderLeft/Sources/SourceItem.tsx

@@ -1,12 +1,14 @@
+import { IconCut } from "@/assets/icons";
 import Thumbnail from "@/components/Thumbnail";
 import { css, cx } from "@linaria/core";
 import { useAuth } from "@queenjs-modules/auth";
 import { IconMore } from "@queenjs/icons";
 import { Dropdown, Menu, Tag } from "ant-design-vue";
 import dayjs from "dayjs";
-import { defineUI } from "queenjs";
+import { defineUI, queenApi } from "queenjs";
 import { defineComponent, reactive } from "vue";
 import { any, bool, object } from "vue-types";
+import EditVideoModal from "./EditVideoModal";
 
 const VideoItem = defineComponent({
   props: {
@@ -18,6 +20,14 @@ const VideoItem = defineComponent({
       play: false,
     });
 
+    function showModal(record: any) {
+      queenApi.dialog(<EditVideoModal record={record} />, {
+        title: "编辑视频",
+        width: "1100px",
+        centered: true,
+      });
+    }
+
     return () => {
       const { record } = props;
       const { play } = state;
@@ -25,11 +35,18 @@ const VideoItem = defineComponent({
       return (
         <div
           style={{ aspectRatio: 1.5 }}
-          class="overflow-hidden"
+          class="relative overflow-hidden"
           onMouseenter={() => (state.play = true)}
           onMouseleave={() => (state.play = false)}
           onClick={() => emit("click", record.file.url)}
         >
+          <IconCut
+            class="absolute right-5px top-5px z-2 rounded-2px text-20px bg-light-50 transition hover:(bg-opacity-50)"
+            onClick={(e: any) => {
+              e.stopPropagation();
+              showModal(record);
+            }}
+          />
           {!play ? (
             <Thumbnail
               class="w-1/1 h-1/1"

+ 1 - 1
src/modules/editor/components/Viewport/Slider/SliderLeft/index.tsx

@@ -24,7 +24,7 @@ import Comp3d from "./Comp3d";
 import CompsUser from "./CompsUser";
 import CustomComps from "./CustomComps";
 import Shapes from "./Shapes";
-import { Sources } from "./Sources";
+import { Sources } from "./Sources/List";
 import Templates from "./Templates";
 import Text from "./Text";
 

+ 2 - 2
src/modules/editor/components/Viewport/Toolbar/index.tsx

@@ -50,13 +50,13 @@ export default defineUI({
               </div>
             </div>
           }
-          <div class="absolute right-25px top-80px space-x-10px z-1001">
+          <div class="absolute right-25px top-80px space-x-10px z-1000">
             <TipIcons.Screenshot
               class={bottomBtnStyles}
               onClick={() => actions.updateThumbnailByScreenshot(true)}
             />
           </div>
-          <div class="absolute bottom-20px right-20px z-1001">
+          <div class="absolute bottom-20px right-20px z-1000">
             <TipIcons.QueenService
               class={bottomBtnStyles}
               onClick={() => {

+ 0 - 2
src/modules/editor/components/index.ts

@@ -1,9 +1,7 @@
 import Preview from "./Preview";
-import Viewport from "./Viewport";
 import CompSave from "./CompSave";
 
 export default {
-  Viewport,
   Preview,
   CompSave,
 };

+ 6 - 3
src/modules/editor/objects/DesignTemp/creates/createCompStyle.ts

@@ -1,3 +1,4 @@
+import { formatColor } from "@/modules/editor/components/CompUI/formItems/NewColorPicker/ColorList";
 import { Matrix } from "@/modules/editor/controllers/SelectCtrl/matrix";
 import { EditorModule } from "@/modules/editor/module";
 import { Layout } from "@/modules/editor/typings";
@@ -78,12 +79,14 @@ export function createCompStyle(
   }
 
   if (layout.size) {
-    const [w, h, wUnit, hUnit] = layout.size;
+    const [w, h, sizeOpts] = layout.size;
     if (w) {
       style.width = designToNaturalSize(w);
     }
     if (h) {
-      style.height = designToNaturalSize(h, { adaptiveH: hUnit === "%" });
+      style.height = designToNaturalSize(h, {
+        adaptiveH: sizeOpts?.unit === "%",
+      });
     }
   }
 
@@ -105,7 +108,7 @@ export function createCompStyle(
 
   if (layout.background) {
     if (layout.background.color) {
-      style.backgroundColor = layout.background.color;
+      style.background = formatColor(layout.background.color);
     }
   }
 

+ 3 - 2
src/modules/editor/typings.ts

@@ -2,12 +2,13 @@ import { CompUI } from "./components/CompUI";
 
 export type ICompKeys = keyof typeof CompUI;
 
-export type EditorMode = "editPage" | "editComp" | "preview" | "display"
+export type EditorMode = "editPage" | "editComp" | "preview" | "display";
 
 export type Layout = {
   position?: "absolute";
   visible?: boolean;
-  size: [number, number, string?, string?]; // width height wUnit hUnit
+  size: [number, number, { unit?: "px" | "%" }?]; // width height wUnit hUnit
+  direction?: "top" | "bottom"; // 默认top
   mask?: string;
   alignSelf?: string;
   transform?: {

+ 7 - 5
src/modules/resource/actions/material.ts

@@ -1,4 +1,6 @@
+import { TimeController } from "@/controllers/TimeController";
 import { queenApi } from "queenjs";
+import Modal from "queenjs/adapter/vue/components/modal";
 import { ResourceModule } from "..";
 
 export const materialActions = ResourceModule.action({
@@ -30,12 +32,12 @@ export const materialActions = ResourceModule.action({
     // this.controls.materialListCtrl.fresh();
   },
 
-  publishMaterial(record: any, publish:boolean ) {
-    return this.https.publishResource({_id: record._id, published: publish});
+  publishMaterial(record: any, publish: boolean) {
+    return this.https.publishResource({ _id: record._id, published: publish });
   },
-  
-  publishFrame(record: any, publish:boolean ) {
-    return this.https.publishFrame({_id: record._id, published: publish});
+
+  publishFrame(record: any, publish: boolean) {
+    return this.https.publishFrame({ _id: record._id, published: publish });
   },
 
   downloadMaterial(record) {

+ 22 - 8
src/modules/resource/http.ts

@@ -5,20 +5,31 @@ export const http = ResourceModule.http({
   deleteResource(id: string) {
     return this.request(`/source/delete/${id}`, { method: "POST" });
   },
-  publishResource(data:any) {
-    return this.request(`sys/source/publish`, { method: "POST" , data});
+  publishResource(data: any) {
+    return this.request(`sys/source/publish`, { method: "POST", data });
   },
-  publishFrame(data:any) {
-    return this.request(`sys/frame/publish`, { method: "POST" , data});
+  publishFrame(data: any) {
+    return this.request(`sys/frame/publish`, { method: "POST", data });
   },
 
-  createResource(data: any) {
+  createResource(data: createSourceType) {
     return this.request("/source/create", { method: "POST", data });
   },
+
   updateResouce(data: any) {
     return this.request("/source/update", { method: "POST", data });
   },
 
+  cutVideo(data: { id: string; start: number; end: number }) {
+    return this.request("/video/cut", { method: "POST", data });
+  },
+  queryVideoStatus(id: string) {
+    return this.request(`/video/cut/${id}`, { method: "GET" });
+  },
+  updateVideoUrl(data: { id: string; url: string }) {
+    return this.request("source/video/update", { method: "POST", data });
+  },
+
   //   templates
   queryTplsDetail(id: string) {
     return this.request(`/tpls/detail/${id}`, { method: "GET" });
@@ -41,10 +52,13 @@ export const http = ResourceModule.http({
     return this.request(`/h5/delete/${id}`, { method: "POST" });
   },
 
-  publishPromotion(id: string, publish: boolean) { 
-    return this.request("/sys/h5/publish", { method: "POST",  data:{_id: id, published: publish}});
+  publishPromotion(id: string, publish: boolean) {
+    return this.request("/sys/h5/publish", {
+      method: "POST",
+      data: { _id: id, published: publish },
+    });
   },
-  
+
   //   comp
   createComp(data: any) {
     return this.request("/frame/create", { method: "POST", data });

+ 2 - 1
src/pages/editor/EditPage/index.tsx

@@ -1,4 +1,5 @@
 import { initEditor } from "@/modules/editor";
+import Viewport from "@/modules/editor/components/Viewport";
 import { EditorMode } from "@/modules/editor/typings";
 import { useResource } from "@/modules/resource";
 import { SelectOneImage } from "@/pages/website/Material2/modal";
@@ -41,5 +42,5 @@ export default defineComponent(() => {
     return resource.treeController.selectOnePackScene();
   };
 
-  return () => <editor.components.Viewport class="!h-100vh" />;
+  return () => <Viewport class="!h-100vh" />;
 });

+ 7 - 0
src/typings/pro.d.ts

@@ -31,3 +31,10 @@ declare type compType = {
   updateTime?: string;
   _id: string;
 };
+
+declare type createSourceType = {
+  file: { url: any; size?: number };
+  fileType: "image" | "video" | "svg";
+  from: string;
+  isSvg?: boolean;
+};

+ 23 - 0
yarn.lock

@@ -1551,6 +1551,19 @@
   resolved "http://124.70.149.18:4873/@humanwhocodes%2fobject-schema/-/object-schema-1.2.1.tgz"
   integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
 
+"@irojs/iro-core@^1.2.1":
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/@irojs/iro-core/-/iro-core-1.2.1.tgz#ab45cf0899717fae785ef9661b82bb6f78cd3748"
+  integrity sha512-p2OvsBSSmidsDsTSkID6jEyXDF7lcyxPrkh3qBzasBZFpjkYd6kZ3yMWai3MlAaQ3F7li/Et7rSJVV09Fpei+A==
+
+"@jaames/iro@^5.5.2":
+  version "5.5.2"
+  resolved "https://registry.yarnpkg.com/@jaames/iro/-/iro-5.5.2.tgz#15301c19080b985aede06c5f07867aa20d06e679"
+  integrity sha512-Fbi5U4Vdkw6UsF+R3oMlPONqkvUDMkwzh+mX718gQsDFt3+1r1jvGsrfCbedmXAAy0WsjDHOrefK0BkDk99TQg==
+  dependencies:
+    "@irojs/iro-core" "^1.2.1"
+    preact "^10.0.0"
+
 "@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2":
   version "0.3.3"
   resolved "http://124.70.149.18:4873/@jridgewell%2fgen-mapping/-/gen-mapping-0.3.3.tgz"
@@ -7198,6 +7211,11 @@ postcss@^8.1.10, postcss@^8.2.15, postcss@^8.2.6, postcss@^8.3.5, postcss@^8.4.1
     picocolors "^1.0.0"
     source-map-js "^1.0.2"
 
+preact@^10.0.0:
+  version "10.16.0"
+  resolved "https://registry.yarnpkg.com/preact/-/preact-10.16.0.tgz#68a06d70b191b8a313ea722d61e09c6b2a79a37e"
+  integrity sha512-XTSj3dJ4roKIC93pald6rWuB2qQJO9gO2iLLyTe87MrjQN+HklueLsmskbywEWqCHlclgz3/M4YLL2iBr9UmMA==
+
 prelude-ls@^1.2.1:
   version "1.2.1"
   resolved "http://124.70.149.18:4873/prelude-ls/-/prelude-ls-1.2.1.tgz"
@@ -8664,6 +8682,11 @@ vue-cli-plugin-windicss@~1.1.6:
     defu "^6.0.0"
     windicss-webpack-plugin "^1.7.4"
 
+vue-color-kit@^1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/vue-color-kit/-/vue-color-kit-1.0.6.tgz#1f8b4780e7b1d3a079f852f47128c1e6b5eb05fc"
+  integrity sha512-56oSUp8hGIZ3E4ayZLqTDAb2C7VzaWAC1EVUsZEvPCD2wiiiFd2L2BiEGN1ingfz538sYWKNXxMZQGslqbMiBg==
+
 vue-demi@*, vue-demi@>=0.14.0:
   version "0.14.5"
   resolved "http://124.70.149.18:4873/vue-demi/-/vue-demi-0.14.5.tgz"