Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 头像裁剪组件 #2570

Merged
merged 23 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bbe6d7f
feat: 头像裁剪组件
yi-boide Sep 18, 2023
2d09298
docs: update config.json
yi-boide Sep 18, 2023
843bac6
feat: 增加bottom插槽,控制工具栏位置,抛出工具栏相关方法
yi-boide Sep 18, 2023
45be547
feat: 中间的文字提示可以通过props更改
yi-boide Sep 18, 2023
1593060
feat: 命名规范更改
yi-boide Sep 19, 2023
2ae48e3
docs: update
yi-boide Sep 19, 2023
833eed7
feat: 头像裁剪组件-taro版本
yi-boide Sep 23, 2023
b9e775f
feat: 冲突
yi-boide Sep 23, 2023
87c690d
Merge branch 'avatar-taro' into avatar
yi-boide Sep 23, 2023
b2a7040
feat: 优化文档结构
yi-boide Sep 23, 2023
5a9934f
feat: 针对web绘制使用设备像素比提升图像质量
yi-boide Sep 23, 2023
c7a751a
feat: 适配支付宝小程序canvas 2d,要求基础库版本2.7.0或更高
yi-boide Sep 25, 2023
5223b74
feat: vue版本优化获取元素宽高的方式
yi-boide Sep 30, 2023
b80590f
test: update test
yi-boide Sep 30, 2023
81f0843
test: update test avatarcropper toouch
yi-boide Sep 30, 2023
858911e
Revert "test: update test avatarcropper toouch"
yi-boide Sep 30, 2023
86afda9
test: update test avatarcropper toouch
yi-boide Sep 30, 2023
2c6e712
test: touch x
yi-boide Sep 30, 2023
3301a28
test: update touch
yi-boide Sep 30, 2023
ef51d8e
test: update test image
yi-boide Sep 30, 2023
b704349
test: update input set value
yi-boide Sep 30, 2023
991a6ea
Revert "test: update test image"
yi-boide Sep 30, 2023
2b605fb
Merge branch 'v4' into pr/2570
eiinu Oct 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"unplugin-vue-markdown": "^0.24.3",
"vite": "^4.4.11",
"vitest": "^0.34.6",
"vitest-canvas-mock": "^0.3.3",
"vue": "^3.3.4",
"vue-tsc": "1.8.15"
},
Expand Down
7 changes: 7 additions & 0 deletions packages/nutui-taro-demo/project.private.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
"condition": {
"miniprogram": {
"list": [
{
"name": "AvatarCropper",
"pathName": "business/pages/avatarcropper/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "exhibition/pages/imagepreview/index",
"pathName": "exhibition/pages/imagepreview/index",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
navigationBarTitleText: 'AvatarCropper'
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<template>
<div class="demo barrage-demo" :class="{ web: env === 'WEB' }">
<Header v-if="env === 'WEB'" />
<h2>基础用法</h2>
<nut-cell>
<nut-avatar-cropper @confirm="cutImage">
<nut-avatar size="large">
<img :src="imageUrl" />
</nut-avatar>
</nut-avatar-cropper>
</nut-cell>
<h2>裁剪区域toolbar插槽</h2>
<nut-cell>
<nut-avatar-cropper ref="avatarCropperRef" toolbar-position="top" edit-text="修改" @confirm="cutImage">
<nut-avatar size="large">
<img :src="imageUrl" />
</nut-avatar>
<template #toolbar>
<div class="toolbar">
<nut-button type="primary" @click="avatarCropperRef.cancel()">取消</nut-button>
<nut-button type="primary" @click="avatarCropperRef.reset()">重置</nut-button>
<nut-button type="primary" @click="avatarCropperRef.rotate()">旋转</nut-button>
<nut-button type="primary" @click="avatarCropperRef.confirm()">确认</nut-button>
</div>
</template>
</nut-avatar-cropper>
</nut-cell>
</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import Taro from '@tarojs/taro';
import Header from '../../../components/header.vue';
const env = Taro.getEnv();
const imageUrl = ref(
'https://img12.360buyimg.com/imagetools/jfs/t1/196430/38/8105/14329/60c806a4Ed506298a/e6de9fb7b8490f38.png'
);
const avatarCropperRef = ref();
const cutImage = (url: string) => {
imageUrl.value = url;
};
</script>

<style>
.toolbar {
display: flex;
justify-content: space-between;
}
</style>
29 changes: 29 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions src/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,17 @@
"type": "component",
"author": "ailululu",
"taro": true
},
{
"version": "1.0.0",
"name": "AvatarCropper",
"type": "component",
"tarodoc": true,
"show": true,
"cName": "头像裁剪",
"desc": "仿微信头像裁剪功能",
"taro": true,
"author": "Marvin"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`layout default slot 1`] = `
"<div class=\\"nut-avatar-cropper\\" data-edit-text=\\"编辑\\"><img src=\\"https://img12.360buyimg.com/imagetools/jfs/t1/196430/38/8105/14329/60c806a4Ed506298a/e6de9fb7b8490f38.png\\"><input type=\\"file\\" accept=\\"image/*\\" class=\\"nut-avatar-cropper__input\\"></div>
<div class=\\"nut-cropper-popup\\" style=\\"display: none;\\"><canvas class=\\"nut-cropper-popup__canvas\\"></canvas>
<div class=\\"nut-cropper-popup__highlight\\">
<div class=\\"highlight\\" style=\\"width: 0px; height: 0px;\\"></div>
</div>
<div class=\\"nut-cropper-popup__toolbar bottom\\">
<div class=\\"flex-sb\\">
<div class=\\"nut-cropper-popup__toolbar-item\\">
<view class=\\"nut-button nut-button--danger nut-button--normal nut-button--round\\">
<view class=\\"nut-button__wrap\\">
<!--v-if-->
<!--v-if-->
<view class=\\"\\">取消</view>
</view>
</view>
</div>
<div class=\\"nut-cropper-popup__toolbar-item\\"><svg class=\\"nut-icon nut-icon-refresh2\\" style=\\"color: rgb(255, 255, 255);\\" xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 1024 1024\\" role=\\"presentation\\">
<path d=\\"M771.938 315.077h199.55L958.358 469.99 758.81 364.964c-13.128-7.877-18.38-23.63-10.502-36.759 2.625-7.877 13.128-13.128 23.63-13.128zm-535.63 393.846H44.636L57.764 554.01l191.672 105.026c13.128 7.877 18.38 23.63 10.502 36.759-5.25 7.877-15.753 13.128-23.63 13.128zM509.374 1024C257.313 1024 44.636 845.456 5.251 596.02 0 575.016 15.754 556.637 36.76 551.386c21.005-2.626 42.01 10.502 44.636 31.507 34.133 210.052 215.302 362.339 427.98 362.339 191.671 0 362.338-128.657 417.476-312.452 5.252-21.005 28.882-34.133 49.887-26.256 21.006 5.251 34.134 28.882 26.257 49.887C937.354 871.713 735.179 1024 509.375 1024zm467.364-551.385c-18.379 0-36.759-13.128-39.384-34.133C903.22 231.056 722.05 78.77 509.374 78.77c-191.671 0-362.338 128.657-414.85 312.452-5.252 21.005-28.883 34.133-49.888 26.256-21.005-5.251-34.133-28.882-26.257-49.887C81.395 152.287 283.57 0 509.374 0c252.062 0 464.739 178.544 504.123 427.98 2.626 21.005-10.502 42.01-31.507 44.635h-5.252z\\" fill=\\"currentColor\\" fill-opacity=\\"0.9\\"></path>
</svg></div>
<div class=\\"nut-cropper-popup__toolbar-item\\"><svg class=\\"nut-icon nut-icon-retweet\\" style=\\"color: rgb(255, 255, 255);\\" xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 1024 1024\\" role=\\"presentation\\">
<path d=\\"M136 552h63.6c4.4 0 8-3.6 8-8V288.7h528.6v72.6c0 1.9.6 3.7 1.8 5.2 2.9 3.6 8.1 4.3 11.7 1.4L893 255.4c4.3-5 3.6-10.3 0-13.2L749.7 129.8c-1.5-1.2-3.3-1.8-5.2-1.8-4.6 0-8.4 3.8-8.4 8.4V209H199.7c-39.5 0-71.7 32.2-71.7 71.8V544c0 4.4 3.6 8 8 8zm752-80h-63.6c-4.4 0-8 3.6-8 8v255.3H287.8v-72.6c0-1.9-.6-3.7-1.8-5.2-2.9-3.6-8.1-4.3-11.7-1.4L131 768.6c-4.3 5-3.6 10.3 0 13.2l143.3 112.4c1.5 1.2 3.3 1.8 5.2 1.8 4.6 0 8.4-3.8 8.4-8.4V815h536.6c39.5 0 71.7-32.2 71.7-71.8V480c-.2-4.4-3.8-8-8.2-8z\\" fill=\\"currentColor\\" fill-opacity=\\"0.9\\"></path>
</svg></div>
<div class=\\"nut-cropper-popup__toolbar-item\\">
<view class=\\"nut-button nut-button--success nut-button--normal nut-button--round\\">
<view class=\\"nut-button__wrap\\">
<!--v-if-->
<!--v-if-->
<view class=\\"\\">确定</view>
</view>
</view>
</div>
</div>
</div>
</div>"
`;
83 changes: 83 additions & 0 deletions src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'vitest-canvas-mock';
import { mount } from '@vue/test-utils';
import AvatarCropper from '../index.vue';
import { sleep, trigger, triggerDrag } from '@/packages/utils/unit';
import { h } from 'vue';

const mockFile = new File([new ArrayBuffer(10000)], 'test.jpg', {
type: 'image/jpg'
});

test('layout default slot', () => {
const wrapper = mount(AvatarCropper, {
slots: {
default: h('img', {
src: 'https://img12.360buyimg.com/imagetools/jfs/t1/196430/38/8105/14329/60c806a4Ed506298a/e6de9fb7b8490f38.png'
})
}
});

expect(wrapper.html()).toMatchSnapshot();
expect(wrapper.find('.nut-avatar-cropper').html()).toContain(
'<img src="https://img12.360buyimg.com/imagetools/jfs/t1/196430/38/8105/14329/60c806a4Ed506298a/e6de9fb7b8490f38.png">'
);
});

test('should render base cutAvatar and type', async () => {
const wrapper = mount(AvatarCropper);
const up_load = wrapper.find('.nut-avatar-cropper');
expect(up_load.exists()).toBe(true);
const up_load1 = wrapper.find('.nut-avatar-cropper__input');
expect(up_load1.attributes().type).toBe('file');
});

test('AvatarCropper: Select the image to open the crop window', async () => {
const wrapper = mount(AvatarCropper);
const input: any = wrapper.find<HTMLInputElement>('.nut-avatar-cropper__input');
expect(input.exists()).toBe(true);
const smallFile = new File([new ArrayBuffer(100)], 'small.jpg');
Object.defineProperty(input.element, 'files', {
get: vi.fn().mockReturnValue([mockFile, smallFile])
});
expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', 'display: none;');
await input.trigger('change');
await sleep();
expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', '');
const canvas = wrapper.find('.nut-cropper-popup__canvas');
expect(canvas.exists()).toBe(true);

const track = wrapper.find('.nut-cropper-popup__highlight');

trigger(track, 'touchstart', 0, 0, { x: 0, y: 0 });
trigger(track, 'touchmove', 20, 20, { x: 40, y: 60 });
trigger(track, 'touchend', 20, 100, { x: 40, y: 60 });

triggerDrag(track, 50, 60);
const toolbar = wrapper.findAll('.nut-cropper-popup__toolbar-item');
expect(toolbar.length).toBe(4);

const cancel = toolbar[0];
cancel.trigger('click');
expect(wrapper.emitted('cancel')).toBeTruthy();
expect(input.element.value).toBe('');
await sleep();
expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', 'display: none;');

const reset = toolbar[1];
reset.trigger('click');
expect(wrapper.vm.angle).toBe(0);

const rotate = toolbar[2];
rotate.trigger('click');
expect(wrapper.vm.angle).toBe(90);
triggerDrag(track, 1000, 2000);
rotate.trigger('click');
expect(wrapper.vm.angle).toBe(180);
rotate.trigger('click');
rotate.trigger('click');
expect(wrapper.vm.angle).toBe(0);

const confirm = toolbar[3];
confirm.trigger('click');
expect(wrapper.emitted('confirm')).toBeTruthy();
});
46 changes: 46 additions & 0 deletions src/packages/__VUE/avatarcropper/demo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<template>
<div class="demo full">
<h2>基础用法</h2>
<nut-cell>
<nut-avatar-cropper @confirm="cutImage">
<nut-avatar size="large">
<img :src="imageUrl" />
</nut-avatar>
</nut-avatar-cropper>
</nut-cell>
<h2>裁剪区域toolbar插槽</h2>
<nut-cell>
<nut-avatar-cropper ref="avatarCropperRef" toolbar-position="top" edit-text="修改" @confirm="cutImage">
<nut-avatar size="large">
<img :src="imageUrl" />
</nut-avatar>
<template #toolbar>
<div class="toolbar">
<nut-button type="primary" @click="avatarCropperRef.cancel()">取消</nut-button>
<nut-button type="primary" @click="avatarCropperRef.reset()">重置</nut-button>
<nut-button type="primary" @click="avatarCropperRef.rotate()">旋转</nut-button>
<nut-button type="primary" @click="avatarCropperRef.confirm()">确认</nut-button>
</div>
</template>
</nut-avatar-cropper>
</nut-cell>
</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
const imageUrl = ref(
'https://img12.360buyimg.com/imagetools/jfs/t1/196430/38/8105/14329/60c806a4Ed506298a/e6de9fb7b8490f38.png'
);
const avatarCropperRef = ref();
const cutImage = (url: string) => {
imageUrl.value = url;
};
</script>

<style>
.toolbar {
display: flex;
justify-content: space-between;
}
</style>
Loading