feat: upload cover not over yet
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
@use '~@/scss/images.scss' as img;
|
||||
@use '~@/scss/themeColor.scss' as theme;
|
||||
|
||||
.upload-cover-root {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 112px;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
align-items: flex-end;
|
||||
|
||||
&.upload-cover-act-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.upload-cover-act {
|
||||
display: flex;
|
||||
width: 108px;
|
||||
height: 108px;
|
||||
padding: 16px 12px 10px 12px;
|
||||
margin-top: 4px;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 20px;
|
||||
border: 1px dashed rgba(0, 0, 0, 0.12);
|
||||
background: theme.$page-background-color;
|
||||
box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.06);
|
||||
z-index: 1;
|
||||
|
||||
.upload-cover-act-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.upload-cover-text {
|
||||
color: var(--Labels-Secondary, var(--Labels-Secondary, rgba(60, 60, 67, 0.60)));
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 166.667% */
|
||||
}
|
||||
}
|
||||
|
||||
.cover-image-list-container {
|
||||
position: absolute;
|
||||
left: 114px;
|
||||
top: 0;
|
||||
width: calc(100% - 114px);
|
||||
overflow-x: scroll;
|
||||
height: 112px;
|
||||
|
||||
&.full {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cover-image-list {
|
||||
width: auto;
|
||||
height: 112px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
flex-wrap: nowrap;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
.cover-image-item {
|
||||
display: flex;
|
||||
width: 108px;
|
||||
height: 108px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
box-sizing: border-box;
|
||||
|
||||
.cover-image-item-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.cover-image-item-delete {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-source-popup-container {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 26px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.upload-source-popup-item {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
padding: 16px 24px;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,128 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Popup } from "@nutui/nutui-react-taro";
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { Image, View, Text } from '@tarojs/components'
|
||||
import img from '../../config/images'
|
||||
import UploadSourcePopup from './upload-source-popup'
|
||||
import UploadFromWx from './upload-from-wx'
|
||||
import { CommonPopup } from '../'
|
||||
|
||||
import './index.scss'
|
||||
import { uploadFileResponseData } from '@/services/uploadFiles'
|
||||
|
||||
export default function UploadCover(props) {
|
||||
const { value = [], onChange = () => {} } = props
|
||||
export type sourceType = 'album' | 'history' | 'preset'
|
||||
|
||||
export type source = sourceType[]
|
||||
|
||||
export type CoverImageValue = {
|
||||
id: string
|
||||
url: string
|
||||
tempFilePath?: string
|
||||
}
|
||||
|
||||
export interface UploadCoverProps {
|
||||
value: CoverImageValue[]
|
||||
onChange: (value: CoverImageValue[] | ((prev: CoverImageValue[]) => CoverImageValue[])
|
||||
) => void
|
||||
source: source
|
||||
maxCount: number
|
||||
}
|
||||
|
||||
// const values = [
|
||||
// 'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/1a35ebbf-2361-44da-b338-7608561d0b31.png',
|
||||
// 'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/cf5a82ba-90af-4138-a1b3-9119adcde9e0.png',
|
||||
// 'http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/49d7cdf0-b03c-4a0f-91c6-e7778080cfcd.png'
|
||||
// ]
|
||||
|
||||
const mergeCoverImages = (value: CoverImageValue[], images: CoverImageValue[]) => {
|
||||
console.log(value, images, 11111)
|
||||
// 根据id来更新url, 如果id不存在,则添加到value中
|
||||
const newImages = images
|
||||
const updatedValue = value.map(item => {
|
||||
const index = images.findIndex(image => image.id === item.id)
|
||||
if (index !== -1) {
|
||||
newImages.splice(index, 1)
|
||||
return { ...item, url: images[index].url }
|
||||
}
|
||||
return item
|
||||
})
|
||||
return [...updatedValue, ...newImages]
|
||||
}
|
||||
|
||||
export default function UploadCover(props: UploadCoverProps) {
|
||||
const {
|
||||
value = [],
|
||||
onChange = () => void 0,
|
||||
source = ['album', 'history', 'preset'],
|
||||
maxCount = 9,
|
||||
} = props
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
const onAdd = useCallback((images: CoverImageValue[]) => {
|
||||
onChange(prev => mergeCoverImages(prev, images))
|
||||
setVisible(false)
|
||||
}, [value])
|
||||
|
||||
const onWxAdd = useCallback((images: CoverImageValue[], onFileUploaded: Promise<{ id: string, data: uploadFileResponseData }[]>) => {
|
||||
onAdd(images)
|
||||
onFileUploaded.then(res => {
|
||||
console.log(res, 11111)
|
||||
onAdd(res.map(item => ({
|
||||
id: item.id,
|
||||
url: item.data.file_path,
|
||||
})))
|
||||
})
|
||||
}, [onAdd])
|
||||
const onDelete = (image: CoverImageValue) => {
|
||||
onChange(value.filter(item => item.id !== image.id))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup
|
||||
<CommonPopup
|
||||
visible={visible}
|
||||
onClose={() => setVisible(false)}
|
||||
round
|
||||
closeable
|
||||
position="bottom"
|
||||
hideFooter
|
||||
>
|
||||
<div className="upload-cover-popup" onClick={}>
|
||||
<div className="upload-cover-popup-title">上传封面</div>
|
||||
<View className="upload-source-popup-container" style={{ height: source.length * 56 + 52 + 'px' }}>
|
||||
{
|
||||
source.map((item) => {
|
||||
return (
|
||||
<View className="upload-source-popup-item" key={item}>
|
||||
{
|
||||
item === 'album' ? (
|
||||
<UploadFromWx onAdd={onWxAdd} maxCount={maxCount - value.length} />
|
||||
) : (
|
||||
<UploadSourcePopup sourceType={item} onAdd={onAdd} maxCount={maxCount - value.length} />
|
||||
)
|
||||
}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
}
|
||||
</View>
|
||||
</CommonPopup>
|
||||
<div className={`upload-cover-root ${value.length === 0 ? 'upload-cover-act-center' : ''}`}>
|
||||
{value.length < maxCount && (
|
||||
<div className="upload-cover-act" onClick={() => setVisible(true)}>
|
||||
<Image className='upload-cover-act-icon' src={img.ICON_ADD} />
|
||||
<div className="upload-cover-text">添加活动封面</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={`cover-image-list-container ${value.length === maxCount ? 'full' : ''}`}>
|
||||
<div className="cover-image-list">
|
||||
{
|
||||
value.map((item) => {
|
||||
return (
|
||||
<View className="cover-image-item" key={item.id}>
|
||||
<Image className="cover-image-item-image" src={item.url} />
|
||||
<Image className="cover-image-item-delete" src={img.ICON_REMOVE} onClick={() => onDelete(item)} />
|
||||
</View>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
11
src/components/UploadCover/upload-from-wx.scss
Normal file
11
src/components/UploadCover/upload-from-wx.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
.upload-from-wx-text {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
padding: 16px 24px;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
47
src/components/UploadCover/upload-from-wx.tsx
Normal file
47
src/components/UploadCover/upload-from-wx.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import uploadApi from '@/services/uploadFiles'
|
||||
import './upload-from-wx.scss'
|
||||
import { CoverImageValue } from '.'
|
||||
import { uploadFileResponseData } from '@/services/uploadFiles'
|
||||
|
||||
export interface UploadFromWxProps {
|
||||
onAdd: (images: CoverImageValue[], onFileUploaded: Promise<{ id: string, data: uploadFileResponseData }[]>) => void
|
||||
maxCount: number
|
||||
}
|
||||
|
||||
export default function UploadFromWx(props: UploadFromWxProps) {
|
||||
const {
|
||||
onAdd = () => void 0,
|
||||
maxCount = 9, // calc from parent
|
||||
} = props
|
||||
const handleImportFromWx = () => {
|
||||
Taro.chooseImage({
|
||||
count: maxCount,
|
||||
sizeType: ['original', 'compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
}).then(async (res) => {
|
||||
// TODO: compress image
|
||||
// TODO: cropping image to const size
|
||||
let count = 0
|
||||
const files = res.tempFiles.map(item => ({
|
||||
filePath: item.path,
|
||||
description: 'test',
|
||||
tags: 'test',
|
||||
is_public: 1 as unknown as 0 | 1,
|
||||
id: (Date.now() + count++).toString(),
|
||||
}))
|
||||
const onFileUploaded = uploadApi.batchUpload(files)
|
||||
onAdd(res.tempFiles.map(item => ({
|
||||
id: Date.now().toString(),
|
||||
url: item.path,
|
||||
})), onFileUploaded) // TODO: add loading state
|
||||
})
|
||||
}
|
||||
return (
|
||||
<View onClick={handleImportFromWx}>
|
||||
<Text className="upload-from-wx-text">从相册添加</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
156
src/components/UploadCover/upload-source-popup.scss
Normal file
156
src/components/UploadCover/upload-source-popup.scss
Normal file
@@ -0,0 +1,156 @@
|
||||
@use '~@/scss/themeColor.scss' as theme;
|
||||
|
||||
.upload-source-popup-text {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
padding: 16px 24px;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-popup {
|
||||
width: 100%;
|
||||
padding: 26px 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
.upload-popup-title {
|
||||
display: flex;
|
||||
padding: 18px 20px 10px 20px;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px; /* 150% */
|
||||
letter-spacing: -0.23px;
|
||||
}
|
||||
|
||||
.upload-popup-scroll-view {
|
||||
max-height: calc(100vh - 260px);
|
||||
overflow-y: auto;
|
||||
|
||||
.upload-popup-image-list {
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px 10px;
|
||||
|
||||
.upload-popup-image-item {
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 9px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
box-sizing: border-box;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
margin: 0;
|
||||
position: relative;
|
||||
|
||||
&:not(.selected) {
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-popup-image-item-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 9px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.upload-popup-image-item-select {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.selected {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
|
||||
.select-image-icon {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.select-image-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-popup-image-list-empty {
|
||||
width: 100%;
|
||||
height: 60vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.upload-popup-image-list-empty-image {
|
||||
width: 80%;
|
||||
aspect-ratio: 4/3;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.upload-popup-image-list-empty-text {
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 24px; /* 150% */
|
||||
letter-spacing: -0.23px;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-popup-footer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 62px;
|
||||
padding: 8px 10px 10px 10px;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
.upload-popup-footer-cancel, .upload-popup-footer-confirm {
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: "PingFang SC";
|
||||
box-sizing: border-box;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.upload-popup-footer-cancel {
|
||||
background: theme.$page-background-color;
|
||||
}
|
||||
|
||||
.upload-popup-footer-confirm {
|
||||
background: theme.$primary-color;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
161
src/components/UploadCover/upload-source-popup.tsx
Normal file
161
src/components/UploadCover/upload-source-popup.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Image, View, Text, ScrollView, Button } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import img from '../../config/images'
|
||||
import publishService from '../../services/publishService'
|
||||
import { CommonPopup } from '../'
|
||||
import emptyStatus from '../../static/emptyStatus/publish-empty.png'
|
||||
|
||||
import './upload-source-popup.scss'
|
||||
|
||||
type SourceType = 'history' | 'preset'
|
||||
|
||||
type ImageItem = {
|
||||
id: string
|
||||
url: string
|
||||
tempFilePath?: string
|
||||
}
|
||||
|
||||
interface UploadImageProps {
|
||||
sourceType: SourceType
|
||||
onAdd: (images: ImageItem[]) => void
|
||||
maxCount: number
|
||||
}
|
||||
|
||||
const sourceMap = new Map<SourceType, string>([
|
||||
['history', '历史图库'],
|
||||
['preset', '预设图库']
|
||||
])
|
||||
|
||||
const checkImageSelected = (images: ImageItem[], image: ImageItem) => {
|
||||
return images.some(item => item.id === image.id)
|
||||
}
|
||||
|
||||
export default function UploadImage(props: UploadImageProps) {
|
||||
const {
|
||||
sourceType = 'history',
|
||||
onAdd = () => void 0,
|
||||
maxCount = 9,
|
||||
} = props
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [images, setImages] = useState<ImageItem[]>([])
|
||||
const [selectedImages, setSelectedImages] = useState<ImageItem[]>([])
|
||||
|
||||
const handleImageClick = (image: ImageItem) => {
|
||||
if (checkImageSelected(selectedImages, image)) {
|
||||
setSelectedImages(selectedImages.filter(item => item.id !== image.id))
|
||||
} else if (!outOfMax) {
|
||||
setSelectedImages([...selectedImages, image])
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: `最多选择${maxCount}张图片`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
publishService.getPictures({
|
||||
pageOption: {
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
},
|
||||
seachOption: {
|
||||
tag: '',
|
||||
resource_type: 'image',
|
||||
dateRange: [],
|
||||
},
|
||||
}).then(res => {
|
||||
if (res.success) {
|
||||
setImages(res.data.data.rows.map(item => ({
|
||||
id: Date.now().toString(),
|
||||
url: item.thumbnail_url,
|
||||
})))
|
||||
} else {
|
||||
// TODO: 显示错误信息
|
||||
Taro.showToast({
|
||||
title: res.message,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setSelectedImages([])
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (selectedImages.length > 0) {
|
||||
onAdd(selectedImages)
|
||||
setVisible(false)
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: '请选择图片',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const outOfMax = selectedImages.length >= maxCount
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommonPopup
|
||||
visible={visible}
|
||||
onClose={() => setVisible(false)}
|
||||
round
|
||||
hideFooter
|
||||
position='bottom'
|
||||
>
|
||||
<View className="upload-popup">
|
||||
<View className="upload-popup-title">{sourceMap.get(sourceType)}</View>
|
||||
{/* TODO: 分页 加载更多 */}
|
||||
{/* TODO: 图片加载失败 */}
|
||||
{/* TODO: 图片加载中 */}
|
||||
<ScrollView
|
||||
scrollY
|
||||
className="upload-popup-scroll-view"
|
||||
>
|
||||
{images.length > 0 ? (
|
||||
<View className="upload-popup-image-list">
|
||||
{images.map(item => {
|
||||
const isSelected = checkImageSelected(selectedImages, item)
|
||||
return (
|
||||
<View className={`upload-popup-image-item ${outOfMax ? 'disabled' : ''} ${isSelected ? 'selected' : ''}`} onClick={() => handleImageClick(item)}>
|
||||
<Image className="upload-popup-image-item-image" src={item.url} />
|
||||
<View className={`upload-popup-image-item-select ${isSelected ? 'selected' : ''}`}>
|
||||
{isSelected ? (
|
||||
<Image className="select-image-icon" src={img.ICON_CIRCLE_SELECT_ARROW} />
|
||||
) : (
|
||||
<Image className="select-image-icon" src={img.ICON_CIRCLE_UNSELECT} />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
) : (
|
||||
<View className="upload-popup-image-list-empty">
|
||||
<Image className="upload-popup-image-list-empty-image" src={emptyStatus} />
|
||||
<Text className="upload-popup-image-list-empty-text">暂无内容</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
{images.length > 0 ? (
|
||||
<View className="upload-popup-footer">
|
||||
<Button className="upload-popup-footer-cancel" onClick={() => setVisible(false)}>取消</Button>
|
||||
<Button className={`upload-popup-footer-confirm ${selectedImages.length > 0 ? 'active' : ''}`} type='primary' onClick={handleConfirm}>完成</Button>
|
||||
</View>
|
||||
) : (
|
||||
<View className="upload-popup-footer">
|
||||
<Button className="upload-popup-footer-cancel" onClick={() => setVisible(false)}>取消</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</CommonPopup>
|
||||
<View className="upload-source-popup-text" onClick={() => setVisible(true)}>{sourceMap.get(sourceType)}选取</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,19 +9,21 @@ import { SelectStadium, StadiumDetail } from './SelectStadium'
|
||||
import TimeSelector from './TimeSelector'
|
||||
import TitleInput from './TitleInput'
|
||||
import CommonPopup from './CommonPopup'
|
||||
import UploadCover from './UploadCover'
|
||||
|
||||
export {
|
||||
ActivityTypeSwitch,
|
||||
TextareaTag,
|
||||
export {
|
||||
ActivityTypeSwitch,
|
||||
TextareaTag,
|
||||
FormSwitch,
|
||||
ImageUpload,
|
||||
ImageUpload,
|
||||
FormBasicInfo,
|
||||
Range,
|
||||
NumberInterval,
|
||||
SelectStadium,
|
||||
TimeSelector,
|
||||
Range,
|
||||
NumberInterval,
|
||||
SelectStadium,
|
||||
TimeSelector,
|
||||
TitleInput,
|
||||
StadiumDetail,
|
||||
CommonPopup
|
||||
CommonPopup,
|
||||
UploadCover
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user