Jelajahi Sumber

add: 右侧面板、注释工具栏 新增颜色选择器

wzl 3 bulan lalu
induk
melakukan
5a81a7e64c

+ 71 - 0
packages/core/src/ui_utils.js

@@ -1620,11 +1620,14 @@ function getColorFormat(colorString) {
     return "hex";
   } else if (colorString.startsWith("rgb(")) {
     return "rgb";
+  } else if (colorString.startsWith("hsl")) {
+    return "hsl";
   } else {
     return "unknown";
   }
 }
 
+// 返回的RGB值范围:0-1
 function convertColorToCppFormat (value) {
   const format = getColorFormat(value)
   if (format === 'hex') {
@@ -1644,6 +1647,46 @@ function convertColorToCppFormat (value) {
       G,
       B
     }
+  } else if (format === 'hsl') {
+    const match = value.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/)
+    if (!match) return { error: 'Invalid HSL format' }
+    const H = parseInt(match[1])
+    const S = parseInt(match[2]) / 100
+    const L = parseInt(match[3]) / 100
+    const { R, G, B } = hslToRgb(H, S, L)
+    return {
+      R: roundToDecimalPlaces(R / 255),
+      G: roundToDecimalPlaces(G / 255),
+      B: roundToDecimalPlaces(B / 255)
+    }
+  } else {
+    console.error({
+      message: 'Invalid color value'
+    })
+  }
+}
+
+// 返回的RGB值范围:0-255
+function convertColorToRGB (value) {
+  const format = getColorFormat(value)
+
+  if (format === 'hex') {
+    return hexToRgb(value)
+  } else if (format === 'rgb') {
+    const match = value.match(/(\d+),\s*(\d+),\s*(\d+)/)
+    if (!match) return { error: 'Invalid RGB format' }
+    return {
+      R: parseInt(match[1]),
+      G: parseInt(match[2]),
+      B: parseInt(match[3])
+    }
+  } else if (format === 'hsl') {
+    const match = value.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/)
+    if (!match) return { error: 'Invalid HSL format' }
+    const H = parseInt(match[1])
+    const S = parseInt(match[2]) / 100
+    const L = parseInt(match[3]) / 100
+    return hslToRgb(H, S, L)
   } else {
     console.error({
       message: 'Invalid color value'
@@ -1803,6 +1846,33 @@ export function hexToRgb(hexColor) {
   }
 }
 
+const hslToRgb = (h, s, l) => {
+  let R, G, B;
+
+  h = h / 360; // 将 H 值从 0-360 转换到 0-1
+
+  if (s === 0) {
+    R = G = B = Math.round(l * 255); // 当饱和度为0时,直接返回灰度值
+  } else {
+    const hue2rgb = function hue2rgb(p, q, t) {
+      if (t < 0) t += 1;
+      if (t > 1) t -= 1;
+      if (t < 1 / 6) return p + (q - p) * 6 * t;
+      if (t < 1 / 2) return q;
+      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+      return p;
+    }
+
+    const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+    const p = 2 * l - q;
+    R = Math.round(hue2rgb(p, q, h + 1 / 3) * 255);
+    G = Math.round(hue2rgb(p, q, h) * 255);
+    B = Math.round(hue2rgb(p, q, h - 1 / 3) * 255);
+  }
+
+  return { R, G, B };
+}
+
 const setCss = (ele, cssText) => {
   if (!ele) return
   if (cssText) {
@@ -1875,4 +1945,5 @@ export {
   toDateObject,
   setCss,
   isMobileDevice,
+  convertColorToRGB
 };

+ 5 - 3
packages/core/src/worker/compdfkit_worker.js

@@ -1,7 +1,7 @@
-importScripts("./ComPDFkit.js")
+importScripts("./ComPDFKit.js")
 // importScripts("./pdfium.js")
 
-import { parseAdobePDFTimestamp, convertToPDFTimestamp, roundToDecimalPlaces, convertColorToCppFormat, convertCppRGBToHex } from '../ui_utils';
+import { parseAdobePDFTimestamp, convertToPDFTimestamp, roundToDecimalPlaces, convertColorToCppFormat, convertCppRGBToHex, convertColorToRGB } from '../ui_utils';
 import { AnnotationType, WidgetType, LineTypeString, StampType, StampTypeString, TextStampShapeString, ActionTypeString, WidgetTypeString, AnnotationFlags, BorderStyleInt, BorderStyleString, ALIGN, ALIGNMAP, AnnotationState, AnnotationStateString } from '../../constants'
 
 let ComPDFKitJS = {
@@ -1242,7 +1242,9 @@ class CPDFWorker {
     })
 
     messageHandler.on('SetCharsFontColor', (data) => {
-      const { editAreaPtr, start, end, color } = data
+      const { editAreaPtr, start, end, color: rawColor } = data
+      const color = convertColorToRGB(rawColor)
+
       Module._SetCharsFontColor(
         editAreaPtr,
         start.SectionIndex, start.LineIndex, start.RunIndex, start.CharIndex,

+ 45 - 4
packages/webview/src/components/Annotate/Annotate.vue

@@ -55,15 +55,29 @@
       <span class="cell-outer" :class="{ active: activeToolColor === '#64BC38' }" @click="setActiveToolColor('#64BC38')">
         <span class="cell green"></span></span>
     </span>
+    <div class="color-picker">
+      <div class="preview" :style="{ backgroundColor: colorPickerValue }"></div>
+      <n-color-picker
+        @complete="onColorPickerComplete"
+        @update:value="onColorPickerUpdate"
+        :value="colorPickerValue"
+        :show-alpha="false"
+        class="annot-color"
+      >
+        <template #label><Colorful /></template>
+      </n-color-picker>
+    </div>
   </div>
 </template>
 
 <script setup>
-  import { computed, onUnmounted } from 'vue'
+  import { computed, onUnmounted, ref } from 'vue'
   import { useViewerStore } from '@/stores/modules/viewer'
   import { useDocumentStore } from '@/stores/modules/document'
   import core from '@/core'
   const { switchTool, switchAnnotationEditorMode } = core
+  import { NColorPicker } from 'naive-ui'
+  import { areColorsSimilar } from '@/helpers/utils'
 
   defineProps(['items'])
 
@@ -82,13 +96,12 @@
     const shapes = ['square', 'circle', 'arrow', 'line']
     return shapes.includes(activeTool.value)
   })
-
   const markupTool = computed(() => useDocument.getMarkupToolState)
-
   const shapeTool = computed(() => useDocument.getShapeToolState)
-
   const activeToolColor = computed(() => useDocument.getActiveToolColor)
 
+  const colorPickerValue = ref('rgb(0, 0, 0)')
+
   const changeActiveTool = (tool) => {
     useDocument.setToolState(tool)
     
@@ -129,6 +142,16 @@
   }
   core.addEvent('imageChange', unselectImage)
 
+  // 颜色选择器选中颜色事件
+  const onColorPickerComplete = (value) => {
+    if (areColorsSimilar(value, activeToolColor.value)) return
+    setActiveToolColor(value)
+  }
+  // 颜色选择器颜色改变事件
+  const onColorPickerUpdate = (value) => {
+    colorPickerValue.value = value
+  }
+
   onUnmounted(() => {
     core.removeEvent('imageChange', unselectImage)
   })
@@ -188,6 +211,11 @@
       }
     }
   }
+
+  .annot-color {
+    width: 14px;
+    height: 14px;
+  }
 }
 @media screen and (max-width: 768px) {
   .pc {
@@ -196,3 +224,16 @@
 }
 </style>
 
+<style lang="scss" scoped>
+.color-picker {
+  margin-left: 3px;
+  padding: 0 3px;
+
+  .preview {
+    margin-right: 3px;
+    width: 14px;
+    height: 14px;
+  }
+}
+</style>
+

+ 60 - 116
packages/webview/src/components/ContentEditorPanel/ContentEditorPanel.vue

@@ -8,33 +8,31 @@
         <div class="color-title">{{ $t('editorPanel.fontColor') }}</div>
         <div class="colors-container">
           <span class="cell-container">
-            <span class="cell-outer" :class="{ active: property.color === 'rgb(255, 0, 0)' }" @click="setActiveToolColor({ R: 255, G: 0, B: 0})">
+            <span class="cell-outer" :class="{ active: areColorsSimilar(property.color, 'rgb(255, 0, 0)') }" @click="setActiveToolColor('rgb(255, 0, 0)')">
               <span class="cell red"></span>
             </span>
           </span>
           <span class="cell-container">
-            <span class="cell-outer" :class="{ active: property.color === 'rgb(255, 236, 102)' }" @click="setActiveToolColor({ R: 255, G: 236, B: 102})">
+            <span class="cell-outer" :class="{ active: areColorsSimilar(property.color, 'rgb(255, 236, 102)') }" @click="setActiveToolColor('rgb(255, 236, 102)')">
               <span class="cell yellow"></span>
             </span>
           </span>
           <span class="cell-container">
-            <span class="cell-outer" :class="{ active: property.color === 'rgb(45, 119, 250)' }" @click="setActiveToolColor({ R: 45, G: 119, B: 250})">
+            <span class="cell-outer" :class="{ active: areColorsSimilar(property.color, 'rgb(45, 119, 250)') }" @click="setActiveToolColor('rgb(45, 119, 250)')">
               <span class="cell blue"></span>
             </span>
           </span>
           <div class="color-picker">
+            <div class="preview" :style="{ backgroundColor: colorPickerValue }"></div>
             <n-color-picker
               to="#editorPanel"
               @complete="onColorPickerComplete"
-              @update:show="onColorPickerShow"
-              @update:value="onColorPickerValue"
+              @update:value="onColorPickerUpdate"
               :value="colorPickerValue"
               :show-alpha="false"
-              :class="{ active: isColorPickerShow || property.color === colorPickerValue }"
             >
-              <template #label></template>
+              <template #label><img src="../Icon/colorful.svg" alt="colorPicker"></template>
             </n-color-picker>
-            <Colorful />
           </div>
         </div>
       </div>
@@ -147,6 +145,7 @@
   import { useViewerStore } from '@/stores/modules/viewer'
   import { NSlider, NSelect, NColorPicker } from 'naive-ui'
   import core from '@/core'
+  import { areColorsSimilar } from '@/helpers/utils'
 
   const useViewer = useViewerStore()
   const instance = getCurrentInstance().appContext.app.config.globalProperties
@@ -224,7 +223,6 @@
   const rightPanelButtonDisabled = computed(() => useViewer.getRightPanelButtonDisabled)
 
   const imageUrl = ref('')
-  const isColorPickerShow = ref(false)
   const colorPickerValue = ref('rgb(0, 0, 0)')
 
   // 打开右侧属性面板时,关闭视图面板
@@ -258,7 +256,7 @@
   })
 
   const setActiveToolColor = (color) => {
-    property.color = `rgb(${color.R}, ${color.G}, ${color.B})`
+    property.color = color
     core.setContentEditorProperty(type.value, { color })
   }
 
