Browse Source

add sample editor components

dev
powersir 1 year ago
parent
commit
6d73761ab0
14 changed files with 536 additions and 46 deletions
  1. +214
    -0
      mock/propertyById.json
  2. +2
    -1
      package.json
  3. +5
    -5
      src/layout/header/index.tsx
  4. +11
    -2
      src/layout/index.tsx
  5. +10
    -11
      src/models/user.ts
  6. +1
    -1
      src/pages/custom/product/sample/components/mask-picture-editor.tsx
  7. +227
    -0
      src/pages/custom/product/sample/editor/components/ImageStencil.tsx
  8. +29
    -0
      src/pages/custom/product/sample/editor/index.tsx
  9. +6
    -2
      src/pages/custom/product/sample/index.tsx
  10. +4
    -2
      src/pages/login/index.tsx
  11. +5
    -3
      src/request/index.ts
  12. +5
    -1
      src/request/service/user.ts
  13. +15
    -16
      src/router/config.tsx
  14. +2
    -2
      src/store/global/user.ts

+ 214
- 0
mock/propertyById.json View File

@@ -0,0 +1,214 @@
{
"id": 89,
"createTime": "2023-08-21 11:43:46",
"updateTime": "2023-08-21 11:43:46",
"spuCode": "1-28X7LF",
"categoryId": 1272,
"prototypeName": "TEST男装",
"costPrice": 10.000000,
"maxSyntheticNumber": 100,
"createId": 1,
"isDelete": 2,
"categoryName": "男士短裤",
"prototypeImgs": [
{
"id": 412,
"prototypeId": 89,
"imgId": 1991125,
"maskImgId": 1991130,
"imgName": "3.jpg",
"imgType": 2,
"imgWidth": 600.0,
"imgHeight": 600.0,
"imgUrl": "https://test.vogocm.com:9010/eshop/eshop_img/2023/8/21/20230821114346A056.jpg",
"locations": [
{
"id": 394,
"prototypeImgId": 412,
"clipHeight": 139.2,
"clipWidth": 108.0,
"clipX": 230.4,
"clipY": 318.0,
"cropBoxJson": "{\"left\":317,\"top\":264.9999694824219,\"width\":90,\"height\":116.00003051757812}"
}
]
},
{
"id": 413,
"prototypeId": 89,
"imgId": 1991122,
"maskImgId": 1991131,
"imgName": "1.jpg",
"imgType": 2,
"imgWidth": 600.0,
"imgHeight": 600.0,
"imgUrl": "https://test.vogocm.com:9010/eshop/eshop_img/2023/8/21/20230821114346A053.jpg",
"locations": [
{
"id": 395,
"prototypeImgId": 413,
"clipHeight": 145.2,
"clipWidth": 116.4,
"clipX": 224.4,
"clipY": 314.4001,
"cropBoxJson": "{\"left\":312,\"top\":262.00006103515625,\"width\":97,\"height\":121}"
}
]
},
{
"id": 414,
"prototypeId": 89,
"imgId": 1991127,
"imgName": "f60911abe38e82e9dd2aaa75f3292ed2.jpg",
"imgType": 2,
"imgWidth": 1000.0,
"imgHeight": 1000.0,
"imgUrl": "https://test.vogocm.com:9010/eshop/eshop_img/2023/8/21/20230821114346A058.jpg",
"locations": [
{
"id": 396,
"prototypeImgId": 414,
"clipHeight": 234.0,
"clipWidth": 192.0,
"clipX": 190.0,
"clipY": 288.0,
"cropBoxJson": "{\"left\":220,\"top\":144,\"width\":96,\"height\":117}"
}
]
},
{
"id": 415,
"prototypeId": 89,
"imgId": 1991123,
"imgName": "d247c5ec0ef78f302b803c6bf00658a2.jpg",
"imgType": 2,
"imgWidth": 1000.0,
"imgHeight": 1000.0,
"imgUrl": "https://test.vogocm.com:9010/eshop/eshop_img/2023/8/21/20230821114346A054.jpg",
"locations": [
{
"id": 397,
"prototypeImgId": 415,
"clipHeight": 279.9999,
"clipWidth": 226.0,
"clipX": 380.0,
"clipY": 404.0001,
"cropBoxJson": "{\"left\":315,\"top\":202.00006103515625,\"width\":113,\"height\":139.99993896484375}"
}
]
},
{
"id": 416,
"prototypeId": 89,
"imgId": 1991129,
"imgName": "b470dfa7876630bb2b6ba9ed294b58ca.jpg",
"imgType": 1,
"imgWidth": 1000.0,
"imgHeight": 1000.0,
"imgUrl": "https://test.vogocm.com:9010/eshop/eshop_img/2023/8/21/20230821114346A060.jpg",
"locations": [
{
"id": 398,
"prototypeImgId": 416,
"clipHeight": 247.9999,
"clipWidth": 200.0,
"clipX": 414.0,
"clipY": 402.0001,
"cropBoxJson": "{\"left\":332,\"top\":201.00003051757812,\"width\":100,\"height\":123.99996948242188}"
}
]
},
{
"id": 417,
"prototypeId": 89,
"imgId": 1991124,
"imgName": "e870596429cb1e46065fe25a71b29070.jpg",
"imgType": 1,
"imgWidth": 1000.0,
"imgHeight": 1000.0,
"imgUrl": "https://test.vogocm.com:9010/eshop/eshop_img/2023/8/21/20230821114346A055.jpg",
"locations": [
{
"id": 399,
"prototypeImgId": 417,
"clipHeight": 282.0,
"clipWidth": 216.0,
"clipX": 408.0,
"clipY": 394.0,
"cropBoxJson": "{\"left\":329,\"top\":197,\"width\":108,\"height\":141}"
}
]
},
{
"id": 418,
"prototypeId": 89,
"imgId": 1991128,
"imgName": "067b756a187026d1af97aa78b8d7d0ba.jpg",
"imgType": 1,
"imgWidth": 1000.0,
"imgHeight": 1000.0,
"imgUrl": "https://test.vogocm.com:9010/eshop/eshop_img/2023/8/21/20230821114346A059.jpg",
"locations": [
{
"id": 400,
"prototypeImgId": 418,
"clipHeight": 300.0,
"clipWidth": 230.0001,
"clipX": 389.9999,
"clipY": 384.0,
"cropBoxJson": "{\"left\":319.99993896484375,\"top\":192,\"width\":115.00006103515625,\"height\":150}"
}
]
},
{
"id": 419,
"prototypeId": 89,
"imgId": 1991126,
"imgName": "e944022ccb44a7fb14483da12f1cf8eb.jpg",
"imgType": 1,
"imgWidth": 1000.0,
"imgHeight": 1000.0,
"imgUrl": "https://test.vogocm.com:9010/eshop/eshop_img/2023/8/21/20230821114346A057.jpg",
"locations": [
{
"id": 401,
"prototypeImgId": 419,
"clipHeight": 299.9999,
"clipWidth": 206.0,
"clipX": 400.0,
"clipY": 392.0001,
"cropBoxJson": "{\"left\":325,\"top\":196.00006103515625,\"width\":103,\"height\":149.99993896484375}"
}
]
}
],
"materialClassifys": [
{
"id": 20,
"createTime": "2023-04-10 17:41:00",
"updateTime": "2023-04-10 17:41:06",
"parentId": 2,
"classifyName": "海贼王",
"createId": 2,
"isDelete": 2
},
{
"id": 21,
"createTime": "2023-04-10 17:41:07",
"updateTime": "2023-04-10 17:41:13",
"parentId": 2,
"classifyName": "火影忍者",
"createId": 2,
"isDelete": 2
},
{
"id": 22,
"createTime": "2023-04-10 17:41:16",
"updateTime": "2023-04-10 17:41:22",
"parentId": 2,
"classifyName": "死神",
"createId": 2,
"isDelete": 2
}
]
}

