From f17e1c0f4c349065f63445c961b9bec3b82bec1c Mon Sep 17 00:00:00 2001 From: qiang Date: Fri, 9 Jul 2021 16:12:37 +0800 Subject: [PATCH] feat(App): cover-view, cover-image --- packages/uni-app-plus/src/helpers/useCover.ts | 203 ++++++++++++++++++ .../src/view/components/cover-image/index.tsx | 125 +++++++++++ .../src/view/components/cover-view/index.tsx | 37 ++++ packages/uni-components/src/index.ts | 1 + 4 files changed, 366 insertions(+) create mode 100644 packages/uni-app-plus/src/helpers/useCover.ts create mode 100644 packages/uni-app-plus/src/view/components/cover-image/index.tsx create mode 100644 packages/uni-app-plus/src/view/components/cover-view/index.tsx diff --git a/packages/uni-app-plus/src/helpers/useCover.ts b/packages/uni-app-plus/src/helpers/useCover.ts new file mode 100644 index 000000000..7bc0c73f9 --- /dev/null +++ b/packages/uni-app-plus/src/helpers/useCover.ts @@ -0,0 +1,203 @@ +import { computed, Ref, reactive, watch } from 'vue' +import { CustomEventTrigger } from '@dcloudio/uni-components' +import { Position, useNative } from './useNative' + +let id = 0 + +export function useCover( + rootRef: Ref, + trigger: CustomEventTrigger, + content: { src?: string; text?: string } +) { + const { position, hidden, onParentReady } = useNative(rootRef) + onParentReady((parentPosition) => { + const viewPosition = computed(() => { + const object: Position = {} as Position + for (const key in position) { + let val = position[key as keyof Position] + const valNumber = parseFloat(val) + const parentValNumber = parseFloat( + parentPosition[key as keyof Position] + ) + if (key === 'top' || key === 'left') { + val = Math.max(valNumber, parentValNumber) + 'px' + } else if (key === 'width' || key === 'height') { + const base = key === 'width' ? 'left' : 'top' + const parentStart = parseFloat(parentPosition[base]) + const viewStart = parseFloat(position[base]) + const diff1 = Math.max(parentStart - viewStart, 0) + const diff2 = Math.max( + viewStart + valNumber - (parentStart + parentValNumber), + 0 + ) + val = Math.max(valNumber - diff1 - diff2, 0) + 'px' + } + object[key as keyof Position] = val as any + } + return object + }) + + const baseStyle: Array = [ + 'borderRadius', + 'borderColor', + 'borderWidth', + 'backgroundColor', + ] + const textStyle: Array = [ + 'paddingTop', + 'paddingRight', + 'paddingBottom', + 'paddingLeft', + 'color', + 'textAlign', + 'lineHeight', + 'fontSize', + 'fontWeight', + 'textOverflow', + 'whiteSpace', + ] + const imageStyle: Array = [] + const textAlign = { start: 'left', end: 'right' } + function updateStyle(style: Record) { + const computedStyle = getComputedStyle(rootRef.value as HTMLElement) + baseStyle.concat(textStyle, imageStyle).forEach((key) => { + style[key] = computedStyle[key] + }) + return style + } + const style = reactive(updateStyle({} as CSSStyleDeclaration)) + let request: null | number = null + function requestStyleUpdate() { + if (request) { + cancelAnimationFrame(request) + } + request = requestAnimationFrame(() => { + request = null + updateStyle(style) + }) + } + window.addEventListener('updateview', requestStyleUpdate) + + function getTagPosition() { + const position: Partial = {} + for (const key in position) { + let val = position[key as keyof Position]! + if (key === 'top' || key === 'left') { + val = + Math.min(parseFloat(val) - parseFloat(parentPosition[key]), 0) + + 'px' + } + position[key as keyof Position] = val as any + } + return position + } + + const tags = computed(() => { + const position: Position = getTagPosition() as Position + const tags: PlusNativeObjViewDrawTagStyles[] = [ + { + tag: 'rect', + position, + rectStyles: { + color: style.backgroundColor, + radius: style.borderRadius, + borderColor: style.borderColor, + borderWidth: style.borderWidth, + }, + }, + ] + + if ('src' in content) { + if (content.src) { + tags.push({ + tag: 'img', + position, + src: content.src, + }) + } + } else { + const lineSpacing = + parseFloat(style.lineHeight) - parseFloat(style.fontSize) + let width = + parseFloat(position.width) - + parseFloat(style.paddingLeft) - + parseFloat(style.paddingRight) + width = width < 0 ? 0 : width + let height = + parseFloat(position.height) - + parseFloat(style.paddingTop) - + lineSpacing / 2 - + parseFloat(style.paddingBottom) + height = height < 0 ? 0 : height + tags.push({ + tag: 'font', + position: { + top: `${ + parseFloat(position.top) + + parseFloat(style.paddingTop) + + lineSpacing / 2 + }px`, + left: `${ + parseFloat(position.left) + parseFloat(style.paddingLeft) + }px`, + width: `${width}px`, + height: `${height}px`, + }, + textStyles: { + align: + textAlign[style.textAlign as keyof typeof textAlign] || + style.textAlign, + color: style.color, + decoration: 'none', + lineSpacing: `${lineSpacing}px`, + margin: '0px', + overflow: style.textOverflow, + size: style.fontSize, + verticalAlign: 'top', + weight: style.fontWeight, + whiteSpace: style.whiteSpace, + }, + text: content.text, + }) + } + + return tags + }) + + const cover = new plus.nativeObj.View!( + `cover-${Date.now()}-${id++}`, + viewPosition.value, + tags.value + ) + plus.webview.currentWebview().append(cover) + if (hidden.value) { + cover.hide() + } + + cover.addEventListener('click', () => { + trigger('click', {} as Event, {}) + }) + + watch( + () => hidden.value, + (val) => { + cover[val ? 'hide' : 'show']() + } + ) + watch( + () => viewPosition.value, + (val) => { + cover.setStyle(val) + }, + { deep: true } + ) + watch( + () => tags.value, + () => { + cover.reset() + cover.draw(tags.value) + }, + { deep: true } + ) + }) +} diff --git a/packages/uni-app-plus/src/view/components/cover-image/index.tsx b/packages/uni-app-plus/src/view/components/cover-image/index.tsx new file mode 100644 index 000000000..3ea41a53f --- /dev/null +++ b/packages/uni-app-plus/src/view/components/cover-image/index.tsx @@ -0,0 +1,125 @@ +import { + Ref, + ref, + reactive, + ExtractPropTypes, + watch, + onBeforeUnmount, +} from 'vue' +import { plusReady } from '@dcloudio/uni-shared' +import { + defineBuiltInComponent, + useCustomEvent, + CustomEventTrigger, + EmitEvent, +} from '@dcloudio/uni-components' +import { useCover } from '../../../helpers/useCover' +import { getRealPath } from '../../../platform/getRealPath' +// TODO 从 service 层传入 +const TEMP_PATH = '_doc/uniapp_temp/' + +const props = { + src: { + type: String, + default: '', + }, + autoSize: { + type: [Boolean, String], + default: false, + }, +} +type Props = ExtractPropTypes + +function useImageLoad( + props: Props, + content: { src: string }, + trigger: CustomEventTrigger +) { + const style = ref('') + let downloaTask: PlusDownloaderDownload + function loadImage() { + content.src = '' + style.value = props.autoSize ? 'width:0;height:0;' : '' + const realPath = props.src ? getRealPath(props.src) : '' + if ( + realPath.indexOf('http://') === 0 || + realPath.indexOf('https://') === 0 + ) { + plusReady(() => { + downloaTask = plus.downloader.createDownload( + realPath, + { + filename: TEMP_PATH + '/download/', + }, + (task, status) => { + if (status === 200) { + getImageInfo(task.filename!) + } else { + trigger('error', {} as Event, { + errMsg: 'error', + }) + } + } + ) + downloaTask.start() + }) + } else if (realPath) { + getImageInfo(realPath) + } + } + + function getImageInfo(src: string) { + content.src = src + plusReady(() => { + plus.io.getImageInfo({ + src, + success: ({ width, height }) => { + if (props.autoSize) { + style.value = `width:${width}px;height:${height}px;` + window.dispatchEvent(new CustomEvent('updateview')) + } + trigger('load', {} as Event, { width, height }) + }, + fail: () => { + trigger('error', {} as Event, { + errMsg: 'error', + }) + }, + }) + }) + } + + if (props.src) { + loadImage() + } + watch(() => props.src, loadImage) + + onBeforeUnmount(() => { + if (downloaTask) { + downloaTask.abort() + } + }) + + return style +} + +export default /*#__PURE__*/ defineBuiltInComponent({ + name: 'CoverImage', + props, + emits: ['click', 'load', 'error'], + setup(props, { emit }) { + const rootRef: Ref = ref(null) + const trigger = useCustomEvent>(rootRef, emit) + let content = reactive({ src: '' }) + const style = useImageLoad(props, content, trigger) + useCover(rootRef, trigger, content) + + return () => { + return ( + +
+
+ ) + } + }, +}) diff --git a/packages/uni-app-plus/src/view/components/cover-view/index.tsx b/packages/uni-app-plus/src/view/components/cover-view/index.tsx new file mode 100644 index 000000000..34cc626dd --- /dev/null +++ b/packages/uni-app-plus/src/view/components/cover-view/index.tsx @@ -0,0 +1,37 @@ +import { Ref, ref, reactive } from 'vue' +import { + defineBuiltInComponent, + useCustomEvent, + EmitEvent, + flatVNode, + Text, +} from '@dcloudio/uni-components' +import { useCover } from '../../../helpers/useCover' + +export default /*#__PURE__*/ defineBuiltInComponent({ + name: 'CoverView', + emits: ['click'], + setup(_, { emit, slots }) { + const rootRef: Ref = ref(null) + const trigger = useCustomEvent>(rootRef, emit) + let content = reactive({ text: '' }) + + useCover(rootRef, trigger, content) + + return () => { + const defaultSlots = slots.default ? flatVNode(slots.default()) : [] + let text = '' + defaultSlots.forEach((node) => { + if (!node.type === Text) { + text += node.children || '' + } + }) + content.text = text + return ( + +
{text}
+
+ ) + } + }, +}) diff --git a/packages/uni-components/src/index.ts b/packages/uni-components/src/index.ts index 1e767545e..84d3b1367 100644 --- a/packages/uni-components/src/index.ts +++ b/packages/uni-components/src/index.ts @@ -20,5 +20,6 @@ export { defineBuiltInComponent, defineSystemComponent, } from './helpers/component' +export { flatVNode } from './helpers/flatVNode' export { uniFormKey } from './components/form' export type { UniFormCtx } from './components/form' -- GitLab