@@ -308,88 +306,14 @@
 
   // 颜色选择器选中颜色事件
   const onColorPickerComplete = (value) => {
-    if (value === property.color) {
-      colorPickerValue.value = value
-      return
-    }
-    
-    const color = convertColor(value)
-    if (color) {
-      setActiveToolColor(color)
-      colorPickerValue.value = `rgb(${color.R}, ${color.G}, ${color.B})`
-    }
-  }
-
-  // 颜色选择器打开或关闭事件
-  const onColorPickerShow = (value) => {
-    isColorPickerShow.value = value
+    if (areColorsSimilar(value, property.color)) return
+    setActiveToolColor(value)
   }
 
   // 颜色选择器颜色改变事件
-  const onColorPickerValue = (value) => {
+  const onColorPickerUpdate = (value) => {
     colorPickerValue.value = value
   }
-
-  const hslToRgb = (h, s, l) => {
-    let R, G, B;
-
-    if (s === 0) {
-      R = G = B = l; // achromatic
-    } else {
-      const hue2rgb = function hue2rgb(p, q, t) {
-        if (t < 0) t += 1;
-        if (t > 1) t -= 1;
-        if (t < 1 / 6) return p + (q - p) * 6 * t;
-        if (t < 1 / 2) return q;
-        if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
-        return p;
-      }
-
-      const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
-      const p = 2 * l - q;
-      R = Math.round(hue2rgb(p, q, h + 1 / 3) * 255);
-      G = Math.round(hue2rgb(p, q, h) * 255);
-      B = Math.round(hue2rgb(p, q, h - 1 / 3) * 255);
-    }
-
-    return {R, G, B};
-  }
-
-  // 转换颜色 hsl / hex -> rgb
-  const convertColor = (colorStr) => {
-    if (colorStr.startsWith('#')) {
-      const hexValues = colorStr.slice(1).match(/.{2}/g);
-      if (hexValues) {
-        return {
-          R: parseInt(hexValues[0], 16),
-          G: parseInt(hexValues[1], 16),
-          B: parseInt(hexValues[2], 16)
-        };
-      }
-    }
-
-    if (colorStr.startsWith('rgb')) {
-      const matches = colorStr.match(/\d+/g);
-      if (matches && matches.length === 3) {
-        return {
-          R: matches[0],
-          G: matches[1],
-          B: matches[2]
-        };
-      }
-    }
-
-    if (colorStr.startsWith('hsl')) {
-      const matches = colorStr.match(/(\d+),\s*(\d+%),\s*(\d+%)/);
-      if (matches) {
-        const [h, s, l] = [parseInt(matches[1]), parseInt(matches[2]) / 100, parseInt(matches[3]) / 100];
-        const {R, G, B} = hslToRgb(h / 360, s, l);
-        return {R, G, B};
-      }
-    }
-
-    return null;
-  }
 </script>
 
 <style lang="scss" scoped>
@@ -400,7 +324,6 @@
     display: flex;
     flex-direction: column;
     height: calc(100% - 88px);
-    overflow: hidden;
     transition: transform .3s ease-in-out;
     background-color: var(--c-side-bg);
     border-left: 1px solid var(--c-side-header-border);
@@ -469,18 +392,6 @@
           .cell-container + .cell-container {
             margin-left: 8px;
           }
-
-          .color-picker {
-            display: flex;
-            align-items: center;
-            margin-left: 8px;
-            border: 1px solid #0000001F;
-            border-radius: 20px;
-
-            svg {
-              margin: 3px;
-            }
-          }
         }
       }
       