+ 2
- 1
package.json View File

@@ -1,7 +1,7 @@
{
"name": "vogocm-web",
"private": true,
"version": "0.0.0",
"version": "0.0.1",
"type": "module",
"scripts": {
"start": "vite",
@@ -31,6 +31,7 @@
"nprogress": "^0.2.0",
"rc-resize-observer": "^1.3.1",
"react": "^18.2.0",
"react-advanced-cropper": "^0.19.3",
"react-dom": "^18.2.0",
"react-router-dom": "latest",
"react-use": "^17.4.0",


+ 5
- 5
src/layout/header/index.tsx View File

@@ -135,13 +135,13 @@ const Header = () => {
>
<div className='p-[16px]'>
<p className='text-[16px] dark:text-[rgb(237,242,247)] text-[rgb(17,25,39)] '>
{currentUser?.userName}
{currentUser?.username}
</p>
<p className='text-[rgb(108,115,127)] dark:text-[rgb(160,174,192)] mt-[10px]'>
{currentUser?.phoneNumber}
{currentUser?.mobile}
</p>
<p className='text-[rgb(108,115,127)] dark:text-[rgb(160,174,192)] mt-[0px]'>
{currentUser?.emailAddress}
{currentUser?.email}
</p>
</div>
<hr style={{ borderWidth: '0 0 thin' }} className='m-[0] border-solid dark:border-[rgb(45,55,72)] border-[rgb(242,244,247)]' />
@@ -153,8 +153,8 @@ const Header = () => {
}}
>
<div className='btn-icon rounded-[27px] pl-[10px] pr-[14px] justify-between h-[48px] w-[92px] text-[20px] bg-[rgb(227,242,253)] text-[rgb(30,136,229)] hover:(bg-[rgb(33,150,243)] text-[rgb(227,242,253)])'>
{currentUser?.avatarUrl ? (
<Avatar style={{ verticalAlign: 'middle' }} src={currentUser.avatarUrl} />
{currentUser?.avatar ? (
<Avatar style={{ verticalAlign: 'middle' }} src={currentUser.avatar} />
) : (
<Avatar style={{ backgroundColor: 'gold', verticalAlign: 'middle' }} icon={<IconBuguang />} />
)}


+ 11
- 2
src/layout/index.tsx View File

@@ -80,7 +80,7 @@ const BasicLayout: React.FC = () => {

const formatedMenus = formatMenus(menus.filter(o => !o.parentId), menuGroup, routes);
setMenus(formatedMenus);
console.log(components, 'components');
console.log('components', components);
replaceRoutes('*', [
...routes.map(menu => ({
path: `/*${menu.path}`,
@@ -100,9 +100,18 @@ const BasicLayout: React.FC = () => {
path: '404',
name: '404',
},
},
{
path: `/*/custom/product/sample/editor`,
Component: lazy(components['/custom/product/sample/editor/index.tsx']),
id: `/*/custom/product/sample/editor`,
handle: {
parentPaths: ['/custom', '/custom/product', '/custom/product/sample'],
path: '/*/custom/product/sample/editor',
name: '蒙版编辑',
},
}
]);

setLoading(false);

// replace一下当前路由,为了触发路由匹配


+ 10
- 11
src/models/user.ts View File

@@ -73,16 +73,15 @@ export interface Menu {

export interface User {
id: number;
userName: string;
nickName: string;
phoneNumber: string;
username: string;
nickname: string;
deptId:number;
postIds: number;
mobile: string;
email: string;
createDate: string;
updateDate: string;
avatar?: any;
menus: Menu[];
routes: any[];
flatMenus: Menu[];
avatarPath: string;
authList: string[];
sex: number;
createTime: string;
loginDate: string;
avatar?: string;
status: number;
}

+ 1
- 1
src/pages/custom/product/sample/components/mask-picture-editor.tsx View File

@@ -115,7 +115,7 @@ const MaskPictureEditor: React.FC<MaskPictureProps> = (props) => {
url: item.maskImgUrl,
}] : []
return (
<div className='flex justify-start'>
<div className='flex justify-start' key={item.id}>
<Upload
listType="picture-card"
fileList={mainPicture}


+ 227
- 0
src/pages/custom/product/sample/editor/components/ImageStencil.tsx View File

@@ -0,0 +1,227 @@
import React, { FC, forwardRef, useImperativeHandle } from 'react';
import cn from 'classnames';
import {
CardinalDirection,
OrdinalDirection,
CropperTransitions,
CropperState,
MoveDirections,
ResizeOptions,
getStencilCoordinates,
CropperInteractions,
ResizeAnchor,
isFunction,
Coordinates,
RawAspectRatio,
createAspectRatio,
SimpleLine,
SimpleHandler,
BoundingBox,
DraggableArea,
StencilWrapper,
} from 'react-advanced-cropper';

import { Image } from 'antd';

type HandlerComponent = FC<any>;

type LineComponent = FC<any>;

interface HandlerClassNames extends Partial<Record<OrdinalDirection, string>> {
default?: string;
disabled?: string;
hover?: string;
}

interface LineClassNames extends Partial<Record<CardinalDirection, string>> {
default?: string;
disabled?: string;
hover?: string;
}

interface DesiredCropperRef {
getState: () => CropperState | null;
getTransitions: () => CropperTransitions;
getInteractions: () => CropperInteractions;
hasInteractions: () => boolean;
resizeCoordinates: (anchor: ResizeAnchor, directions: Partial<MoveDirections>, parameters: unknown) => void;
resizeCoordinatesEnd: () => void;
moveCoordinates: (directions: Partial<MoveDirections>) => void;
moveCoordinatesEnd: () => void;
}

interface Props {
cropper: DesiredCropperRef;
coordinates?: Coordinates | ((state: CropperState | null) => Coordinates);
handlerComponent?: HandlerComponent;
handlers?: Partial<Record<OrdinalDirection, boolean>>;
handlerClassNames?: HandlerClassNames;
handlerWrapperClassNames?: HandlerClassNames;
lines?: Partial<Record<CardinalDirection, boolean>>;
lineComponent?: LineComponent;
lineClassNames?: LineClassNames;
lineWrapperClassNames?: LineClassNames;
className?: string;
movingClassName?: string;
resizingClassName?: string;
gridClassName?: string;
previewClassName?: string;
boundingBoxClassName?: string;
overlayClassName?: string;
draggableAreaClassName?: string;
minAspectRatio?: number;
maxAspectRatio?: number;
aspectRatio?: RawAspectRatio;
movable?: boolean;
resizable?: boolean;
grid?: boolean;
}

interface Methods {
aspectRatio: RawAspectRatio;
}

export const ImageStencil = forwardRef<Methods, Props>(
(
{
cropper,
coordinates,
aspectRatio,
minAspectRatio,
maxAspectRatio,
handlerComponent = SimpleHandler,
handlers = {
eastNorth: true,
north: true,
westNorth: true,
west: true,
westSouth: true,
south: true,
eastSouth: true,
east: true,
},
handlerClassNames = {},
handlerWrapperClassNames = {},
lines = {
west: true,
north: true,
east: true,
south: true,
},
lineComponent = SimpleLine,
lineClassNames = {},
lineWrapperClassNames = {},
resizable = true,
movable = true,
grid,
gridClassName,
className,
movingClassName,
resizingClassName,
previewClassName,
boundingBoxClassName,
overlayClassName,
draggableAreaClassName,
}: Props,
ref,
) => {
const state = cropper.getState();
const transitions = cropper.getTransitions();
const interactions = cropper.getInteractions();

useImperativeHandle(ref, () => ({
aspectRatio: createAspectRatio(
aspectRatio || {
minimum: minAspectRatio,
maximum: maxAspectRatio,
},
),
}));

const onMove = (directions: MoveDirections) => {
if (cropper && movable) {
cropper.moveCoordinates(directions);
}
};

const onMoveEnd = () => {
if (cropper) {
cropper.moveCoordinatesEnd();
}
};

const onResize = (anchor: ResizeAnchor, directions: MoveDirections, options: ResizeOptions) => {
if (cropper && resizable) {
cropper.resizeCoordinates(anchor, directions, options);
}
};

const onResizeEnd = () => {
if (cropper) {
cropper.resizeCoordinatesEnd();
}
};

const { width, height, left, top } = coordinates
? isFunction(coordinates)
? coordinates(state)
: coordinates
: getStencilCoordinates(state);

return (
state && (
<StencilWrapper
className={cn(
'advanced-cropper-rectangle-stencil',
className,
interactions.moveCoordinates && movingClassName,
interactions.resizeCoordinates && resizingClassName,
{
'advanced-cropper-rectangle-stencil--movable': movable,
'advanced-cropper-rectangle-stencil--moving': interactions.moveCoordinates,
'advanced-cropper-rectangle-stencil--resizable': resizable,
'advanced-cropper-rectangle-stencil--resizing': interactions.resizeCoordinates,
},
)}
width={width}
height={height}
left={left}
top={top}
transitions={transitions}
>
<BoundingBox
reference={state.coordinates}
className={cn(boundingBoxClassName, 'advanced-cropper-rectangle-stencil__bounding-box')}
handlers={handlers}
handlerComponent={handlerComponent}
handlerClassNames={handlerClassNames}
handlerWrapperClassNames={handlerWrapperClassNames}
lines={lines}
lineComponent={lineComponent}
lineClassNames={lineClassNames}
lineWrapperClassNames={lineWrapperClassNames}
onResize={onResize}
onResizeEnd={onResizeEnd}
disabled={!resizable}
>
<DraggableArea
disabled={!movable}
onMove={onMove}
onMoveEnd={onMoveEnd}
className={cn('advanced-cropper-rectangle-stencil__draggable-area', draggableAreaClassName)}
>
<Image
width={width}
height={height}
preview={false}
src="https://test.vogocm.com:9696/image/material/20230413162155A010.png"
/>
</DraggableArea>
</BoundingBox>
</StencilWrapper>
)
);
},
);

ImageStencil.displayName = 'ImageStencil';

+ 29
- 0
src/pages/custom/product/sample/editor/index.tsx View File

@@ -0,0 +1,29 @@
import React, { useState } from 'react';
import { CropperRef, Cropper } from 'react-advanced-cropper';
import 'react-advanced-cropper/dist/style.css'
import { ImageStencil } from "./components/ImageStencil";

export default () => {
const [image] = useState(
'https://test.vogocm.com:9010/eshop/eshop_img/2023/8/21/20230821114346A053.jpg',
);

const onChange = (cropper: CropperRef) => {
console.log(cropper.getCoordinates(), cropper.getCanvas());
};

return (
<Cropper
src={image}
className={'cropper'}
onChange={onChange}
stencilComponent={ImageStencil}
defaultCoordinates={{
left: 100,
top: 100,
width: 400,
height: 400,
}}
/>
)
}

+ 6
- 2
src/pages/custom/product/sample/index.tsx View File

@@ -8,6 +8,7 @@ import SampleAttrEditor from './components/attr-editor'
import MaskPictureEditor from './components/mask-picture-editor';
import type { SampleAttribute } from './components/attr-editor'
import type { MaskPicture } from './components/mask-picture-editor';
import { useNavigate } from 'react-router-dom';

interface DataType {
id: number;
@@ -79,7 +80,10 @@ const TablePage: React.FC = () => {
// setEditData(record);
setAttrEditorVisible(true);
}}>属性设置</a>
<a>编辑</a>
<a
onClick={() => {
navigate('/custom/product/sample/editor')
}}>编辑</a>
<a>删除</a>
</Space>
),
@@ -206,7 +210,7 @@ const TablePage: React.FC = () => {
const [attrEditorVisible, setAttrEditorVisible] = useState(false);
const [maskEditorVisible, setMaskEditorVisible] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const navigate = useNavigate();
const cancelHandle = () => {
setAttrEditorVisible(false);
};


+ 4
- 2
src/pages/login/index.tsx View File

@@ -14,6 +14,7 @@ import './index.css'
const Login = () => {
const navigate = useNavigate();
const { runAsync: listMenus } = useRequest(userService.listMenus, { manual: true });
const { runAsync: getProfile } = useRequest(userService.getProfile, { manual: true });
const { runAsync: login, loading } = useRequest(loginService.login, { manual: true });
const { runAsync: rerefshToken } = useRequest(loginService.rerefshToken, { manual: true });
const { setCurrentUser } = useUserStore();
@@ -28,9 +29,10 @@ const Login = () => {
// data.data.avatarUrl = 'https://test.vogocm.com:9010/eshop/eshop_img/2023/5/24/43853633d16749bfb291f81bebb73451_20230524150631A001.jpg';
setRefreshToken(data.refreshToken);
setToken(data.accessToken);
const [ _, { data: menus } ] = await listMenus()
const [ _, { data: menus } ] = await listMenus();
const [err, {data: profile}] = await getProfile();
// const [ error, {data: tokenData}] = await rerefshToken(data.refreshToken)
debugger
setCurrentUser(profile)
navigate('/');
};



+ 5
- 3
src/request/index.ts View File

@@ -13,8 +13,8 @@ import { ResponseDTO } from '@/models';

const { apiUrl = '' } = useGlobSetting();

const refreshTokenUrl = '/api/auth/refresh/token';
const loginUrl = '/auth/login';
const refreshTokenUrl = '/app-api/member/auth/refresh-token';

export type Response<T> = Promise<[boolean, T, AxiosResponse<T>]>;

@@ -70,9 +70,10 @@ class Request {

const {token} = useGlobalStore.getState();

if (token) {
if (token && !axiosConfig.url?.endsWith(loginUrl)) {
axiosConfig.headers.Authorization = `Bearer ${token}`;
}
axiosConfig.headers['tenant-id'] = '1';
return Promise.resolve(axiosConfig);
}

@@ -99,6 +100,7 @@ class Request {
this.requestingCount += 1;
const {token} = useGlobalStore.getState();
config.headers.Authorization = `Bearer ${token}`;
config.headers['tenant-id'] = '1'
resolve(config);
}
}


+ 5
- 1
src/request/service/user.ts View File

@@ -1,8 +1,12 @@
import request from '@/request';
import { Menu } from '@/models';
import { Menu, User } from '@/models';

export default {
listMenus: () => {
return request.get<Menu>('/admin-api/system/auth/list-menus');
},

getProfile: () => {
return request.get<User>('/admin-api/system/user/profile/get')
}
};

+ 15
- 16
src/router/config.tsx View File

@@ -8,24 +8,23 @@ export const components = Object.keys(modules).reduce<Record<string, () => Promi
const formatPath = path.replace('../pages', '');
prev[formatPath] = async () => {
try {
// 这里其实就是动态加载js,如果报错了说明js资源不存在
return await modules[path]() as any;
} catch {
// 如果manifest已经存在了,就不用再请求了
if (manifest) {
try {
// 有可能manifest是过期的,所以可能还会加载失败
return await import('/' + manifest[`src/pages${formatPath}`]?.file);
} catch {
// 如果失败,重新获取一下manifest.json,拿到最新的路径
manifest = await (await fetch('/manifest.json')).json() as any;
return await import('/' + manifest[`src/pages${formatPath}`]?.file);
}
} else {
// 如果manifest.json为空,请求manifest.json,并根据最新的路径加载对应js
manifest = await (await fetch('/manifest.json')).json() as any;
return await import('/' + manifest[`src/pages${formatPath}`]?.file);
}
// // 如果manifest已经存在了,就不用再请求了
// if (manifest) {
// try {
// // 有可能manifest是过期的,所以可能还会加载失败
// return await import('/' + manifest[`src/pages${formatPath}`]?.file);
// } catch {
// // 如果失败,重新获取一下manifest.json,拿到最新的路径
// manifest = await (await fetch('/manifest.json')).json() as any;
// return await import('/' + manifest[`src/pages${formatPath}`]?.file);
// }
// } else {
// // 如果manifest.json为空,请求manifest.json,并根据最新的路径加载对应js
// manifest = await (await fetch('/manifest.json')).json() as any;
// return await import('/' + manifest[`src/pages${formatPath}`]?.file);
// }
}
}
return prev;


+ 2
- 2
src/store/global/user.ts View File

@@ -1,9 +1,9 @@
import { UserDTO } from '@/models';
import { User } from '@/models';
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

interface State {
currentUser: UserDTO | null;
currentUser: User | null;
}

interface Action {


Loading…
Cancel
Save