提交 ba3de478 编写于 作者: fxy060608's avatar fxy060608

feat: Image

上级 ceb5c350
......@@ -69,7 +69,7 @@
"size-limit": "^4.10.1",
"ts-jest": "^26.4.4",
"typescript": "~4.1.3",
"vite": "^2.1.5",
"vite": "^2.2.1",
"vue": "3.0.11",
"yorkie": "^2.0.0"
}
......
import {
ref,
defineComponent,
computed,
Ref,
watchEffect,
watch,
onMounted,
computed,
reactive,
onMounted,
onBeforeUnmount,
defineComponent,
ExtractPropTypes,
} from 'vue'
import { getRealPath } from '@dcloudio/uni-platform'
import { useCustomEvent } from '../../helpers/useEvent'
import { CustomEventTrigger, useCustomEvent } from '../../helpers/useEvent'
import ResizeSensor from '../resize-sensor/index.vue'
export default /*#__PURE__*/ defineComponent({
name: 'Image',
props: {
const props = {
src: {
type: String,
default: '',
......@@ -31,127 +30,213 @@ export default /*#__PURE__*/ defineComponent({
type: Boolean,
default: true,
},
},
}
type ImageProps = ExtractPropTypes<typeof props>
type ImageState = ReturnType<typeof useImageState>
type FixSize = ReturnType<typeof useImageSize>['fixSize']
type ResetSize = ReturnType<typeof useImageSize>['resetSize']
const FIX_MODES = {
widthFix: ['width', 'height'],
heightFix: ['height', 'width'],
}
const IMAGE_MODES = {
aspectFit: ['center center', 'contain'],
aspectFill: ['center center', 'cover'],
widthFix: [, '100% 100%'],
heightFix: [, '100% 100%'],
top: ['center top'],
bottom: ['center bottom'],
center: ['center center'],
left: ['left center'],
right: ['right center'],
'top left': ['left top'],
'top right': ['right top'],
'bottom left': ['left bottom'],
'bottom right': ['right bottom'],
}
export default /*#__PURE__*/ defineComponent({
name: 'Image',
props,
setup(props, { emit }) {
const rootRef = ref(null)
const src = useImageSrc(props)
const modeStyle = useImageMode(props, src)
const rootRef = ref<HTMLElement | null>(null)
const state = useImageState(rootRef, props)
const trigger = useCustomEvent(rootRef, emit)
const { fixSize, resetSize } = useImageSize(rootRef, props, state)
useImageLoader(state, {
trigger,
fixSize,
resetSize,
})
return () => {
const { mode } = props
const imgSrc = src.value
const { imgSrc, modeStyle } = state
return (
<uni-image ref={rootRef}>
<div ref="content" style={modeStyle.value} />
{imgSrc && <img src={imgSrc} />}
{(mode === 'widthFix' || mode === 'heightFix') && <ResizeSensor />}
<div style={modeStyle} />
{imgSrc && <img src={imgSrc} draggable={props.draggable} />}
{FIX_MODES[mode as keyof typeof FIX_MODES] && (
<ResizeSensor onResize={fixSize} />
)}
</uni-image>
)
}
},
})
function useImageData() {
return reactive({
originalWidth: 0,
originalHeight: 0,
originalStyle: { width: '', height: '' },
src: '',
})
}
function loadImage(src: string) {}
function useImageSrc(props: { src: string }) {
const src = computed(() => getRealPath(props.src))
watch(
() => props.src,
() => {
// loadImage
}
)
return src
}
const IMAGE_MODES = {
aspectFit: ['contain', 'center center'],
aspectFill: ['cover', 'center center'],
widthFix: ['100% 100%'],
heightFix: ['100% 100%'],
top: [, 'center top'],
bottom: [, 'center bottom'],
center: [, 'center center'],
left: [, 'left center'],
right: [, 'right center'],
'top left': [, 'left top'],
'top right': [, 'right top'],
'bottom left': [, 'left bottom'],
'bottom right': [, 'right bottom'],
}
function useImageMode(props: { mode: string }, rootRef: Ref, src: Ref<string>) {
const style = computed(() => {
function useImageState(rootRef: Ref<HTMLElement | null>, props: ImageProps) {
const imgSrc = ref('')
const modeStyleRef = computed(() => {
let size = 'auto'
let position = ''
const opts = IMAGE_MODES[props.mode as keyof typeof IMAGE_MODES]
if (opts) {
} else {
size = '100% 100%'
if (!opts) {
position = '0% 0%'
size = '100% 100%'
} else {
opts[0] && (position = opts[0])
opts[1] && (size = opts[1])
}
const srcVal = src.value
const srcVal = imgSrc.value
return `background-image:${
srcVal ? 'url("' + srcVal + '")' : 'none'
};background-position:${position};background-size:${size};background-repeat:no-repeat;`
})
const ratio = ref(0)
const origWidth = ref(0)
const origHeight = ref(0)
const state = reactive({
rootEl: rootRef,
src: computed(() => getRealPath(props.src)),
origWidth: 0,
origHeight: 0,
origStyle: { width: '', height: '' },
modeStyle: modeStyleRef,
imgSrc,
})
onMounted(() => {
const rootVal = rootRef.value as HTMLElement
const style = rootVal.style
origWidth.value = Number(style.width) || 0
origHeight.value = Number(style.height) || 0
const rootEl = rootRef.value!
const style = rootEl!.style
state.origWidth = Number(style.width) || 0
state.origHeight = Number(style.height) || 0
})
watch(
() => props.mode,
() => {
// const { mode } = props
// fixSize(rootRef.value as HTMLElement, props.mode)
// TODO
// resetSize()
return state
}
function useImageLoader(
state: ImageState,
{
trigger,
fixSize,
resetSize,
}: {
fixSize: FixSize
resetSize: ResetSize
trigger: CustomEventTrigger
}
) {
let img: HTMLImageElement | null
const loadImage = (src: string) => {
if (!src) {
resetImage()
resetSize()
return
}
if (!img) {
img = new Image()
}
img.onload = (evt) => {
const { width, height } = img!
state.origWidth = width
state.origHeight = height
state.imgSrc = src
fixSize()
resetImage()
trigger('load', evt, {
width,
height,
})
}
img.onerror = (evt) => {
const { src } = state
state.origWidth = 0
state.origHeight = 0
state.imgSrc = ''
resetImage()
trigger('error', evt as Event, {
errMsg: `GET ${src} 404 (Not Found)`,
})
}
img.src = src
}
const resetImage = () => {
if (img) {
img.onload = null
img.onerror = null
img = null
}
}
watch(
() => state.src,
(value) => loadImage(value)
)
return style
onMounted(() => loadImage(state.src))
onBeforeUnmount(() => resetImage())
}
const isChrome = navigator.vendor === 'Google Inc.'
function fixNumber(num: number) {
// fix: 解决 Chrome 浏览器上某些情况下导致 1px 缝隙的问题
if (typeof navigator && navigator.vendor === 'Google Inc.' && num > 10) {
if (isChrome && num > 10) {
num = Math.round(num / 2) * 2
}
return num
}
function fixSize(el: HTMLElement, mode: string, ratio: number) {
if (!ratio) {
function useImageSize(
rootRef: Ref<HTMLElement | null>,
props: ImageProps,
state: ImageState
) {
const fixSize = () => {
const { mode } = props
const names = FIX_MODES[mode as keyof typeof FIX_MODES]
if (!names) {
return
}
const rect = el.getBoundingClientRect()
if (mode === 'widthFix') {
const width = rect.width
if (width) {
el.style.height = fixNumber(width / ratio) + 'px'
const { origWidth, origHeight } = state
const ratio = origWidth && origHeight ? origWidth / origHeight : 0
if (!ratio) {
return
}
} else if (mode === 'heightFix') {
const height = rect.height
if (height) {
el.style.width = fixNumber(height * ratio) + 'px'
const rootEl = rootRef.value!
const rect = rootEl.getBoundingClientRect()
const value = rect[names[0] as keyof DOMRect] as number
if (value) {
rootEl.style[names[1] as 'height' | 'width'] =
fixNumber(value / ratio) + 'px'
}
}
}
function resetSize(el: HTMLElement, width: string, height: string) {
const style = el.style
const resetSize = () => {
const { style } = rootRef.value!
const {
origStyle: { width, height },
} = state
style.width = width
style.height = height
}
watch(
() => props.mode,
(value, oldValue) => {
if (FIX_MODES[oldValue as keyof typeof FIX_MODES]) {
resetSize()
}
if (FIX_MODES[value as keyof typeof FIX_MODES]) {
fixSize()
}
}
)
return {
fixSize,
resetSize,
}
}
<template>
<uni-image v-bind="$attrs">
<div ref="content" :style="modeStyle" />
<img :src="realImagePath" />
<ResizeSensor v-if="mode === 'widthFix'" ref="sensor" @resize="_resize" />
</uni-image>
</template>
<script>
import ResizeSensor from '../resize-sensor/index.vue'
import { getRealPath } from '@dcloudio/uni-platform'
export default {
name: 'Image',
props: {
src: {
type: String,
default: '',
},
mode: {
type: String,
default: 'scaleToFill',
},
// TODO 懒加载
lazyLoad: {
type: [Boolean, String],
default: false,
},
},
data() {
return {
originalWidth: 0,
originalHeight: 0,
availHeight: '',
}
},
computed: {
ratio() {
return this.originalWidth && this.originalHeight
? this.originalWidth / this.originalHeight
: 0
},
realImagePath() {
return getRealPath(this.src)
},
modeStyle() {
let size = 'auto'
let position = ''
const repeat = 'no-repeat'
switch (this.mode) {
case 'aspectFit':
size = 'contain'
position = 'center center'
break
case 'aspectFill':
size = 'cover'
position = 'center center'
break
case 'widthFix':
size = '100% 100%'
break
case 'top':
position = 'center top'
break
case 'bottom':
position = 'center bottom'
break
case 'center':
position = 'center center'
break
case 'left':
position = 'left center'
break
case 'right':
position = 'right center'
break
case 'top left':
position = 'left top'
break
case 'top right':
position = 'right top'
break
case 'bottom left':
position = 'left bottom'
break
case 'bottom right':
position = 'right bottom'
break
default:
size = '100% 100%'
position = '0% 0%'
break
}
return `background-position:${position};background-size:${size};background-repeat:${repeat};`
},
},
watch: {
src(newValue, oldValue) {
this._setContentImage()
this._loadImage()
},
mode(newValue, oldValue) {
if (oldValue === 'widthFix') {
this.$el.style.height = this.availHeight
}
if (newValue === 'widthFix' && this.ratio) {
this._fixSize()
}
},
},
components: {
ResizeSensor,
},
mounted() {
this.availHeight = this.$el.style.height || ''
this._setContentImage()
if (!this.realImagePath) {
return
}
this._loadImage()
},
methods: {
_resize() {
if (this.mode === 'widthFix') {
this._fixSize()
}
},
_fixSize() {
const elWidth = this._getWidth()
if (elWidth) {
let height = elWidth / this.ratio
// fix: 解决 Chrome 浏览器上某些情况下导致 1px 缝隙的问题
if (
typeof navigator &&
navigator.vendor === 'Google Inc.' &&
height > 10
) {
height = Math.round(height / 2) * 2
}
this.$el.style.height = height + 'px'
}
},
_setContentImage() {
this.$refs.content.style.backgroundImage = this.src
? `url("${this.realImagePath}")`
: 'none'
},
_loadImage() {
const _self = this
const img = new Image()
img.onload = function ($event) {
_self.originalWidth = this.width
_self.originalHeight = this.height
if (_self.mode === 'widthFix') {
_self._fixSize()
}
_self.$trigger('load', $event, {
width: this.width,
height: this.height,
})
}
img.onerror = function ($event) {
_self.$trigger('error', $event, {
errMsg: `GET ${_self.src} 404 (Not Found)`,
})
}
img.src = this.realImagePath
},
_getWidth() {
const computedStyle = window.getComputedStyle(this.$el)
const borderWidth =
(parseFloat(computedStyle.borderLeftWidth, 10) || 0) +
(parseFloat(computedStyle.borderRightWidth, 10) || 0)
const paddingWidth =
(parseFloat(computedStyle.paddingLeft, 10) || 0) +
(parseFloat(computedStyle.paddingRight, 10) || 0)
return this.$el.offsetWidth - borderWidth - paddingWidth
},
},
}
</script>
\ No newline at end of file
......@@ -6,7 +6,7 @@ import CheckboxGroup from './checkbox-group/index.vue'
import Editor from './editor/index.vue'
import Form from './form/index'
import Icon from './icon/index'
import Image from './image/index.vue'
import Image from './image/index'
import Input from './input/index.vue'
import Label from './label/index.vue'
// import MovableArea from './movable-area/index.vue'
......
import { Ref, SetupContext } from 'vue'
type EventDetail = Record<string, any>
export type CustomEventTrigger = ReturnType<typeof useCustomEvent>
export function useCustomEvent(ref: Ref, emit: SetupContext['emit']) {
export function useCustomEvent(
ref: Ref<HTMLElement | null>,
emit: SetupContext['emit']
) {
return (name: string, evt: Event, detail?: EventDetail) => {
emit(
name,
......@@ -16,10 +20,9 @@ function normalizeDataset(el: HTMLElement) {
}
function normalizeTarget(el: HTMLElement): WechatMiniprogram.Target {
const { id, tagName, offsetTop, offsetLeft } = el
const { id, offsetTop, offsetLeft } = el
return {
id,
tagName,
dataset: normalizeDataset(el),
offsetTop,
offsetLeft,
......
此差异已折叠。
......@@ -36,8 +36,8 @@
"peerDependencies": {
"@dcloudio/uni-cli-shared": "^3.0.0",
"@dcloudio/uni-shared": "^3.0.0",
"@vue/shared": "^3.0.10",
"vite": "^2.1.5"
"@vue/shared": "^3.0.11",
"vite": "^2.2.1"
},
"devDependencies": {
"@types/mime": "^2.0.3",
......
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册