@@ -741,10 +652,6 @@
   @media screen and (min-width: 641px) {
     .editor-panel {
       width: 260px;
-
-      ::v-deep(.v-binder-follower-content) {
-        transform: translate(10px, 122px) !important;
-      }
     }
   }
 </style>
@@ -773,31 +680,68 @@
     }
   }
 
+  .color-picker {
+    display: flex;
+    align-items: center;
+    margin-left: 8px;
+    padding: 0 3px;
+    border: 1px solid #0000001F;
+    border-radius: 20px;
+
+    .preview {
+      margin-right: 8px;
+      width: 24px;
+      height: 24px;
+      background-color: #000000;
+      border-radius: 50%;
+    }
+  }
+
   .n-color-picker {
-    margin-left: 1px;
-    width: 28px;
-    height: 28px;
-    cursor: auto;
+    width: 24px;
+    height: 24px;
 
     .n-color-picker-trigger {
       border-radius: 50%;
-      cursor: pointer;
       border: none;
 
       .n-color-picker-trigger__fill {
         border-radius: 50%;
         overflow: hidden;
-        left: 3px;
-        right: 3px;
-        top: 3px;
-        bottom: 3px;
+        left: 0;
+        right: 0;
+        top: 0;
+        bottom: 0;
+
+        div {
+          display: none;
+        }
+
+        .n-color-picker-trigger__value {
+          display: flex;
+          align-items: center;
+          position: unset;
+        }
       }
     }
+  }
 
