@veltra/desktop 1.7.7 → 1.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/cascade/use-check.js +5 -2
- package/dist/components/cascade/use-check.js.map +1 -1
- package/dist/components/cascade/use-select.js +5 -1
- package/dist/components/cascade/use-select.js.map +1 -1
- package/dist/components/code-editor/langs/bash/index.js +1 -1
- package/dist/components/code-editor/langs/powershell/index.js +1 -1
- package/dist/components/image-cropper/draw-crop.js +63 -0
- package/dist/components/image-cropper/draw-crop.js.map +1 -0
- package/dist/components/image-cropper/image-cropper.js +344 -0
- package/dist/components/image-cropper/image-cropper.js.map +1 -0
- package/dist/components/image-cropper/style.d.ts +1 -0
- package/dist/components/image-cropper/style.js +1 -0
- package/dist/components/image-cropper/style2.css +205 -0
- package/dist/components/image-cropper/use-image-loader.js +59 -0
- package/dist/components/image-cropper/use-image-loader.js.map +1 -0
- package/dist/components/image-cropper/use-preview.js +44 -0
- package/dist/components/image-cropper/use-preview.js.map +1 -0
- package/dist/components/image-cropper/use-selection.js +183 -0
- package/dist/components/image-cropper/use-selection.js.map +1 -0
- package/dist/components/image-cropper/use-transform.js +182 -0
- package/dist/components/image-cropper/use-transform.js.map +1 -0
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.js +3 -1
- package/dist/components/index.js.map +1 -1
- package/dist/components/table/style2.css +1 -1
- package/dist/index.d.ts +47 -45
- package/dist/index.js +46 -45
- package/dist/node_modules/.bun/{@codemirror_legacy-modes@6.5.3 → @codemirror_legacy-modes@6.5.4}/node_modules/@codemirror/legacy-modes/mode/powershell.js +1 -1
- package/dist/node_modules/.bun/{@codemirror_legacy-modes@6.5.3 → @codemirror_legacy-modes@6.5.4}/node_modules/@codemirror/legacy-modes/mode/powershell.js.map +1 -1
- package/dist/node_modules/.bun/{@codemirror_legacy-modes@6.5.3 → @codemirror_legacy-modes@6.5.4}/node_modules/@codemirror/legacy-modes/mode/shell.js +1 -1
- package/dist/node_modules/.bun/{@codemirror_legacy-modes@6.5.3 → @codemirror_legacy-modes@6.5.4}/node_modules/@codemirror/legacy-modes/mode/shell.js.map +1 -1
- package/dist/types/cascade.d.ts +6 -2
- package/dist/types/image-cropper.d.ts +68 -0
- package/dist/types/index.d.ts +2 -1
- package/package.json +15 -15
- package/src/components/auto-complete/style.scss +3 -15
- package/src/components/cascade/__test__/cascade.test.ts +103 -0
- package/src/components/cascade/use-check.ts +5 -2
- package/src/components/cascade/use-select.ts +2 -1
- package/src/components/contextmenu/style.scss +4 -20
- package/src/components/image-cropper/__test__/draw-crop.test.ts +131 -0
- package/src/components/image-cropper/__test__/image-cropper.test.ts +744 -0
- package/src/components/image-cropper/__test__/use-transform.test.ts +229 -0
- package/src/components/image-cropper/draw-crop.ts +112 -0
- package/src/components/image-cropper/image-cropper.vue +302 -0
- package/src/components/image-cropper/index.ts +1 -0
- package/src/components/image-cropper/style.scss +232 -0
- package/src/components/image-cropper/style.ts +1 -0
- package/src/components/image-cropper/use-image-loader.ts +79 -0
- package/src/components/image-cropper/use-preview.ts +58 -0
- package/src/components/image-cropper/use-selection.ts +255 -0
- package/src/components/image-cropper/use-transform.ts +256 -0
- package/src/components/index.ts +2 -0
- package/src/components/select/style.scss +3 -15
- package/src/components/table/style.scss +1 -1
- package/src/types/cascade.ts +10 -2
- package/src/types/image-cropper.ts +73 -0
- package/src/types/index.ts +1 -0
|
@@ -22,10 +22,13 @@ function useCheck(options) {
|
|
|
22
22
|
const checkedArr = Array.from(checkedSet.value);
|
|
23
23
|
const targetValues = checkedArr.map((item) => item.value);
|
|
24
24
|
const targetLabels = checkedArr.map((item) => item.label);
|
|
25
|
-
const targetItems = checkedArr.map((item) =>
|
|
25
|
+
const targetItems = checkedArr.map((item) => {
|
|
26
|
+
const data = item.data;
|
|
27
|
+
return data ? Object.assign({}, data, { fullLabel: item.label }) : data;
|
|
28
|
+
});
|
|
26
29
|
emit("update:modelValue", targetValues);
|
|
27
30
|
emit("update:label", targetLabels.length ? targetLabels : void 0);
|
|
28
|
-
emit("change", targetItems);
|
|
31
|
+
emit("change", targetItems, targetLabels.length ? targetLabels : void 0);
|
|
29
32
|
}
|
|
30
33
|
const restTag = computed(() => {
|
|
31
34
|
const { visibilityLimit, modelValue } = props;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-check.js","names":[],"sources":["../../../src/components/cascade/use-check.ts"],"sourcesContent":["import { dfs, type Forest } from '@cat-kit/core'\nimport {\n computed,\n shallowRef,\n triggerRef,\n watch,\n type ComputedRef,\n type Ref,\n type ShallowRef\n} from 'vue'\n\nimport type { CascadeProps, CascadeEmits, CascadeNode } from '../../types'\n\ninterface CheckOptions {\n props: CascadeProps\n emit: CascadeEmits\n dataMap: ShallowRef<Map<string, CascadeNode>>\n disabled: Ref<boolean>\n readonly: Ref<boolean>\n forest: Ref<Forest<Record<string, unknown>, any>>\n isUserActive: () => boolean\n getPanelItemList: (data?: CascadeNode[]) => void\n}\n\ninterface UseCheckReturned {\n hovered: ShallowRef<boolean>\n tags: ComputedRef<CascadeNode[]>\n checkedSet: ShallowRef<Set<CascadeNode>>\n restTag: ComputedRef<number>\n handleCloseTag: (tag: CascadeNode) => void\n updateMultipleValue: () => void\n checkItem: (item: CascadeNode, checked: boolean) => void\n}\n\nexport function useCheck(options: CheckOptions): UseCheckReturned {\n const { props, emit, dataMap, disabled, readonly, forest, isUserActive, getPanelItemList } =\n options\n\n const hovered = shallowRef(false)\n\n const checkedSet = shallowRef(new Set<CascadeNode>())\n\n const tags = computed(() => {\n const { modelValue, multiple } = props\n let tags: CascadeNode[] = []\n\n if (!multiple || !Array.isArray(modelValue)) return tags\n\n let { visibilityLimit } = props\n if (visibilityLimit! < 0) {\n visibilityLimit = 0\n }\n\n // 禁用时,显示全部\n if (disabled.value || readonly.value) {\n visibilityLimit = props.modelValue?.length ?? 0\n }\n\n modelValue.slice(0, visibilityLimit).forEach((k) => {\n const option = dataMap.value.get(k)\n option && tags.push(option)\n })\n\n return tags\n })\n\n function updateMultipleValue() {\n const checkedArr = Array.from(checkedSet.value)\n const targetValues = checkedArr.map((item) => item.value)\n const targetLabels = checkedArr.map((item) => item.label)\n const targetItems = checkedArr.map((item) => item.data as Record<string, any
|
|
1
|
+
{"version":3,"file":"use-check.js","names":[],"sources":["../../../src/components/cascade/use-check.ts"],"sourcesContent":["import { dfs, type Forest } from '@cat-kit/core'\nimport {\n computed,\n shallowRef,\n triggerRef,\n watch,\n type ComputedRef,\n type Ref,\n type ShallowRef\n} from 'vue'\n\nimport type { CascadeProps, CascadeEmits, CascadeNode } from '../../types'\n\ninterface CheckOptions {\n props: CascadeProps\n emit: CascadeEmits\n dataMap: ShallowRef<Map<string, CascadeNode>>\n disabled: Ref<boolean>\n readonly: Ref<boolean>\n forest: Ref<Forest<Record<string, unknown>, any>>\n isUserActive: () => boolean\n getPanelItemList: (data?: CascadeNode[]) => void\n}\n\ninterface UseCheckReturned {\n hovered: ShallowRef<boolean>\n tags: ComputedRef<CascadeNode[]>\n checkedSet: ShallowRef<Set<CascadeNode>>\n restTag: ComputedRef<number>\n handleCloseTag: (tag: CascadeNode) => void\n updateMultipleValue: () => void\n checkItem: (item: CascadeNode, checked: boolean) => void\n}\n\nexport function useCheck(options: CheckOptions): UseCheckReturned {\n const { props, emit, dataMap, disabled, readonly, forest, isUserActive, getPanelItemList } =\n options\n\n const hovered = shallowRef(false)\n\n const checkedSet = shallowRef(new Set<CascadeNode>())\n\n const tags = computed(() => {\n const { modelValue, multiple } = props\n let tags: CascadeNode[] = []\n\n if (!multiple || !Array.isArray(modelValue)) return tags\n\n let { visibilityLimit } = props\n if (visibilityLimit! < 0) {\n visibilityLimit = 0\n }\n\n // 禁用时,显示全部\n if (disabled.value || readonly.value) {\n visibilityLimit = props.modelValue?.length ?? 0\n }\n\n modelValue.slice(0, visibilityLimit).forEach((k) => {\n const option = dataMap.value.get(k)\n option && tags.push(option)\n })\n\n return tags\n })\n\n function updateMultipleValue() {\n const checkedArr = Array.from(checkedSet.value)\n const targetValues = checkedArr.map((item) => item.value)\n const targetLabels = checkedArr.map((item) => item.label)\n const targetItems = checkedArr.map((item) => {\n const data = item.data as Record<string, any>\n return data ? Object.assign({}, data, { fullLabel: item.label }) : data\n })\n emit('update:modelValue', targetValues)\n emit('update:label', targetLabels.length ? targetLabels : undefined)\n emit('change', targetItems, targetLabels.length ? targetLabels : undefined)\n }\n\n const restTag = computed(() => {\n const { visibilityLimit, modelValue } = props\n\n return Math.max((modelValue?.length ?? 0) - visibilityLimit!, 0)\n })\n\n function handleCloseTag(tag: CascadeNode) {\n checkedSet.value.delete(tag)\n updateMultipleValue()\n }\n\n function checkItem(item: CascadeNode, checked: boolean) {\n const childrenKey = props.childrenKey ?? 'children'\n if (checked) {\n dfs(\n item as unknown as Record<string, unknown>,\n (node) => {\n checkedSet.value.add(node as unknown as CascadeNode)\n },\n childrenKey\n )\n } else {\n dfs(\n item as unknown as Record<string, unknown>,\n (node) => {\n checkedSet.value.delete(node as unknown as CascadeNode)\n },\n childrenKey\n )\n }\n\n triggerRef(checkedSet)\n\n updateMultipleValue()\n }\n\n function initMultipleCheck() {\n const { modelValue } = props\n getPanelItemList()\n if (Array.isArray(modelValue)) {\n checkedSet.value = new Set(modelValue.map((v) => dataMap.value.get(v)!))\n }\n }\n\n watch(\n [() => props.multiple, () => props.modelValue, forest],\n ([multiple]) => {\n if (!multiple || isUserActive()) return\n initMultipleCheck()\n },\n { immediate: false }\n )\n\n return { hovered, tags, checkedSet, restTag, handleCloseTag, updateMultipleValue, checkItem }\n}\n"],"mappings":";;;AAkCA,SAAgB,SAAS,SAAyC;CAChE,MAAM,EAAE,OAAO,MAAM,SAAS,UAAU,UAAU,QAAQ,cAAc,qBACtE;CAEF,MAAM,UAAU,WAAW,KAAK;CAEhC,MAAM,aAAa,2BAAW,IAAI,IAAiB,CAAC;CAEpD,MAAM,OAAO,eAAe;EAC1B,MAAM,EAAE,YAAY,aAAa;EACjC,IAAI,OAAsB,CAAC;EAE3B,IAAI,CAAC,YAAY,CAAC,MAAM,QAAQ,UAAU,GAAG,OAAO;EAEpD,IAAI,EAAE,oBAAoB;EAC1B,IAAI,kBAAmB,GACrB,kBAAkB;EAIpB,IAAI,SAAS,SAAS,SAAS,OAC7B,kBAAkB,MAAM,YAAY,UAAU;EAGhD,WAAW,MAAM,GAAG,eAAe,CAAC,CAAC,SAAS,MAAM;GAClD,MAAM,SAAS,QAAQ,MAAM,IAAI,CAAC;GAClC,UAAU,KAAK,KAAK,MAAM;EAC5B,CAAC;EAED,OAAO;CACT,CAAC;CAED,SAAS,sBAAsB;EAC7B,MAAM,aAAa,MAAM,KAAK,WAAW,KAAK;EAC9C,MAAM,eAAe,WAAW,KAAK,SAAS,KAAK,KAAK;EACxD,MAAM,eAAe,WAAW,KAAK,SAAS,KAAK,KAAK;EACxD,MAAM,cAAc,WAAW,KAAK,SAAS;GAC3C,MAAM,OAAO,KAAK;GAClB,OAAO,OAAO,OAAO,OAAO,CAAC,GAAG,MAAM,EAAE,WAAW,KAAK,MAAM,CAAC,IAAI;EACrE,CAAC;EACD,KAAK,qBAAqB,YAAY;EACtC,KAAK,gBAAgB,aAAa,SAAS,eAAe,KAAA,CAAS;EACnE,KAAK,UAAU,aAAa,aAAa,SAAS,eAAe,KAAA,CAAS;CAC5E;CAEA,MAAM,UAAU,eAAe;EAC7B,MAAM,EAAE,iBAAiB,eAAe;EAExC,OAAO,KAAK,KAAK,YAAY,UAAU,KAAK,iBAAkB,CAAC;CACjE,CAAC;CAED,SAAS,eAAe,KAAkB;EACxC,WAAW,MAAM,OAAO,GAAG;EAC3B,oBAAoB;CACtB;CAEA,SAAS,UAAU,MAAmB,SAAkB;EACtD,MAAM,cAAc,MAAM,eAAe;EACzC,IAAI,SACF,IACE,OACC,SAAS;GACR,WAAW,MAAM,IAAI,IAA8B;EACrD,GACA,WACF;OAEA,IACE,OACC,SAAS;GACR,WAAW,MAAM,OAAO,IAA8B;EACxD,GACA,WACF;EAGF,WAAW,UAAU;EAErB,oBAAoB;CACtB;CAEA,SAAS,oBAAoB;EAC3B,MAAM,EAAE,eAAe;EACvB,iBAAiB;EACjB,IAAI,MAAM,QAAQ,UAAU,GAC1B,WAAW,QAAQ,IAAI,IAAI,WAAW,KAAK,MAAM,QAAQ,MAAM,IAAI,CAAC,CAAE,CAAC;CAE3E;CAEA,MACE;QAAO,MAAM;QAAgB,MAAM;EAAY;CAAM,IACpD,CAAC,cAAc;EACd,IAAI,CAAC,YAAY,aAAa,GAAG;EACjC,kBAAkB;CACpB,GACA,EAAE,WAAW,MAAM,CACrB;CAEA,OAAO;EAAE;EAAS;EAAM;EAAY;EAAS;EAAgB;EAAqB;CAAU;AAC9F"}
|
|
@@ -55,9 +55,13 @@ function useSelect(options) {
|
|
|
55
55
|
const targetValue = showFullPath ? keys.length ? keys.join(separator) : void 0 : leafKey;
|
|
56
56
|
const targetLabel = showFullPath ? keys.map((key) => dataMap.value.get(key)?.label).filter(Boolean).join(separator) : leafLabel;
|
|
57
57
|
const leafNode = leafKey ? dataMap.value.get(leafKey) : void 0;
|
|
58
|
+
const changeItem = leafNode?.data ? {
|
|
59
|
+
...leafNode.data,
|
|
60
|
+
fullLabel: targetLabel
|
|
61
|
+
} : void 0;
|
|
58
62
|
emit("update:modelValue", targetValue);
|
|
59
63
|
emit("update:label", targetLabel);
|
|
60
|
-
emit("change",
|
|
64
|
+
emit("change", changeItem, targetLabel);
|
|
61
65
|
}
|
|
62
66
|
/**
|
|
63
67
|
* 单选选择
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-select.js","names":[],"sources":["../../../src/components/cascade/use-select.ts"],"sourcesContent":["import { Forest } from '@cat-kit/core'\nimport { createIncrease } from '@veltra/utils'\nimport {\n computed,\n nextTick,\n shallowRef,\n triggerRef,\n watch,\n type ComputedRef,\n type Ref,\n type ShallowRef\n} from 'vue'\n\nimport type {\n CascadeProps,\n CascadeEmits,\n CascadeNode,\n DropdownExposed,\n PanelItem\n} from '../../types'\n\ninterface SelectOptions {\n props: CascadeProps\n emit: CascadeEmits\n isUserActive: () => boolean\n forest: Ref<Forest<Record<string, unknown>, any>>\n dataMap: Ref<Map<string, CascadeNode>>\n dropdownRef: Ref<DropdownExposed | undefined>\n}\n\nfunction buildPathKeys(leaf: CascadeNode): string[] {\n const keys: string[] = []\n let node: CascadeNode | undefined = leaf\n while (node) {\n keys.unshift(node.value)\n node = node.parent\n }\n return keys\n}\n\ninterface UseSelectReturned {\n displayedValue: ComputedRef<string | undefined>\n selectItem: (panelIndex: number, item: CascadeNode) => void\n selectedNodeKeys: ShallowRef<string[]>\n updateSingleValue: () => void\n initSingleSelect: () => void\n panelItemList: ShallowRef<PanelItem[]>\n getPanelItemList: (data?: CascadeNode[]) => void\n setPanelItem: (panelIndex: number, panelData?: CascadeNode[]) => void\n createPanelItem: (nodes: CascadeNode[]) => PanelItem\n}\n\nexport function useSelect(options: SelectOptions): UseSelectReturned {\n const { props, emit, dataMap, isUserActive, dropdownRef, forest } = options\n\n /** 选中的节点key */\n const selectedNodeKeys = shallowRef<string[]>([])\n\n /** 面板数据 */\n const panelItemList = shallowRef<PanelItem[]>([])\n const uid = createIncrease()\n function createPanelItem(nodes: CascadeNode[]): PanelItem {\n return { key: uid(), nodes: nodes.filter((node) => node.visible) }\n }\n\n const displayedValue = computed(() => {\n const { modelValue, separator, showFullPath } = props\n if (!modelValue || typeof modelValue !== 'string') return ''\n\n if (showFullPath) {\n return modelValue\n .split(separator!)\n .map((v) => dataMap.value.get(v)?.label ?? v)\n .join(separator)\n }\n return dataMap.value.get(modelValue)?.label ?? modelValue\n })\n\n function getPanelItemList(data?: CascadeNode[]) {\n const _panelItemList: Array<PanelItem> = []\n if (!data?.length) return panelItemList\n _panelItemList.push(createPanelItem(data))\n\n selectedNodeKeys.value.slice(0, -1).forEach((key) => {\n const node = dataMap.value.get(key)\n node?.children?.length && _panelItemList.push(createPanelItem(node.children))\n })\n\n panelItemList.value = _panelItemList\n }\n\n function setPanelItem(panelIndex: number, panelData?: CascadeNode[]) {\n if (panelData?.length) {\n panelItemList.value[panelIndex + 1] = createPanelItem(panelData)\n panelItemList.value.splice(panelIndex + 2)\n } else {\n panelItemList.value.splice(panelIndex + 1)\n }\n\n triggerRef(panelItemList)\n }\n\n function updateSingleValue() {\n const { separator, showFullPath } = props\n const keys = selectedNodeKeys.value\n const leafKey = keys.length ? keys[keys.length - 1] : undefined\n const leafLabel = leafKey ? dataMap.value.get(leafKey)?.label : undefined\n\n const targetValue = showFullPath ? (keys.length ? keys.join(separator!) : undefined) : leafKey\n const targetLabel = showFullPath\n ? keys\n .map((key) => dataMap.value.get(key)?.label)\n .filter(Boolean)\n .join(separator!)\n : leafLabel\n\n const leafNode = leafKey ? dataMap.value.get(leafKey) : undefined\n\n emit('update:modelValue', targetValue)\n emit('update:label', targetLabel)\n emit('change',
|
|
1
|
+
{"version":3,"file":"use-select.js","names":[],"sources":["../../../src/components/cascade/use-select.ts"],"sourcesContent":["import { Forest } from '@cat-kit/core'\nimport { createIncrease } from '@veltra/utils'\nimport {\n computed,\n nextTick,\n shallowRef,\n triggerRef,\n watch,\n type ComputedRef,\n type Ref,\n type ShallowRef\n} from 'vue'\n\nimport type {\n CascadeProps,\n CascadeEmits,\n CascadeNode,\n DropdownExposed,\n PanelItem\n} from '../../types'\n\ninterface SelectOptions {\n props: CascadeProps\n emit: CascadeEmits\n isUserActive: () => boolean\n forest: Ref<Forest<Record<string, unknown>, any>>\n dataMap: Ref<Map<string, CascadeNode>>\n dropdownRef: Ref<DropdownExposed | undefined>\n}\n\nfunction buildPathKeys(leaf: CascadeNode): string[] {\n const keys: string[] = []\n let node: CascadeNode | undefined = leaf\n while (node) {\n keys.unshift(node.value)\n node = node.parent\n }\n return keys\n}\n\ninterface UseSelectReturned {\n displayedValue: ComputedRef<string | undefined>\n selectItem: (panelIndex: number, item: CascadeNode) => void\n selectedNodeKeys: ShallowRef<string[]>\n updateSingleValue: () => void\n initSingleSelect: () => void\n panelItemList: ShallowRef<PanelItem[]>\n getPanelItemList: (data?: CascadeNode[]) => void\n setPanelItem: (panelIndex: number, panelData?: CascadeNode[]) => void\n createPanelItem: (nodes: CascadeNode[]) => PanelItem\n}\n\nexport function useSelect(options: SelectOptions): UseSelectReturned {\n const { props, emit, dataMap, isUserActive, dropdownRef, forest } = options\n\n /** 选中的节点key */\n const selectedNodeKeys = shallowRef<string[]>([])\n\n /** 面板数据 */\n const panelItemList = shallowRef<PanelItem[]>([])\n const uid = createIncrease()\n function createPanelItem(nodes: CascadeNode[]): PanelItem {\n return { key: uid(), nodes: nodes.filter((node) => node.visible) }\n }\n\n const displayedValue = computed(() => {\n const { modelValue, separator, showFullPath } = props\n if (!modelValue || typeof modelValue !== 'string') return ''\n\n if (showFullPath) {\n return modelValue\n .split(separator!)\n .map((v) => dataMap.value.get(v)?.label ?? v)\n .join(separator)\n }\n return dataMap.value.get(modelValue)?.label ?? modelValue\n })\n\n function getPanelItemList(data?: CascadeNode[]) {\n const _panelItemList: Array<PanelItem> = []\n if (!data?.length) return panelItemList\n _panelItemList.push(createPanelItem(data))\n\n selectedNodeKeys.value.slice(0, -1).forEach((key) => {\n const node = dataMap.value.get(key)\n node?.children?.length && _panelItemList.push(createPanelItem(node.children))\n })\n\n panelItemList.value = _panelItemList\n }\n\n function setPanelItem(panelIndex: number, panelData?: CascadeNode[]) {\n if (panelData?.length) {\n panelItemList.value[panelIndex + 1] = createPanelItem(panelData)\n panelItemList.value.splice(panelIndex + 2)\n } else {\n panelItemList.value.splice(panelIndex + 1)\n }\n\n triggerRef(panelItemList)\n }\n\n function updateSingleValue() {\n const { separator, showFullPath } = props\n const keys = selectedNodeKeys.value\n const leafKey = keys.length ? keys[keys.length - 1] : undefined\n const leafLabel = leafKey ? dataMap.value.get(leafKey)?.label : undefined\n\n const targetValue = showFullPath ? (keys.length ? keys.join(separator!) : undefined) : leafKey\n const targetLabel = showFullPath\n ? keys\n .map((key) => dataMap.value.get(key)?.label)\n .filter(Boolean)\n .join(separator!)\n : leafLabel\n\n const leafNode = leafKey ? dataMap.value.get(leafKey) : undefined\n const changeItem = leafNode?.data ? { ...leafNode.data, fullLabel: targetLabel } : undefined\n\n emit('update:modelValue', targetValue)\n emit('update:label', targetLabel)\n emit('change', changeItem, targetLabel)\n }\n\n /**\n * 单选选择\n *\n * @param panelIndex 面板索引\n * @param item 选中的节点\n */\n function selectItem(panelIndex: number, item: CascadeNode) {\n // 设置选中的节点\n selectedNodeKeys.value[panelIndex] = item.value\n selectedNodeKeys.value.splice(panelIndex + 1)\n triggerRef(selectedNodeKeys)\n\n // 展开子级面板\n setPanelItem(panelIndex, item.children)\n\n nextTick(() => {\n dropdownRef.value?.updateDropdown()\n })\n\n if (!item.children?.length && !props.multiple) {\n dropdownRef.value?.close()\n }\n }\n\n function initSingleSelect() {\n const { modelValue, separator, showFullPath } = props\n\n if (modelValue && typeof modelValue === 'string') {\n let keys: string[]\n if (showFullPath) {\n keys = modelValue.split(separator!)\n } else {\n const leaf = dataMap.value.get(modelValue)\n keys = leaf ? buildPathKeys(leaf) : [modelValue]\n }\n selectedNodeKeys.value = keys\n getPanelItemList(forest.value.roots)\n } else {\n selectedNodeKeys.value = []\n getPanelItemList(forest.value.roots)\n }\n }\n\n watch(\n [() => props.multiple, () => props.modelValue, forest],\n ([multiple]) => {\n if (multiple || isUserActive()) return\n initSingleSelect()\n },\n { immediate: true }\n )\n\n return {\n displayedValue,\n selectItem,\n selectedNodeKeys,\n updateSingleValue,\n initSingleSelect,\n panelItemList,\n getPanelItemList,\n setPanelItem,\n createPanelItem\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LanguageSupport, StreamLanguage } from "../../../../node_modules/.bun/@codemirror_language@6.12.4/node_modules/@codemirror/language/dist/index.js";
|
|
2
|
-
import { shell } from "../../../../node_modules/.bun/@codemirror_legacy-modes@6.5.
|
|
2
|
+
import { shell } from "../../../../node_modules/.bun/@codemirror_legacy-modes@6.5.4/node_modules/@codemirror/legacy-modes/mode/shell.js";
|
|
3
3
|
//#region src/components/code-editor/langs/bash/index.ts
|
|
4
4
|
/** Bash / shell 脚本语言支持(基于 CodeMirror legacy shell mode) */
|
|
5
5
|
function bash() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LanguageSupport, StreamLanguage } from "../../../../node_modules/.bun/@codemirror_language@6.12.4/node_modules/@codemirror/language/dist/index.js";
|
|
2
|
-
import { powerShell } from "../../../../node_modules/.bun/@codemirror_legacy-modes@6.5.
|
|
2
|
+
import { powerShell } from "../../../../node_modules/.bun/@codemirror_legacy-modes@6.5.4/node_modules/@codemirror/legacy-modes/mode/powershell.js";
|
|
3
3
|
//#region src/components/code-editor/langs/powershell/index.ts
|
|
4
4
|
/** PowerShell 语言支持(基于 CodeMirror legacy powershell mode) */
|
|
5
5
|
function powershell() {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//#region src/components/image-cropper/draw-crop.ts
|
|
2
|
+
/**
|
|
3
|
+
* 把选区按当前旋转 / 翻转变换绘制到目标 canvas,预览(P4)与 getResult()(P5)共用。
|
|
4
|
+
* 输出为屏幕上看到的裁剪结果:先旋转后翻转,与舞台 transform 链一致
|
|
5
|
+
*/
|
|
6
|
+
function drawCropToCanvas(img, selection, transform, canvas, outSize) {
|
|
7
|
+
const { rotation, flipX, flipY } = transform;
|
|
8
|
+
const swapped = rotation % 180 !== 0;
|
|
9
|
+
const baseW = swapped ? selection.height : selection.width;
|
|
10
|
+
const baseH = swapped ? selection.width : selection.height;
|
|
11
|
+
const outW = Math.max(1, Math.round(outSize?.width ?? (outSize?.height ?? baseH) / baseH * baseW));
|
|
12
|
+
const outH = Math.max(1, Math.round(outSize?.height ?? (outSize?.width ?? baseW) / baseW * baseH));
|
|
13
|
+
canvas.width = outW;
|
|
14
|
+
canvas.height = outH;
|
|
15
|
+
const ctx = canvas.getContext("2d");
|
|
16
|
+
if (!ctx) return;
|
|
17
|
+
ctx.translate(outW / 2, outH / 2);
|
|
18
|
+
ctx.rotate(rotation * Math.PI / 180);
|
|
19
|
+
ctx.scale((flipX ? -1 : 1) * outW / baseW, (flipY ? -1 : 1) * outH / baseH);
|
|
20
|
+
ctx.drawImage(img, selection.x, selection.y, selection.width, selection.height, -selection.width / 2, -selection.height / 2, selection.width, selection.height);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* canvas 导出为 Blob。
|
|
24
|
+
* 跨域图片污染画布时 toBlob 同步抛 SecurityError,包装为可捕获的 Promise 拒绝
|
|
25
|
+
*/
|
|
26
|
+
function canvasToBlob(canvas) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
try {
|
|
29
|
+
canvas.toBlob((blob) => {
|
|
30
|
+
if (blob) resolve(blob);
|
|
31
|
+
else reject(/* @__PURE__ */ new Error("Export crop result failed: canvas.toBlob got null"));
|
|
32
|
+
}, "image/png");
|
|
33
|
+
} catch (err) {
|
|
34
|
+
reject(new Error("Export crop result failed: canvas is tainted by cross-origin image", { cause: err }));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** Blob 读取为 dataURL 形式的 base64 */
|
|
39
|
+
function blobToBase64(blob) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const reader = new FileReader();
|
|
42
|
+
reader.onload = () => resolve(reader.result);
|
|
43
|
+
reader.onerror = () => reject(reader.error ?? /* @__PURE__ */ new Error("Export crop result failed: read blob as base64 failed"));
|
|
44
|
+
reader.readAsDataURL(blob);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 输出选区裁剪结果:离屏 canvas 经 drawCropToCanvas 按原图像素绘制后导出 Blob + base64。
|
|
49
|
+
* outSize 缺省为原图选区像素尺寸,传入则按该尺寸缩放输出
|
|
50
|
+
*/
|
|
51
|
+
async function cropToResult(img, selection, transform, outSize) {
|
|
52
|
+
const canvas = document.createElement("canvas");
|
|
53
|
+
drawCropToCanvas(img, selection, transform, canvas, outSize);
|
|
54
|
+
const blob = await canvasToBlob(canvas);
|
|
55
|
+
return {
|
|
56
|
+
blob,
|
|
57
|
+
base64: await blobToBase64(blob)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { cropToResult, drawCropToCanvas };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=draw-crop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draw-crop.js","names":[],"sources":["../../../src/components/image-cropper/draw-crop.ts"],"sourcesContent":["import type { ImageCropperResult } from '../../types'\nimport type { SelectionRect } from './use-selection'\n\n/** 影响裁剪输出的变换(缩放 / 平移只影响画布展示,与输出无关) */\nexport interface CropTransform {\n /** 90° 步进角度:0 / 90 / 180 / 270 */\n rotation: number\n flipX: boolean\n flipY: boolean\n}\n\n/** 输出尺寸,缺省维度按选区比例推算,都不传则为原图选区像素尺寸 */\nexport interface CropOutputSize {\n width?: number\n height?: number\n}\n\n/**\n * 把选区按当前旋转 / 翻转变换绘制到目标 canvas,预览(P4)与 getResult()(P5)共用。\n * 输出为屏幕上看到的裁剪结果:先旋转后翻转,与舞台 transform 链一致\n */\nexport function drawCropToCanvas(\n img: HTMLImageElement,\n selection: SelectionRect,\n transform: CropTransform,\n canvas: HTMLCanvasElement,\n outSize?: CropOutputSize\n): void {\n const { rotation, flipX, flipY } = transform\n // 旋转 90° / 270° 后输出的宽为选区高、高为选区宽\n const swapped = rotation % 180 !== 0\n const baseW = swapped ? selection.height : selection.width\n const baseH = swapped ? selection.width : selection.height\n\n const outW = Math.max(\n 1,\n Math.round(outSize?.width ?? ((outSize?.height ?? baseH) / baseH) * baseW)\n )\n const outH = Math.max(\n 1,\n Math.round(outSize?.height ?? ((outSize?.width ?? baseW) / baseW) * baseH)\n )\n canvas.width = outW\n canvas.height = outH\n\n const ctx = canvas.getContext('2d')\n if (!ctx) return\n\n ctx.translate(outW / 2, outH / 2)\n ctx.rotate((rotation * Math.PI) / 180)\n ctx.scale(((flipX ? -1 : 1) * outW) / baseW, ((flipY ? -1 : 1) * outH) / baseH)\n // 以画布中心为锚绘制选区,旋转 / 翻转由上面的变换承担\n ctx.drawImage(\n img,\n selection.x,\n selection.y,\n selection.width,\n selection.height,\n -selection.width / 2,\n -selection.height / 2,\n selection.width,\n selection.height\n )\n}\n\n/**\n * canvas 导出为 Blob。\n * 跨域图片污染画布时 toBlob 同步抛 SecurityError,包装为可捕获的 Promise 拒绝\n */\nfunction canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {\n return new Promise((resolve, reject) => {\n try {\n canvas.toBlob((blob) => {\n if (blob) resolve(blob)\n else reject(new Error('Export crop result failed: canvas.toBlob got null'))\n }, 'image/png')\n } catch (err) {\n reject(\n new Error('Export crop result failed: canvas is tainted by cross-origin image', {\n cause: err\n })\n )\n }\n })\n}\n\n/** Blob 读取为 dataURL 形式的 base64 */\nfunction blobToBase64(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader()\n reader.onload = () => resolve(reader.result as string)\n reader.onerror = () =>\n reject(reader.error ?? new Error('Export crop result failed: read blob as base64 failed'))\n reader.readAsDataURL(blob)\n })\n}\n\n/**\n * 输出选区裁剪结果:离屏 canvas 经 drawCropToCanvas 按原图像素绘制后导出 Blob + base64。\n * outSize 缺省为原图选区像素尺寸,传入则按该尺寸缩放输出\n */\nexport async function cropToResult(\n img: HTMLImageElement,\n selection: SelectionRect,\n transform: CropTransform,\n outSize?: CropOutputSize\n): Promise<ImageCropperResult> {\n const canvas = document.createElement('canvas')\n drawCropToCanvas(img, selection, transform, canvas, outSize)\n const blob = await canvasToBlob(canvas)\n return { blob, base64: await blobToBase64(blob) }\n}\n"],"mappings":";;;;;AAqBA,SAAgB,iBACd,KACA,WACA,WACA,QACA,SACM;CACN,MAAM,EAAE,UAAU,OAAO,UAAU;CAEnC,MAAM,UAAU,WAAW,QAAQ;CACnC,MAAM,QAAQ,UAAU,UAAU,SAAS,UAAU;CACrD,MAAM,QAAQ,UAAU,UAAU,QAAQ,UAAU;CAEpD,MAAM,OAAO,KAAK,IAChB,GACA,KAAK,MAAM,SAAS,UAAW,SAAS,UAAU,SAAS,QAAS,KAAK,CAC3E;CACA,MAAM,OAAO,KAAK,IAChB,GACA,KAAK,MAAM,SAAS,WAAY,SAAS,SAAS,SAAS,QAAS,KAAK,CAC3E;CACA,OAAO,QAAQ;CACf,OAAO,SAAS;CAEhB,MAAM,MAAM,OAAO,WAAW,IAAI;CAClC,IAAI,CAAC,KAAK;CAEV,IAAI,UAAU,OAAO,GAAG,OAAO,CAAC;CAChC,IAAI,OAAQ,WAAW,KAAK,KAAM,GAAG;CACrC,IAAI,OAAQ,QAAQ,KAAK,KAAK,OAAQ,QAAS,QAAQ,KAAK,KAAK,OAAQ,KAAK;CAE9E,IAAI,UACF,KACA,UAAU,GACV,UAAU,GACV,UAAU,OACV,UAAU,QACV,CAAC,UAAU,QAAQ,GACnB,CAAC,UAAU,SAAS,GACpB,UAAU,OACV,UAAU,MACZ;AACF;;;;;AAMA,SAAS,aAAa,QAA0C;CAC9D,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,IAAI;GACF,OAAO,QAAQ,SAAS;IACtB,IAAI,MAAM,QAAQ,IAAI;SACjB,uBAAO,IAAI,MAAM,mDAAmD,CAAC;GAC5E,GAAG,WAAW;EAChB,SAAS,KAAK;GACZ,OACE,IAAI,MAAM,sEAAsE,EAC9E,OAAO,IACT,CAAC,CACH;EACF;CACF,CAAC;AACH;;AAGA,SAAS,aAAa,MAA6B;CACjD,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,SAAS,IAAI,WAAW;EAC9B,OAAO,eAAe,QAAQ,OAAO,MAAgB;EACrD,OAAO,gBACL,OAAO,OAAO,yBAAS,IAAI,MAAM,uDAAuD,CAAC;EAC3F,OAAO,cAAc,IAAI;CAC3B,CAAC;AACH;;;;;AAMA,eAAsB,aACpB,KACA,WACA,WACA,SAC6B;CAC7B,MAAM,SAAS,SAAS,cAAc,QAAQ;CAC9C,iBAAiB,KAAK,WAAW,WAAW,QAAQ,OAAO;CAC3D,MAAM,OAAO,MAAM,aAAa,MAAM;CACtC,OAAO;EAAE;EAAM,QAAQ,MAAM,aAAa,IAAI;CAAE;AAClD"}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import _sfc_main$1 from "../icon/icon.js";
|
|
2
|
+
import { cropToResult } from "./draw-crop.js";
|
|
3
|
+
import { useImageLoader } from "./use-image-loader.js";
|
|
4
|
+
import { usePreview } from "./use-preview.js";
|
|
5
|
+
import { SELECTION_HANDLES, useSelection } from "./use-selection.js";
|
|
6
|
+
import { useTransform } from "./use-transform.js";
|
|
7
|
+
import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, normalizeClass, normalizeStyle, openBlock, reactive, renderList, renderSlot, shallowRef, toDisplayString, unref, watch, withCtx } from "vue";
|
|
8
|
+
import { useResizeObserver } from "@veltra/compositions";
|
|
9
|
+
import { bem } from "@veltra/utils";
|
|
10
|
+
import { ArrowUpdown, Refresh, RotateLeft, RotateRight, ZoomIn, ZoomOut } from "@veltra/icons/normal";
|
|
11
|
+
//#region src/components/image-cropper/image-cropper.vue
|
|
12
|
+
const _hoisted_1 = ["onClick"];
|
|
13
|
+
const _hoisted_2 = ["src"];
|
|
14
|
+
const _hoisted_3 = ["data-handle"];
|
|
15
|
+
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
16
|
+
name: "ImageCropper",
|
|
17
|
+
__name: "image-cropper",
|
|
18
|
+
props: {
|
|
19
|
+
src: {},
|
|
20
|
+
aspectRatio: {},
|
|
21
|
+
showToolbar: {
|
|
22
|
+
type: Boolean,
|
|
23
|
+
default: true
|
|
24
|
+
},
|
|
25
|
+
showPreview: {
|
|
26
|
+
type: Boolean,
|
|
27
|
+
default: true
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
emits: ["crop-change"],
|
|
31
|
+
setup(__props, { expose: __expose, emit: __emit }) {
|
|
32
|
+
const props = __props;
|
|
33
|
+
const emit = __emit;
|
|
34
|
+
const cls = bem("image-cropper");
|
|
35
|
+
const canvasRef = shallowRef();
|
|
36
|
+
const selectionRef = shallowRef();
|
|
37
|
+
const previewCanvasRef = shallowRef();
|
|
38
|
+
const { imageUrl, loaded, naturalWidth, naturalHeight, imageEl } = useImageLoader({ src: () => props.src });
|
|
39
|
+
const canvasSize = reactive({
|
|
40
|
+
width: 0,
|
|
41
|
+
height: 0
|
|
42
|
+
});
|
|
43
|
+
/** 宽高比预设,value 为 undefined 表示自由比例 */
|
|
44
|
+
const RATIO_PRESETS = [
|
|
45
|
+
{
|
|
46
|
+
label: "自由",
|
|
47
|
+
value: void 0
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
label: "1:1",
|
|
51
|
+
value: 1
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: "4:3",
|
|
55
|
+
value: 4 / 3
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
label: "16:9",
|
|
59
|
+
value: 16 / 9
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
/** 当前生效的宽高比:工具栏预设可覆盖 prop,prop 变化时同步回来 */
|
|
63
|
+
const currentRatio = shallowRef(props.aspectRatio);
|
|
64
|
+
function setAspectRatio(ratio) {
|
|
65
|
+
currentRatio.value = ratio;
|
|
66
|
+
}
|
|
67
|
+
const { transform, fit, reset, resetTransform, zoomIn, zoomOut, rotate, flip, toImageDelta } = useTransform({
|
|
68
|
+
target: canvasRef,
|
|
69
|
+
canvasSize: () => canvasSize,
|
|
70
|
+
imageWidth: () => naturalWidth.value,
|
|
71
|
+
imageHeight: () => naturalHeight.value,
|
|
72
|
+
onReset: () => initSelection()
|
|
73
|
+
});
|
|
74
|
+
const { selection, initSelection, clearSelection } = useSelection({
|
|
75
|
+
target: selectionRef,
|
|
76
|
+
imageWidth: () => naturalWidth.value,
|
|
77
|
+
imageHeight: () => naturalHeight.value,
|
|
78
|
+
toImageDelta,
|
|
79
|
+
aspectRatio: () => currentRatio.value
|
|
80
|
+
});
|
|
81
|
+
usePreview({
|
|
82
|
+
canvas: previewCanvasRef,
|
|
83
|
+
image: imageEl,
|
|
84
|
+
selection,
|
|
85
|
+
transform,
|
|
86
|
+
canvasSize: () => canvasSize
|
|
87
|
+
});
|
|
88
|
+
useResizeObserver({
|
|
89
|
+
targets: canvasRef,
|
|
90
|
+
onResize([entry]) {
|
|
91
|
+
if (!entry) return;
|
|
92
|
+
canvasSize.width = entry.contentRect.width;
|
|
93
|
+
canvasSize.height = entry.contentRect.height;
|
|
94
|
+
if (loaded.value) fit();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
watch(() => props.src, () => {
|
|
98
|
+
reset();
|
|
99
|
+
clearSelection();
|
|
100
|
+
});
|
|
101
|
+
watch(() => props.aspectRatio, (ratio) => {
|
|
102
|
+
currentRatio.value = ratio;
|
|
103
|
+
});
|
|
104
|
+
watch(loaded, (isLoaded) => {
|
|
105
|
+
if (!isLoaded) return;
|
|
106
|
+
fit();
|
|
107
|
+
initSelection();
|
|
108
|
+
});
|
|
109
|
+
watch([selection, () => ({ ...transform })], () => {
|
|
110
|
+
const sel = selection.value;
|
|
111
|
+
if (!sel) return;
|
|
112
|
+
emit("crop-change", {
|
|
113
|
+
selection: { ...sel },
|
|
114
|
+
transform: {
|
|
115
|
+
rotation: transform.rotation,
|
|
116
|
+
flipX: transform.flipX,
|
|
117
|
+
flipY: transform.flipY
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
/** 输出当前选区裁剪结果:默认原图选区像素,options 传入目标宽 / 高时按该尺寸缩放 */
|
|
122
|
+
async function getResult(options) {
|
|
123
|
+
const img = imageEl.value;
|
|
124
|
+
const sel = selection.value;
|
|
125
|
+
if (!img || !sel) throw new Error("Export crop result failed: image not loaded or no selection");
|
|
126
|
+
return cropToResult(img, sel, transform, options);
|
|
127
|
+
}
|
|
128
|
+
__expose({ getResult });
|
|
129
|
+
/** 舞台样式:图片与选区层共用同一变换,选区以图片像素坐标定位 */
|
|
130
|
+
const stageStyle = computed(() => {
|
|
131
|
+
const { translateX, translateY, rotation, scale, flipX, flipY } = transform;
|
|
132
|
+
return {
|
|
133
|
+
width: `${naturalWidth.value}px`,
|
|
134
|
+
height: `${naturalHeight.value}px`,
|
|
135
|
+
transform: `translate(${translateX}px, ${translateY}px) rotate(${rotation}deg) scale(${(flipX ? -1 : 1) * scale}, ${(flipY ? -1 : 1) * scale})`,
|
|
136
|
+
"--u-image-cropper-inv-scale": String(1 / scale)
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
const selectionStyle = computed(() => {
|
|
140
|
+
const sel = selection.value;
|
|
141
|
+
if (!sel) return {};
|
|
142
|
+
return {
|
|
143
|
+
left: `${sel.x}px`,
|
|
144
|
+
top: `${sel.y}px`,
|
|
145
|
+
width: `${sel.width}px`,
|
|
146
|
+
height: `${sel.height}px`
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
/** 选区外上 / 下 / 左 / 右四向遮罩 */
|
|
150
|
+
const maskStyles = computed(() => {
|
|
151
|
+
const sel = selection.value;
|
|
152
|
+
if (!sel) return [];
|
|
153
|
+
const W = naturalWidth.value;
|
|
154
|
+
const H = naturalHeight.value;
|
|
155
|
+
const right = sel.x + sel.width;
|
|
156
|
+
const bottom = sel.y + sel.height;
|
|
157
|
+
return [
|
|
158
|
+
{
|
|
159
|
+
left: "0px",
|
|
160
|
+
top: "0px",
|
|
161
|
+
width: `${W}px`,
|
|
162
|
+
height: `${sel.y}px`
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
left: "0px",
|
|
166
|
+
top: `${bottom}px`,
|
|
167
|
+
width: `${W}px`,
|
|
168
|
+
height: `${H - bottom}px`
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
left: "0px",
|
|
172
|
+
top: `${sel.y}px`,
|
|
173
|
+
width: `${sel.x}px`,
|
|
174
|
+
height: `${sel.height}px`
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
left: `${right}px`,
|
|
178
|
+
top: `${sel.y}px`,
|
|
179
|
+
width: `${W - right}px`,
|
|
180
|
+
height: `${sel.height}px`
|
|
181
|
+
}
|
|
182
|
+
];
|
|
183
|
+
});
|
|
184
|
+
return (_ctx, _cache) => {
|
|
185
|
+
return openBlock(), createElementBlock("div", { class: normalizeClass(unref(cls).b) }, [
|
|
186
|
+
__props.showToolbar ? (openBlock(), createElementBlock("div", {
|
|
187
|
+
key: 0,
|
|
188
|
+
class: normalizeClass(unref(cls).e("toolbar"))
|
|
189
|
+
}, [renderSlot(_ctx.$slots, "toolbar", {
|
|
190
|
+
zoomIn: unref(zoomIn),
|
|
191
|
+
zoomOut: unref(zoomOut),
|
|
192
|
+
rotate: unref(rotate),
|
|
193
|
+
flip: unref(flip),
|
|
194
|
+
reset: unref(resetTransform),
|
|
195
|
+
setAspectRatio,
|
|
196
|
+
aspectRatio: currentRatio.value
|
|
197
|
+
}, () => [
|
|
198
|
+
createCommentVNode(" 宽高比预设 "),
|
|
199
|
+
createElementVNode("div", { class: normalizeClass(unref(cls).e("tool-group")) }, [(openBlock(), createElementBlock(Fragment, null, renderList(RATIO_PRESETS, (preset) => {
|
|
200
|
+
return createElementVNode("button", {
|
|
201
|
+
key: preset.label,
|
|
202
|
+
type: "button",
|
|
203
|
+
class: normalizeClass([unref(cls).e("tool"), unref(bem).is("active", preset.value === currentRatio.value)]),
|
|
204
|
+
onClick: ($event) => setAspectRatio(preset.value)
|
|
205
|
+
}, toDisplayString(preset.label), 11, _hoisted_1);
|
|
206
|
+
}), 64))], 2),
|
|
207
|
+
createCommentVNode(" 缩放 / 旋转 / 翻转 / 重置 "),
|
|
208
|
+
createElementVNode("div", { class: normalizeClass(unref(cls).e("tool-group")) }, [
|
|
209
|
+
createElementVNode("button", {
|
|
210
|
+
type: "button",
|
|
211
|
+
class: normalizeClass([unref(cls).e("tool"), unref(cls).em("tool", "icon")]),
|
|
212
|
+
"aria-label": "缩小",
|
|
213
|
+
title: "缩小",
|
|
214
|
+
onClick: _cache[0] || (_cache[0] = ($event) => unref(zoomOut)())
|
|
215
|
+
}, [createVNode(unref(_sfc_main$1), { size: 15 }, {
|
|
216
|
+
default: withCtx(() => [createVNode(unref(ZoomOut))]),
|
|
217
|
+
_: 1
|
|
218
|
+
})], 2),
|
|
219
|
+
createElementVNode("button", {
|
|
220
|
+
type: "button",
|
|
221
|
+
class: normalizeClass([unref(cls).e("tool"), unref(cls).em("tool", "icon")]),
|
|
222
|
+
"aria-label": "放大",
|
|
223
|
+
title: "放大",
|
|
224
|
+
onClick: _cache[1] || (_cache[1] = ($event) => unref(zoomIn)())
|
|
225
|
+
}, [createVNode(unref(_sfc_main$1), { size: 15 }, {
|
|
226
|
+
default: withCtx(() => [createVNode(unref(ZoomIn))]),
|
|
227
|
+
_: 1
|
|
228
|
+
})], 2),
|
|
229
|
+
createElementVNode("button", {
|
|
230
|
+
type: "button",
|
|
231
|
+
class: normalizeClass([unref(cls).e("tool"), unref(cls).em("tool", "icon")]),
|
|
232
|
+
"aria-label": "逆时针旋转 90°",
|
|
233
|
+
title: "逆时针旋转 90°",
|
|
234
|
+
onClick: _cache[2] || (_cache[2] = ($event) => unref(rotate)(-1))
|
|
235
|
+
}, [createVNode(unref(_sfc_main$1), { size: 15 }, {
|
|
236
|
+
default: withCtx(() => [createVNode(unref(RotateLeft))]),
|
|
237
|
+
_: 1
|
|
238
|
+
})], 2),
|
|
239
|
+
createElementVNode("button", {
|
|
240
|
+
type: "button",
|
|
241
|
+
class: normalizeClass([unref(cls).e("tool"), unref(cls).em("tool", "icon")]),
|
|
242
|
+
"aria-label": "顺时针旋转 90°",
|
|
243
|
+
title: "顺时针旋转 90°",
|
|
244
|
+
onClick: _cache[3] || (_cache[3] = ($event) => unref(rotate)(1))
|
|
245
|
+
}, [createVNode(unref(_sfc_main$1), { size: 15 }, {
|
|
246
|
+
default: withCtx(() => [createVNode(unref(RotateRight))]),
|
|
247
|
+
_: 1
|
|
248
|
+
})], 2),
|
|
249
|
+
createElementVNode("button", {
|
|
250
|
+
type: "button",
|
|
251
|
+
class: normalizeClass([unref(cls).e("tool"), unref(cls).em("tool", "icon")]),
|
|
252
|
+
"aria-label": "水平翻转",
|
|
253
|
+
title: "水平翻转",
|
|
254
|
+
onClick: _cache[4] || (_cache[4] = ($event) => unref(flip)("horizontal"))
|
|
255
|
+
}, [createVNode(unref(_sfc_main$1), {
|
|
256
|
+
size: 15,
|
|
257
|
+
class: normalizeClass(unref(cls).e("flip-icon"))
|
|
258
|
+
}, {
|
|
259
|
+
default: withCtx(() => [createVNode(unref(ArrowUpdown))]),
|
|
260
|
+
_: 1
|
|
261
|
+
}, 8, ["class"])], 2),
|
|
262
|
+
createElementVNode("button", {
|
|
263
|
+
type: "button",
|
|
264
|
+
class: normalizeClass([unref(cls).e("tool"), unref(cls).em("tool", "icon")]),
|
|
265
|
+
"aria-label": "垂直翻转",
|
|
266
|
+
title: "垂直翻转",
|
|
267
|
+
onClick: _cache[5] || (_cache[5] = ($event) => unref(flip)("vertical"))
|
|
268
|
+
}, [createVNode(unref(_sfc_main$1), { size: 15 }, {
|
|
269
|
+
default: withCtx(() => [createVNode(unref(ArrowUpdown))]),
|
|
270
|
+
_: 1
|
|
271
|
+
})], 2),
|
|
272
|
+
createElementVNode("button", {
|
|
273
|
+
type: "button",
|
|
274
|
+
class: normalizeClass([unref(cls).e("tool"), unref(cls).em("tool", "icon")]),
|
|
275
|
+
"aria-label": "重置",
|
|
276
|
+
title: "重置",
|
|
277
|
+
onClick: _cache[6] || (_cache[6] = ($event) => unref(resetTransform)())
|
|
278
|
+
}, [createVNode(unref(_sfc_main$1), { size: 15 }, {
|
|
279
|
+
default: withCtx(() => [createVNode(unref(Refresh))]),
|
|
280
|
+
_: 1
|
|
281
|
+
})], 2)
|
|
282
|
+
], 2)
|
|
283
|
+
])], 2)) : createCommentVNode("v-if", true),
|
|
284
|
+
createElementVNode("div", {
|
|
285
|
+
class: normalizeClass(unref(cls).e("canvas")),
|
|
286
|
+
ref_key: "canvasRef",
|
|
287
|
+
ref: canvasRef
|
|
288
|
+
}, [unref(loaded) ? (openBlock(), createElementBlock("div", {
|
|
289
|
+
key: 0,
|
|
290
|
+
class: normalizeClass(unref(cls).e("stage")),
|
|
291
|
+
style: normalizeStyle(stageStyle.value)
|
|
292
|
+
}, [createElementVNode("img", {
|
|
293
|
+
class: normalizeClass(unref(cls).e("image")),
|
|
294
|
+
src: unref(imageUrl),
|
|
295
|
+
draggable: "false",
|
|
296
|
+
alt: ""
|
|
297
|
+
}, null, 10, _hoisted_2), unref(selection) ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
|
|
298
|
+
createCommentVNode(" 选区外半透明遮罩 "),
|
|
299
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(maskStyles.value, (style, i) => {
|
|
300
|
+
return openBlock(), createElementBlock("div", {
|
|
301
|
+
key: i,
|
|
302
|
+
class: normalizeClass(unref(cls).e("mask")),
|
|
303
|
+
style: normalizeStyle(style)
|
|
304
|
+
}, null, 6);
|
|
305
|
+
}), 128)),
|
|
306
|
+
createCommentVNode(" 裁剪选区:整体拖动 + 8 个手柄调整大小 "),
|
|
307
|
+
createElementVNode("div", {
|
|
308
|
+
class: normalizeClass(unref(cls).e("selection")),
|
|
309
|
+
ref_key: "selectionRef",
|
|
310
|
+
ref: selectionRef,
|
|
311
|
+
style: normalizeStyle(selectionStyle.value)
|
|
312
|
+
}, [
|
|
313
|
+
createCommentVNode(" 3×3 网格线,顺序对应 style.scss 中 nth-child 的定位 "),
|
|
314
|
+
(openBlock(), createElementBlock(Fragment, null, renderList(4, (i) => {
|
|
315
|
+
return createElementVNode("i", {
|
|
316
|
+
key: i,
|
|
317
|
+
class: normalizeClass(unref(cls).e("grid-line"))
|
|
318
|
+
}, null, 2);
|
|
319
|
+
}), 64)),
|
|
320
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(unref(SELECTION_HANDLES), (handle) => {
|
|
321
|
+
return openBlock(), createElementBlock("i", {
|
|
322
|
+
key: handle,
|
|
323
|
+
class: normalizeClass(unref(cls).e("handle")),
|
|
324
|
+
"data-handle": handle
|
|
325
|
+
}, null, 10, _hoisted_3);
|
|
326
|
+
}), 128))
|
|
327
|
+
], 6)
|
|
328
|
+
], 64)) : createCommentVNode("v-if", true)], 6)) : createCommentVNode("v-if", true)], 2),
|
|
329
|
+
__props.showPreview && unref(loaded) ? (openBlock(), createElementBlock("div", {
|
|
330
|
+
key: 1,
|
|
331
|
+
class: normalizeClass(unref(cls).e("preview"))
|
|
332
|
+
}, [createElementVNode("canvas", {
|
|
333
|
+
class: normalizeClass(unref(cls).e("preview-canvas")),
|
|
334
|
+
ref_key: "previewCanvasRef",
|
|
335
|
+
ref: previewCanvasRef
|
|
336
|
+
}, null, 2)], 2)) : createCommentVNode("v-if", true)
|
|
337
|
+
], 2);
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
//#endregion
|
|
342
|
+
export { _sfc_main as default };
|
|
343
|
+
|
|
344
|
+
//# sourceMappingURL=image-cropper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-cropper.js","names":[],"sources":["../../../src/components/image-cropper/image-cropper.vue"],"sourcesContent":["<template>\n <div :class=\"cls.b\">\n <div v-if=\"showToolbar\" :class=\"cls.e('toolbar')\">\n <slot\n name=\"toolbar\"\n :zoom-in=\"zoomIn\"\n :zoom-out=\"zoomOut\"\n :rotate=\"rotate\"\n :flip=\"flip\"\n :reset=\"resetTransform\"\n :set-aspect-ratio=\"setAspectRatio\"\n :aspect-ratio=\"currentRatio\"\n >\n <!-- 宽高比预设 -->\n <div :class=\"cls.e('tool-group')\">\n <button\n v-for=\"preset in RATIO_PRESETS\"\n :key=\"preset.label\"\n type=\"button\"\n :class=\"[cls.e('tool'), bem.is('active', preset.value === currentRatio)]\"\n @click=\"setAspectRatio(preset.value)\"\n >\n {{ preset.label }}\n </button>\n </div>\n\n <!-- 缩放 / 旋转 / 翻转 / 重置 -->\n <div :class=\"cls.e('tool-group')\">\n <button\n type=\"button\"\n :class=\"[cls.e('tool'), cls.em('tool', 'icon')]\"\n aria-label=\"缩小\"\n title=\"缩小\"\n @click=\"zoomOut()\"\n >\n <u-icon :size=\"15\"><ZoomOut /></u-icon>\n </button>\n <button\n type=\"button\"\n :class=\"[cls.e('tool'), cls.em('tool', 'icon')]\"\n aria-label=\"放大\"\n title=\"放大\"\n @click=\"zoomIn()\"\n >\n <u-icon :size=\"15\"><ZoomIn /></u-icon>\n </button>\n <button\n type=\"button\"\n :class=\"[cls.e('tool'), cls.em('tool', 'icon')]\"\n aria-label=\"逆时针旋转 90°\"\n title=\"逆时针旋转 90°\"\n @click=\"rotate(-1)\"\n >\n <u-icon :size=\"15\"><RotateLeft /></u-icon>\n </button>\n <button\n type=\"button\"\n :class=\"[cls.e('tool'), cls.em('tool', 'icon')]\"\n aria-label=\"顺时针旋转 90°\"\n title=\"顺时针旋转 90°\"\n @click=\"rotate(1)\"\n >\n <u-icon :size=\"15\"><RotateRight /></u-icon>\n </button>\n <button\n type=\"button\"\n :class=\"[cls.e('tool'), cls.em('tool', 'icon')]\"\n aria-label=\"水平翻转\"\n title=\"水平翻转\"\n @click=\"flip('horizontal')\"\n >\n <u-icon :size=\"15\" :class=\"cls.e('flip-icon')\"><ArrowUpdown /></u-icon>\n </button>\n <button\n type=\"button\"\n :class=\"[cls.e('tool'), cls.em('tool', 'icon')]\"\n aria-label=\"垂直翻转\"\n title=\"垂直翻转\"\n @click=\"flip('vertical')\"\n >\n <u-icon :size=\"15\"><ArrowUpdown /></u-icon>\n </button>\n <button\n type=\"button\"\n :class=\"[cls.e('tool'), cls.em('tool', 'icon')]\"\n aria-label=\"重置\"\n title=\"重置\"\n @click=\"resetTransform()\"\n >\n <u-icon :size=\"15\"><Refresh /></u-icon>\n </button>\n </div>\n </slot>\n </div>\n\n <div :class=\"cls.e('canvas')\" ref=\"canvasRef\">\n <div v-if=\"loaded\" :class=\"cls.e('stage')\" :style=\"stageStyle\">\n <img :class=\"cls.e('image')\" :src=\"imageUrl\" draggable=\"false\" alt=\"\" />\n\n <template v-if=\"selection\">\n <!-- 选区外半透明遮罩 -->\n <div v-for=\"(style, i) in maskStyles\" :key=\"i\" :class=\"cls.e('mask')\" :style=\"style\" />\n\n <!-- 裁剪选区:整体拖动 + 8 个手柄调整大小 -->\n <div :class=\"cls.e('selection')\" ref=\"selectionRef\" :style=\"selectionStyle\">\n <!-- 3×3 网格线,顺序对应 style.scss 中 nth-child 的定位 -->\n <i v-for=\"i in 4\" :key=\"i\" :class=\"cls.e('grid-line')\" />\n <i\n v-for=\"handle in SELECTION_HANDLES\"\n :key=\"handle\"\n :class=\"cls.e('handle')\"\n :data-handle=\"handle\"\n />\n </div>\n </template>\n </div>\n </div>\n\n <div v-if=\"showPreview && loaded\" :class=\"cls.e('preview')\">\n <canvas :class=\"cls.e('preview-canvas')\" ref=\"previewCanvasRef\" />\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useResizeObserver } from '@veltra/compositions'\nimport {\n ArrowUpdown,\n Refresh,\n RotateLeft,\n RotateRight,\n ZoomIn,\n ZoomOut\n} from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, reactive, shallowRef, watch } from 'vue'\n\nimport type {\n ImageCropperProps,\n ImageCropperEmits,\n ImageCropperResult,\n ImageCropperResultOptions,\n _ImageCropperExposed\n} from '../../types'\nimport { UIcon } from '../icon'\nimport { cropToResult } from './draw-crop'\nimport { useImageLoader } from './use-image-loader'\nimport { usePreview } from './use-preview'\nimport { SELECTION_HANDLES, useSelection } from './use-selection'\nimport { useTransform } from './use-transform'\n\ndefineOptions({ name: 'ImageCropper' })\n\nconst props = withDefaults(defineProps<ImageCropperProps>(), {\n showToolbar: true,\n showPreview: true\n})\n\nconst emit = defineEmits<ImageCropperEmits>()\n\nconst cls = bem('image-cropper')\n\nconst canvasRef = shallowRef<HTMLElement>()\nconst selectionRef = shallowRef<HTMLElement>()\nconst previewCanvasRef = shallowRef<HTMLCanvasElement>()\n\nconst { imageUrl, loaded, naturalWidth, naturalHeight, imageEl } = useImageLoader({\n src: () => props.src\n})\n\nconst canvasSize = reactive({ width: 0, height: 0 })\n\n/** 宽高比预设,value 为 undefined 表示自由比例 */\nconst RATIO_PRESETS: { label: string; value?: number }[] = [\n { label: '自由', value: undefined },\n { label: '1:1', value: 1 },\n { label: '4:3', value: 4 / 3 },\n { label: '16:9', value: 16 / 9 }\n]\n\n/** 当前生效的宽高比:工具栏预设可覆盖 prop,prop 变化时同步回来 */\nconst currentRatio = shallowRef(props.aspectRatio)\n\nfunction setAspectRatio(ratio: number | undefined) {\n currentRatio.value = ratio\n}\n\nconst { transform, fit, reset, resetTransform, zoomIn, zoomOut, rotate, flip, toImageDelta } =\n useTransform({\n target: canvasRef,\n canvasSize: () => canvasSize,\n imageWidth: () => naturalWidth.value,\n imageHeight: () => naturalHeight.value,\n onReset: () => initSelection()\n })\n\nconst { selection, initSelection, clearSelection } = useSelection({\n target: selectionRef,\n imageWidth: () => naturalWidth.value,\n imageHeight: () => naturalHeight.value,\n toImageDelta,\n aspectRatio: () => currentRatio.value\n})\n\nusePreview({\n canvas: previewCanvasRef,\n image: imageEl,\n selection,\n transform,\n canvasSize: () => canvasSize\n})\n\nuseResizeObserver({\n targets: canvasRef,\n onResize([entry]) {\n if (!entry) return\n canvasSize.width = entry.contentRect.width\n canvasSize.height = entry.contentRect.height\n if (loaded.value) fit()\n }\n})\n\nwatch(\n () => props.src,\n () => {\n reset()\n clearSelection()\n }\n)\n\nwatch(\n () => props.aspectRatio,\n (ratio) => {\n currentRatio.value = ratio\n }\n)\n\nwatch(loaded, (isLoaded) => {\n if (!isLoaded) return\n fit()\n initSelection()\n})\n\n// 选区或图片变换变化时发出裁剪变化事件,载荷为当前选区(图片像素坐标)与变换摘要\nwatch([selection, () => ({ ...transform })], () => {\n const sel = selection.value\n if (!sel) return\n emit('crop-change', {\n selection: { ...sel },\n transform: { rotation: transform.rotation, flipX: transform.flipX, flipY: transform.flipY }\n })\n})\n\n/** 输出当前选区裁剪结果:默认原图选区像素,options 传入目标宽 / 高时按该尺寸缩放 */\nasync function getResult(options?: ImageCropperResultOptions): Promise<ImageCropperResult> {\n const img = imageEl.value\n const sel = selection.value\n if (!img || !sel) throw new Error('Export crop result failed: image not loaded or no selection')\n return cropToResult(img, sel, transform, options)\n}\n\ndefineExpose<_ImageCropperExposed>({ getResult })\n\n/** 舞台样式:图片与选区层共用同一变换,选区以图片像素坐标定位 */\nconst stageStyle = computed(() => {\n const { translateX, translateY, rotation, scale, flipX, flipY } = transform\n return {\n width: `${naturalWidth.value}px`,\n height: `${naturalHeight.value}px`,\n transform: `translate(${translateX}px, ${translateY}px) rotate(${rotation}deg) scale(${(flipX ? -1 : 1) * scale}, ${(flipY ? -1 : 1) * scale})`,\n // 逆缩放系数:选区边框 / 网格线 / 手柄在缩放后保持屏幕恒定尺寸\n '--u-image-cropper-inv-scale': String(1 / scale)\n }\n})\n\nconst selectionStyle = computed(() => {\n const sel = selection.value\n if (!sel) return {}\n return {\n left: `${sel.x}px`,\n top: `${sel.y}px`,\n width: `${sel.width}px`,\n height: `${sel.height}px`\n }\n})\n\n/** 选区外上 / 下 / 左 / 右四向遮罩 */\nconst maskStyles = computed(() => {\n const sel = selection.value\n if (!sel) return []\n const W = naturalWidth.value\n const H = naturalHeight.value\n const right = sel.x + sel.width\n const bottom = sel.y + sel.height\n return [\n { left: '0px', top: '0px', width: `${W}px`, height: `${sel.y}px` },\n { left: '0px', top: `${bottom}px`, width: `${W}px`, height: `${H - bottom}px` },\n { left: '0px', top: `${sel.y}px`, width: `${sel.x}px`, height: `${sel.height}px` },\n { left: `${right}px`, top: `${sel.y}px`, width: `${W - right}px`, height: `${sel.height}px` }\n ]\n})\n</script>\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./style2.css";
|