-    &.active {
-      .n-color-picker-trigger {
-        border: 1px solid #1460F3;
+  .n-input {
+    
+    &:not(.n-input--disabled) {
+
+      &:hover .n-input__state-border {
+        border-color: var(--c-blue-1);
+      }
+
+      &.n-input--focus .n-input__state-border {
+        border-color: var(--c-blue-1);
+        box-shadow: 0 0 0 2px var(--c-right-side-list-item-bg);
       }
     }
+
+    .n-input__input-el {
+      caret-color: var(--c-blue-1);
+    }
   }
 </style>

+ 2 - 0
packages/webview/src/components/DocumentContainer/DocumentContainer.vue

@@ -512,6 +512,8 @@ window.instances.UI.loadDocument = async (file, {
   margin: 20px auto;
   background-clip: content-box;
   background-color: rgba(255, 255, 255, 1);
+  user-select: none;
+  -webkit-user-select: none;
 
   >.freetext {
     position: absolute;

File diff ditekan karena terlalu besar
+ 14 - 0
packages/webview/src/components/Icon/colorful.svg


+ 53 - 13
packages/webview/src/components/RightPanel/RightPanel.vue

@@ -43,21 +43,33 @@
           <h2 class="wider">{{ $t('rightPanel.backgroundColor') }}</h2>
           <div class="colors-container">
             <span class="cell-container">
-              <span class="cell-outer" :class="{ active: property.backgroundColor === '#FBBDBF' }" @click="setActiveToolColor('backgroundColor', '#FBBDBF')">
+              <span class="cell-outer" :class="{ active: areColorsSimilar(property.backgroundColor, 'rgb(251, 189, 191)') }" @click="setActiveToolColor('backgroundColor', 'rgb(251, 189, 191)')">
                 <span class="cell light-red"></span></span>
             </span>
             <span class="cell-container">
-              <span class="cell-outer" :class="{ active: property.backgroundColor === '#FFFFFF' }" @click="setActiveToolColor('backgroundColor', '#FFFFFF')">
+              <span class="cell-outer" :class="{ active: areColorsSimilar(property.backgroundColor, 'rgb(255, 255, 255)') }" @click="setActiveToolColor('backgroundColor', 'rgb(255, 255, 255)')">
                 <span class="cell white"></span></span>
             </span>
             <span class="cell-container">
-              <span class="cell-outer" :class="{ active: property.backgroundColor === '#FDF4B2' || property.backgroundColor === '#FDF4B1' }" @click="setActiveToolColor('backgroundColor', '#FDF4B2')">
+              <span class="cell-outer" :class="{ active: areColorsSimilar(property.backgroundColor, 'rgb(253, 244, 178)') || areColorsSimilar(property.backgroundColor, 'rgb(253, 244, 177)') }" @click="setActiveToolColor('backgroundColor', 'rgb(253, 244, 178)')">
                 <span class="cell light-yellow"></span></span>
             </span>
             <span class="cell-container">
-              <span class="cell-outer" :class="{ active: property.backgroundColor === '#DDE9FF' }" @click="setActiveToolColor('backgroundColor', '#DDE9FF')">
+              <span class="cell-outer" :class="{ active: areColorsSimilar(property.backgroundColor, 'rgb(221, 233, 255)') }" @click="setActiveToolColor('backgroundColor', 'rgb(221, 233, 255)')">
                 <span class="cell light-blue"></span></span>
             </span>
+            <div class="color-picker">
+              <div class="preview" :style="{ backgroundColor: backgroundColorPicker }"></div>
+              <n-color-picker
+                to=".right-panel"
+                @complete="(newColor) => onColorPickerComplete(newColor, 'backgroundColor')"
+                @update:value="(newColor) => onColorPickerUpdate(newColor, 'backgroundColor')"
+                :value="backgroundColorPicker"
+                :show-alpha="false"
+              >
+                <template #label><Colorful /></template>
+              </n-color-picker>
+            </div>
           </div>
         </div>
         <!-- 字体 -->
@@ -66,17 +78,29 @@
             <h2 class="wider">{{ $t('font') }}</h2>
             <div class="colors-container">
               <span class="cell-container">
-                <span class="cell-outer" :class="{ active: property.color === '#FF0000' }" @click="setActiveToolColor('color', '#FF0000')">
+                <span class="cell-outer" :class="{ active: areColorsSimilar(property.color, 'rgb(255, 0, 0)') }" @click="setActiveToolColor('color', 'rgb(255, 0, 0)')">
                   <span class="cell red"></span></span>
               </span>
               <span class="cell-container">
-                <span class="cell-outer" :class="{ active: property.color === '#000000' }" @click="setActiveToolColor('color', '#000000')">
+                <span class="cell-outer" :class="{ active: areColorsSimilar(property.color, 'rgb(0, 0, 0)') }" @click="setActiveToolColor('color', 'rgb(0, 0, 0)')">
                   <span class="cell black"></span></span>
               </span>
               <span class="cell-container">
-                <span class="cell-outer" :class="{ active: property.color === '#2D77FA' || property.color === '#2D77F9' }" @click="setActiveToolColor('color', '#2D77FA')">
+                <span class="cell-outer" :class="{ active: areColorsSimilar(property.color, 'rgb(45, 119, 250)') || areColorsSimilar(property.color, 'rgb(45, 119, 249)') }" @click="setActiveToolColor('color', 'rgb(45, 119, 250)')">
                   <span class="cell blue"></span></span>
               </span>
+              <div class="color-picker">
+                <div class="preview" :style="{ backgroundColor: fontColorPicker }"></div>
+                <n-color-picker
+                  to=".right-panel"
+                  @complete="(newColor) => onColorPickerComplete(newColor, 'color')"
+                  @update:value="(newColor) => onColorPickerUpdate(newColor, 'color')"
+                  :value="fontColorPicker"
+                  :show-alpha="false"
+                >
+                  <template #label><Colorful /></template>
+                </n-color-picker>
+              </div>
             </div>
           </div>
           <div class="content-block">
@@ -213,7 +237,9 @@
   import { computed, reactive, watch, ref, getCurrentInstance } from 'vue'
   import { useViewerStore } from '@/stores/modules/viewer'
   import { useDocumentStore } from '@/stores/modules/document'
+  import { NColorPicker } from 'naive-ui'
   import core from '@/core'
+  import { areColorsSimilar } from '@/helpers/utils'
 
   const useViewer = useViewerStore()
   const useDocument = useDocumentStore()
@@ -248,13 +274,15 @@
   let property = reactive(useDocument.propertyPanel)
   let oldDestPage = property.destPage
   let oldUrl = property.url
-  let fontFamily = ref('Helvetica')
+  const fontFamily = ref('Helvetica')
   const isOneRadioBtn = ref(false)
   const selectedElement = ref(null)
-  let editItem = ref(null)
-  let selectedItemIndex = ref(null)
-  let items = ref([])
-  let isOneCheckbox = ref(true)
+  const editItem = ref(null)
+  const selectedItemIndex = ref(null)
+  const items = ref([])
+  const isOneCheckbox = ref(true)
+  const backgroundColorPicker = ref('rgb(0, 0, 0)')
+  const fontColorPicker = ref('rgb(0, 0, 0)')
 
   const handleProperty = (key) => {
     if (key === 'fieldName') {
@@ -371,6 +399,7 @@
       if (selectedElement.value === 'listbox' || selectedElement.value === 'combobox' || selectedElement.value === 'pushbutton') {
         useViewer.setActiveElementTab('rightPanelTab', 'PREFERENCE')
       }
+      selectedElement.value !== 'checkbox' && (isOneCheckbox.value = true)
     }
 
     for (const item in props) {
@@ -474,6 +503,18 @@
       }
     }
   }
+
+  // 颜色选择器颜色改变事件
+  const onColorPickerUpdate = (newColor, key) => {
+    key === 'backgroundColor' && (backgroundColorPicker.value = newColor)
+    key === 'color' && (fontColorPicker.value = newColor)
+  }
+
+  // 颜色选择器选中颜色事件
+  const onColorPickerComplete = (newColor, key) => {
+    if (areColorsSimilar(newColor, property[key])) return
+    setActiveToolColor(key, newColor)
+  }
 </script>
 
 <style lang="scss">
@@ -484,7 +525,6 @@
     display: flex;
     flex-direction: column;
     height: calc(100% - 88px);
-    overflow: hidden;
     transition: transform .3s ease-in-out;
     background-color: var(--c-side-bg);
     border-left: 1px solid var(--c-side-header-border);

+ 77 - 1
packages/webview/src/helpers/utils.js

@@ -14,4 +14,80 @@ const uploadFile = (extension) =>
     fileInput.click();
   });
 
-export { uploadFile }
+  // HSL 转 RGB
+const hslToRgb = (h, s, l) => {
+  let R, G, B;
+
+  if (s === 0) {
+    R = G = B = l;
+  } else {
+    const hue2rgb = function hue2rgb(p, q, t) {
+      if (t < 0) t += 1;
+      if (t > 1) t -= 1;
+      if (t < 1 / 6) return p + (q - p) * 6 * t;
+      if (t < 1 / 2) return q;
+      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+      return p;
+    }
+
+    const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+    const p = 2 * l - q;
+    R = Math.round(hue2rgb(p, q, h + 1 / 3) * 255);
+    G = Math.round(hue2rgb(p, q, h) * 255);
+    B = Math.round(hue2rgb(p, q, h - 1 / 3) * 255);
+  }
+
+  return {R, G, B};
+}
+
+// 转换颜色 hsl / hex -> rgb
+const convertColor = (colorStr) => {
+  if (colorStr.startsWith('#')) {
+    const hexValues = colorStr.slice(1).match(/.{2}/g);
+    if (hexValues) {
+      return {
+        R: parseInt(hexValues[0], 16),
+        G: parseInt(hexValues[1], 16),
+        B: parseInt(hexValues[2], 16)
+      };
+    }
+  }
+
+  if (colorStr.startsWith('rgb')) {
+    const matches = colorStr.match(/\d+/g);
+    if (matches && matches.length === 3) {
+      return {
+        R: parseInt(matches[0]),
+        G: parseInt(matches[1]),
+        B: parseInt(matches[2])
+      };
+    }
+  }
+
+  if (colorStr.startsWith('hsl')) {
+    const matches = colorStr.match(/(\d+),\s*(\d+%),\s*(\d+%)/);
+    if (matches) {
+      const [h, s, l] = [parseInt(matches[1]), parseInt(matches[2]) / 100, parseInt(matches[3]) / 100];
+      const {R, G, B} = hslToRgb(h / 360, s, l);
+      return {R, G, B};
+    }
+  }
+
+  return null;
+}
+
+// 判断两个颜色字符串在阈值外是否相同
+const areColorsSimilar = (colorStr1, colorStr2, threshold = 10) => {
+  const rgb1 = convertColor(colorStr1);
+  const rgb2 = convertColor(colorStr2);
+
+  if (!rgb1 || !rgb2) return false;
+
+  // 比较RGB值是否在每个分量上差异都小于阈值
+  const diffR = Math.abs(rgb1.R - rgb2.R);
+  const diffG = Math.abs(rgb1.G - rgb2.G);
+  const diffB = Math.abs(rgb1.B - rgb2.B);
+
+  return diffR <= threshold && diffG <= threshold && diffB <= threshold;
+}
+export { uploadFile, areColorsSimilar }