Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
小 白蛋
Cloudbase Extension Cms
Commits
195829ff
Commit
195829ff
authored
4 years ago
by
cwuyiqing
Browse files
Options
Download
Email Patches
Plain Diff
feat: 支持通过 CSV, JSON 导入数据
parent
abbd10d3
Changes
17
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
packages/admin/config/config.ts
+5
-0
packages/admin/config/config.ts
packages/admin/src/components/Fields/FieldEditor.tsx
+252
-0
packages/admin/src/components/Fields/FieldEditor.tsx
packages/admin/src/components/Fields/FieldRender.tsx
+163
-0
packages/admin/src/components/Fields/FieldRender.tsx
packages/admin/src/components/Fields/Image.tsx
+78
-0
packages/admin/src/components/Fields/Image.tsx
packages/admin/src/components/Fields/utils.ts
+57
-0
packages/admin/src/components/Fields/utils.ts
packages/admin/src/pages/index.tsx
+2
-2
packages/admin/src/pages/index.tsx
packages/admin/src/pages/project/content/ContentEditor.tsx
+127
-0
packages/admin/src/pages/project/content/ContentEditor.tsx
packages/admin/src/pages/project/content/ContentTable.tsx
+65
-29
packages/admin/src/pages/project/content/ContentTable.tsx
packages/admin/src/pages/project/content/SearchForm.tsx
+162
-0
packages/admin/src/pages/project/content/SearchForm.tsx
packages/admin/src/pages/project/migrate/index.tsx
+127
-0
packages/admin/src/pages/project/migrate/index.tsx
packages/admin/src/services/content.ts
+27
-0
packages/admin/src/services/content.ts
packages/admin/src/utils/cloudbase.ts
+5
-5
packages/admin/src/utils/cloudbase.ts
packages/admin/src/utils/date.ts
+6
-0
packages/admin/src/utils/date.ts
packages/admin/src/utils/index.ts
+1
-18
packages/admin/src/utils/index.ts
packages/service/src/modules/projects/migrate/migrate.controller.ts
+141
-0
...ervice/src/modules/projects/migrate/migrate.controller.ts
packages/service/src/modules/projects/projects.controller.ts
+2
-2
packages/service/src/modules/projects/projects.controller.ts
packages/service/src/modules/projects/projects.module.ts
+8
-1
packages/service/src/modules/projects/projects.module.ts
with
1228 additions
and
57 deletions
+1228
-57
packages/admin/config/config.ts
+
5
-
0
View file @
195829ff
...
...
@@ -99,6 +99,11 @@ const config: IConfig = {
access
:
'
canContent
'
,
wrappers
:
[
'
../components/SecurityWrapper/index
'
],
routes
:
[
{
exact
:
true
,
path
:
'
/:projectId/content/migrate
'
,
component
:
'
./project/migrate
'
,
},
{
exact
:
true
,
path
:
'
/:projectId/content/:schemaId
'
,
...
...
This diff is collapsed.
Click to expand it.
packages/admin/src/components/Fields/FieldEditor.tsx
0 → 100644
+
252
-
0
View file @
195829ff
import
React
,
{
Suspense
}
from
'
react
'
import
{
Spin
,
Form
,
Input
,
Switch
,
Button
,
Col
,
Select
,
InputNumber
}
from
'
antd
'
import
{
Rule
}
from
'
antd/es/form
'
import
{
IConnectEditor
,
IDatePicker
,
IUploader
}
from
'
@/components/Fields
'
import
{
MinusCircleOutlined
,
PlusOutlined
}
from
'
@ant-design/icons
'
const
MarkdownEditor
=
React
.
lazy
(()
=>
import
(
'
@/components/Fields/Markdown
'
))
const
RichTextEditor
=
React
.
lazy
(()
=>
import
(
'
@/components/Fields/RichText
'
))
const
{
TextArea
}
=
Input
const
{
Option
}
=
Select
const
LazyMarkdownEditor
:
React
.
FC
=
(
props
:
any
)
=>
(
<
Suspense
fallback
=
{
<
Spin
/>
}
>
<
MarkdownEditor
{
...
props
}
/>
</
Suspense
>
)
const
LazyRichTextEditor
:
React
.
FC
=
(
props
:
any
)
=>
(
<
Suspense
fallback
=
{
<
Spin
/>
}
>
<
RichTextEditor
{
...
props
}
/>
</
Suspense
>
)
/**
* 根据类型获取验证规则
*/
function
getValidateRule
(
type
:
string
)
{
let
rule
:
Rule
|
null
switch
(
type
)
{
case
'
Url
'
:
rule
=
{
type
:
'
url
'
,
message
:
'
请输入正确的网址
'
}
break
case
'
Email
'
:
rule
=
{
type
:
'
email
'
,
message
:
'
请输入正确的邮箱
'
,
}
break
case
'
Number
'
:
rule
=
{
type
:
'
number
'
,
message
:
'
请输入正确的数字
'
}
break
case
'
Tel
'
:
rule
=
{
pattern
:
/^
\d
+$/
,
message
:
'
请输入正确的电话号码
'
,
}
break
default
:
rule
=
null
}
return
rule
}
const
getRules
=
(
field
:
SchemaFieldV2
):
Rule
[]
=>
{
const
{
isRequired
,
displayName
,
min
,
max
,
type
}
=
field
const
rules
:
Rule
[]
=
[]
if
(
isRequired
)
{
rules
.
push
({
required
:
isRequired
,
message
:
`
${
displayName
}
字段是必须要的`
})
}
if
(
min
)
{
const
validType
=
type
===
'
String
'
||
type
===
'
MultiLineString
'
?
'
string
'
:
'
number
'
rules
.
push
({
min
,
type
:
validType
,
message
:
validType
===
'
string
'
?
`不能小于最小长度
${
min
}
`
:
`不能小于最小值
${
min
}
`
,
})
}
if
(
max
)
{
const
validType
=
type
===
'
String
'
||
type
===
'
MultiLineString
'
?
'
string
'
:
'
number
'
rules
.
push
({
max
,
type
:
validType
,
message
:
validType
===
'
string
'
?
`不能大于最大长度
${
max
}
`
:
`不能大于最大值
${
max
}
`
,
})
}
const
rule
=
getValidateRule
(
field
.
type
)
rule
&&
rules
.
push
(
rule
)
return
rules
}
/**
* 字段编辑器
*/
export
function
getFieldEditor
(
field
:
SchemaFieldV2
,
key
:
number
)
{
const
{
name
,
type
,
min
,
max
,
enumElements
}
=
field
let
FieldEditor
:
React
.
ReactNode
switch
(
type
)
{
case
'
String
'
:
FieldEditor
=
<
Input
type
=
"text"
/>
break
case
'
MultiLineString
'
:
FieldEditor
=
<
TextArea
/>
break
case
'
Boolean
'
:
FieldEditor
=
<
Switch
checkedChildren
=
"True"
unCheckedChildren
=
"False"
/>
break
case
'
Number
'
:
FieldEditor
=
<
InputNumber
style
=
{
{
width
:
'
100%
'
}
}
min
=
{
min
}
max
=
{
max
}
/>
break
case
'
Url
'
:
FieldEditor
=
<
Input
/>
break
case
'
Email
'
:
FieldEditor
=
<
Input
/>
break
case
'
Tel
'
:
FieldEditor
=
<
Input
style
=
{
{
width
:
'
100%
'
}
}
/>
break
case
'
Date
'
:
FieldEditor
=
<
IDatePicker
type
=
"Date"
/>
break
case
'
DateTime
'
:
FieldEditor
=
<
IDatePicker
type
=
"DateTime"
/>
break
case
'
Image
'
:
FieldEditor
=
<
IUploader
type
=
"image"
/>
break
case
'
File
'
:
FieldEditor
=
<
IUploader
type
=
"file"
/>
break
case
'
Enum
'
:
FieldEditor
=
(
<
Select
>
{
enumElements
?.
length
?
(
enumElements
?.
map
((
ele
,
index
)
=>
(
<
Option
value
=
{
ele
.
value
}
key
=
{
index
}
>
{
ele
.
label
}
</
Option
>
))
)
:
(
<
Option
value
=
""
disabled
>
空
</
Option
>
)
}
</
Select
>
)
break
case
'
Array
'
:
FieldEditor
=
(
<
Form
.
List
name
=
{
name
}
>
{
(
fields
,
{
add
,
remove
})
=>
{
return
(
<
div
>
{
fields
?.
map
((
field
,
index
)
=>
{
return
(
<
Form
.
Item
key
=
{
index
}
>
<
Form
.
Item
{
...
field
}
noStyle
validateTrigger
=
{
[
'
onChange
'
,
'
onBlur
'
]
}
>
<
Input
style
=
{
{
width
:
'
60%
'
}
}
/>
</
Form
.
Item
>
<
MinusCircleOutlined
className
=
"dynamic-delete-button"
style
=
{
{
margin
:
'
0 8px
'
}
}
onClick
=
{
()
=>
{
remove
(
field
.
name
)
}
}
/>
</
Form
.
Item
>
)
})
}
<
Form
.
Item
>
<
Button
type
=
"dashed"
onClick
=
{
()
=>
{
add
()
}
}
style
=
{
{
width
:
'
60%
'
}
}
>
<
PlusOutlined
/>
添加字段
</
Button
>
</
Form
.
Item
>
</
div
>
)
}
}
</
Form
.
List
>
)
break
case
'
Markdown
'
:
FieldEditor
=
<
LazyMarkdownEditor
key
=
{
key
}
/>
break
case
'
RichText
'
:
FieldEditor
=
<
LazyRichTextEditor
key
=
{
String
(
key
)
}
/>
break
case
'
Connect
'
:
FieldEditor
=
<
IConnectEditor
field
=
{
field
}
/>
break
default
:
FieldEditor
=
<
Input
/>
}
return
FieldEditor
}
/**
* 字段编辑表单
*/
export
function
getFieldFormItem
(
field
:
SchemaFieldV2
,
key
:
number
)
{
const
rules
=
getRules
(
field
)
const
{
name
,
type
,
description
,
displayName
}
=
field
let
FieldEditor
:
any
=
getFieldEditor
(
field
,
key
)
let
FormItem
switch
(
type
)
{
case
'
Boolean
'
:
FormItem
=
(
<
Form
.
Item
key
=
{
key
}
name
=
{
name
}
rules
=
{
rules
}
label
=
{
displayName
}
extra
=
{
description
}
valuePropName
=
"checked"
>
{
FieldEditor
}
</
Form
.
Item
>
)
break
default
:
FormItem
=
(
<
Form
.
Item
key
=
{
key
}
name
=
{
name
}
rules
=
{
rules
}
label
=
{
displayName
}
extra
=
{
description
}
>
{
FieldEditor
}
</
Form
.
Item
>
)
}
// 弹性布局
if
(
type
===
'
Markdown
'
||
type
===
'
RichText
'
)
{
return
(
<
Col
xs
=
{
24
}
sm
=
{
24
}
md
=
{
24
}
lg
=
{
24
}
xl
=
{
24
}
key
=
{
key
}
>
{
FormItem
}
</
Col
>
)
}
return
(
<
Col
xs
=
{
24
}
sm
=
{
24
}
md
=
{
12
}
lg
=
{
12
}
xl
=
{
12
}
xxl
=
{
12
}
key
=
{
key
}
>
{
FormItem
}
</
Col
>
)
}
This diff is collapsed.
Click to expand it.
packages/admin/src/components/Fields/FieldRender.tsx
0 → 100644
+
163
-
0
View file @
195829ff
import
React
from
'
react
'
import
{
Space
,
Tag
,
Typography
}
from
'
antd
'
import
{
IConnectRender
,
IFileRender
,
ILazyImage
}
from
'
@/components/Fields
'
import
{
calculateFieldWidth
}
from
'
./utils
'
/**
* 根据类型获取展示字段组件
*/
export
function
getFieldRender
(
field
:
SchemaFieldV2
)
{
const
{
name
,
type
}
=
field
const
width
=
calculateFieldWidth
(
field
)
switch
(
type
)
{
case
'
String
'
:
return
(
text
:
React
.
ReactNode
,
record
:
any
,
index
:
number
,
action
:
any
):
React
.
ReactNode
|
React
.
ReactNode
[]
=>
(
<
Typography
.
Text
ellipsis
style
=
{
{
width
}
}
>
{
text
}
</
Typography
.
Text
>
)
case
'
Text
'
:
case
'
MultiLineString
'
:
return
(
text
:
React
.
ReactNode
,
record
:
any
,
index
:
number
,
action
:
any
):
React
.
ReactNode
|
React
.
ReactNode
[]
=>
(
<
Typography
.
Text
ellipsis
style
=
{
{
width
}
}
>
{
text
}
</
Typography
.
Text
>
)
case
'
Boolean
'
:
return
(
text
:
React
.
ReactNode
,
record
:
any
,
index
:
number
,
action
:
any
):
React
.
ReactNode
|
React
.
ReactNode
[]
=>
{
return
<
Typography
.
Text
>
{
record
[
name
]
?
'
True
'
:
'
False
'
}
</
Typography
.
Text
>
}
case
'
Number
'
:
return
(
text
:
React
.
ReactNode
,
record
:
any
,
index
:
number
,
action
:
any
):
React
.
ReactNode
|
React
.
ReactNode
[]
=>
{
const
num
=
typeof
record
[
name
]
===
'
undefined
'
?
'
-
'
:
record
[
name
]
return
<
Typography
.
Text
>
{
num
}
</
Typography
.
Text
>
}
case
'
Url
'
:
return
(
text
:
React
.
ReactNode
,
record
:
any
,
index
:
number
,
action
:
any
):
React
.
ReactNode
|
React
.
ReactNode
[]
=>
(
<
Typography
.
Link
href
=
{
record
[
name
]
}
target
=
"_blank"
>
{
text
}
</
Typography
.
Link
>
)
case
'
Email
'
:
return
(
text
:
React
.
ReactNode
,
record
:
any
,
index
:
number
,
action
:
any
):
React
.
ReactNode
|
React
.
ReactNode
[]
=>
<
Typography
.
Text
>
{
text
}
</
Typography
.
Text
>
case
'
Tel
'
:
return
(
text
:
React
.
ReactNode
,
record
:
any
,
index
:
number
,
action
:
any
):
React
.
ReactNode
|
React
.
ReactNode
[]
=>
<
Typography
.
Text
>
{
text
}
</
Typography
.
Text
>
case
'
Date
'
:
return
undefined
case
'
DateTime
'
:
return
undefined
case
'
Image
'
:
return
(
text
:
React
.
ReactNode
,
record
:
any
,
index
:
number
,
action
:
any
):
React
.
ReactNode
|
React
.
ReactNode
[]
=>
{
const
data
=
record
[
name
]
return
<
ILazyImage
src
=
{
data
}
/>
}
case
'
File
'
:
return
(
text
:
React
.
ReactNode
,
record
:
any
,
index
:
number
,
action
:
any
):
React
.
ReactNode
|
React
.
ReactNode
[]
=>
<
IFileRender
src
=
{
record
[
name
]
}
/>
case
'
Array
'
:
return
(
text
:
React
.
ReactNode
,
record
:
any
,
index
:
number
,
action
:
any
):
React
.
ReactNode
|
React
.
ReactNode
[]
=>
{
if
(
!
record
[
name
])
{
return
text
}
return
(
<
Space
direction
=
"vertical"
>
{
record
[
name
]?.
map
((
val
:
string
,
index
:
number
)
=>
(
<
Tag
key
=
{
index
}
>
{
val
}
</
Tag
>
))
}
</
Space
>
)
}
case
'
Markdown
'
:
return
(
text
:
React
.
ReactNode
,
record
:
any
,
index
:
number
,
action
:
any
):
React
.
ReactNode
|
React
.
ReactNode
[]
=>
(
<
Typography
.
Text
ellipsis
style
=
{
{
width
}
}
>
{
text
}
</
Typography
.
Text
>
)
case
'
RichText
'
:
return
(
text
:
React
.
ReactNode
,
record
:
any
,
index
:
number
,
action
:
any
):
React
.
ReactNode
|
React
.
ReactNode
[]
=>
(
<
Typography
.
Text
ellipsis
style
=
{
{
width
}
}
>
{
text
}
</
Typography
.
Text
>
)
case
'
Connect
'
:
return
(
text
:
React
.
ReactNode
,
record
:
any
,
index
:
number
,
action
:
any
):
React
.
ReactNode
|
React
.
ReactNode
[]
=>
(
<
IConnectRender
value
=
{
record
[
name
]
}
field
=
{
field
}
/>
)
default
:
return
(
text
:
React
.
ReactNode
,
record
:
any
,
index
:
number
,
action
:
any
):
React
.
ReactNode
|
React
.
ReactNode
[]
=>
text
}
}
This diff is collapsed.
Click to expand it.
packages/admin/src/components/Fields/Image.tsx
0 → 100644
+
78
-
0
View file @
195829ff
import
React
,
{
useEffect
,
useState
}
from
'
react
'
import
{
Button
,
message
,
Space
,
Spin
,
Empty
}
from
'
antd
'
import
{
copyToClipboard
,
downloadFile
,
getTempFileURL
}
from
'
@/utils
'
import
{
CopyTwoTone
}
from
'
@ant-design/icons
'
import
emptyImg
from
'
@/assets/empty.svg
'
/**
* 图片懒加载
*/
export
const
ILazyImage
:
React
.
FC
<
{
src
:
string
}
>
=
({
src
})
=>
{
if
(
!
src
)
{
return
<
Empty
image
=
{
emptyImg
}
imageStyle
=
{
{
height
:
'
60px
'
}
}
description
=
"未设定图片"
/>
}
if
(
!
/^cloud:
\/\/\S
+/
.
test
(
src
))
{
return
<
img
style
=
{
{
maxHeight
:
'
120px
'
,
maxWidth
:
'
200px
'
}
}
src
=
{
src
}
/>
}
const
[
imgUrl
,
setImgUrl
]
=
useState
(
''
)
const
[
loading
,
setLoading
]
=
useState
(
true
)
useEffect
(()
=>
{
getTempFileURL
(
src
)
.
then
((
url
)
=>
{
setLoading
(
false
)
setImgUrl
(
url
)
})
.
catch
((
e
)
=>
{
console
.
log
(
e
)
console
.
log
(
e
.
message
)
message
.
error
(
`获取图片链接失败
${
e
.
message
}
`
)
setLoading
(
false
)
})
},
[])
return
loading
?
(
<
Spin
/>
)
:
(
<
Space
direction
=
"vertical"
>
<
img
style
=
{
{
maxHeight
:
'
120px
'
,
maxWidth
:
'
200px
'
}
}
src
=
{
imgUrl
}
/>
{
imgUrl
&&
(
<
Space
>
<
Button
size
=
"small"
onClick
=
{
()
=>
{
downloadFile
(
src
)
}
}
>
下载图片
</
Button
>
<
Button
size
=
"small"
onClick
=
{
()
=>
{
getTempFileURL
(
src
)
.
then
((
url
)
=>
{
copyToClipboard
(
url
)
.
then
(()
=>
{
message
.
success
(
'
复制到剪切板成功
'
)
})
.
catch
(()
=>
{
message
.
error
(
'
复制到剪切板成功
'
)
})
})
.
catch
((
e
)
=>
{
console
.
log
(
e
)
console
.
log
(
e
.
message
)
message
.
error
(
`获取图片链接失败
${
e
.
message
}
`
)
})
}
}
>
访问链接
<
CopyTwoTone
/>
</
Button
>
</
Space
>
)
}
</
Space
>
)
}
This diff is collapsed.
Click to expand it.
packages/admin/src/components/Fields/utils.ts
0 → 100644
+
57
-
0
View file @
195829ff
// 格式化搜索参数
export
const
formatSearchData
=
(
schema
:
SchemaV2
,
params
:
Record
<
string
,
any
>
)
=>
{
const
{
fields
}
=
schema
return
Object
.
keys
(
params
).
reduce
((
ret
,
key
)
=>
{
const
field
=
fields
.
find
((
_
)
=>
_
.
name
===
key
)
if
(
!
field
)
{
return
{
...
ret
,
[
key
]:
params
[
key
],
}
}
let
value
=
params
[
key
]
// 格式化字符串
if
(
field
.
type
===
'
Number
'
)
{
value
=
Number
(
value
)
}
if
(
field
.
type
===
'
Boolean
'
)
{
value
=
Boolean
(
value
)
}
return
{
...
ret
,
[
key
]:
value
,
}
},
{})
}
export
const
calculateFieldWidth
=
(
field
:
SchemaFieldV2
)
=>
{
const
TypeWidthMap
=
{
String
:
150
,
MultiLineString
:
150
,
Number
:
150
,
Boolean
:
100
,
DateTime
:
150
,
File
:
200
,
Image
:
200
,
RichText
:
150
,
Markdown
:
150
,
Connect
:
250
,
}
const
{
displayName
,
type
}
=
field
// 计算列宽度
const
nameWidth
=
displayName
.
length
*
25
let
width
if
(
TypeWidthMap
[
type
])
{
width
=
nameWidth
>
TypeWidthMap
[
type
]
?
nameWidth
:
TypeWidthMap
[
type
]
}
else
{
width
=
nameWidth
>
150
?
nameWidth
:
150
}
return
width
}
This diff is collapsed.
Click to expand it.
packages/admin/src/pages/index.tsx
+
2
-
2
View file @
195829ff
...
...
@@ -26,7 +26,6 @@ import {
Popover
,
Empty
,
}
from
'
antd
'
import
moment
from
'
moment
'
import
{
useConcent
}
from
'
concent
'
import
{
history
,
useRequest
,
useAccess
}
from
'
umi
'
import
AvatarDropdown
from
'
@/components/AvatarDropdown
'
...
...
@@ -34,6 +33,7 @@ import { getProjects, createProject } from '@/services/project'
import
logo
from
'
@/assets/logo.svg
'
import
{
getCmsNotices
}
from
'
@/services/notice
'
import
'
./index.less
'
import
{
getFullDate
}
from
'
@/utils
'
setTwoToneColor
(
'
#0052d9
'
)
...
...
@@ -232,7 +232,7 @@ export const NoticeRender: React.FC = () => {
<
Timeline
mode
=
"left"
>
{
notices
.
map
((
notice
:
any
)
=>
(
<
Timeline
.
Item
key
=
{
notice
.
_id
}
color
=
"blue"
>
<
h3
>
{
moment
(
notice
.
noticeTime
)
.
format
(
'
YYYY-MM-DD
'
)
}
</
h3
>
<
h3
>
{
getFullDate
(
notice
.
noticeTime
)
}
</
h3
>
<
h3
>
{
notice
.
noticeTitle
}
</
h3
>
<
p
>
{
notice
.
noticeContent
}
</
p
>
</
Timeline
.
Item
>
...
...
This diff is collapsed.
Click to expand it.
packages/admin/src/pages/project/content/ContentEditor.tsx
0 → 100644
+
127
-
0
View file @
195829ff
import
React
from
'
react
'
import
{
useParams
,
useRequest
,
history
}
from
'
umi
'
import
{
useConcent
}
from
'
concent
'
import
{
Form
,
message
,
Space
,
Button
,
Row
,
Col
,
Input
}
from
'
antd
'
import
{
createContent
,
updateContent
}
from
'
@/services/content
'
import
{
getFieldFormItem
}
from
'
@/components/Fields
'
import
ProCard
from
'
@ant-design/pro-card
'
import
{
PageContainer
}
from
'
@ant-design/pro-layout
'
import
{
LeftCircleTwoTone
}
from
'
@ant-design/icons
'
const
ContentEditor
:
React
.
FC
=
()
=>
{
const
{
schemaId
,
projectId
}
=
useParams
<
any
>
()
const
ctx
=
useConcent
(
'
content
'
)
const
{
selectedContent
,
contentAction
}
=
ctx
.
state
const
{
state
:
{
schemas
},
}
=
ctx
const
schema
:
SchemaV2
=
schemas
?.
find
((
item
:
SchemaV2
)
=>
item
.
_id
===
schemaId
)
// 表单初始值
const
initialValues
=
getInitialValues
(
contentAction
,
schema
,
selectedContent
)
// 创建/更新内容
const
{
run
,
loading
}
=
useRequest
(
async
(
payload
:
any
)
=>
{
if
(
contentAction
===
'
create
'
)
{
await
createContent
(
projectId
,
schema
?.
collectionName
,
payload
)
}
if
(
contentAction
===
'
edit
'
)
{
await
updateContent
(
projectId
,
schema
?.
collectionName
,
selectedContent
.
_id
,
payload
)
}
},
{
manual
:
true
,
onError
:
()
=>
{
message
.
error
(
`
${
contentAction
===
'
create
'
?
'
新建
'
:
'
更新
'
}
内容失败`
)
},
onSuccess
:
()
=>
{
message
.
success
(
`
${
contentAction
===
'
create
'
?
'
新建
'
:
'
更新
'
}
内容成功`
)
// 返回
history
.
goBack
()
},
}
)
return
(
<
PageContainer
title
=
{
`
${
contentAction
===
'
create
'
?
'
创建
'
:
'
更新
'
}
【
${
schema
?.
displayName
}
】内容`
}
>
<
div
style
=
{
{
cursor
:
'
pointer
'
}
}
onClick
=
{
()
=>
history
.
goBack
()
}
>
<
Space
align
=
"center"
style
=
{
{
marginBottom
:
'
10px
'
}
}
>
<
LeftCircleTwoTone
style
=
{
{
fontSize
:
'
20px
'
}
}
/>
<
h3
style
=
{
{
marginBottom
:
'
0.25rem
'
}
}
>
返回
</
h3
>
</
Space
>
</
div
>
<
ProCard
>
<
Form
name
=
"basic"
layout
=
"vertical"
initialValues
=
{
initialValues
}
onFinish
=
{
(
v
=
{})
=>
run
(
v
)
}
>
<
Row
gutter
=
{
[
24
,
24
]
}
>
{
contentAction
===
'
edit
'
&&
(
<
Col
xs
=
{
24
}
sm
=
{
24
}
md
=
{
12
}
lg
=
{
12
}
xl
=
{
12
}
xxl
=
{
12
}
>
<
Form
.
Item
label
=
"文档 Id"
name
=
"_id"
>
<
Input
type
=
"text"
disabled
/>
</
Form
.
Item
>
</
Col
>
)
}
{
schema
?.
fields
?.
map
((
filed
,
index
)
=>
getFieldFormItem
(
filed
,
index
))
}
</
Row
>
<
Form
.
Item
>
<
Row
>
<
Col
flex
=
"1 1 auto"
style
=
{
{
textAlign
:
'
right
'
}
}
>
<
Space
size
=
"large"
>
<
Button
onClick
=
{
()
=>
{
history
.
goBack
()
}
}
>
取消
</
Button
>
<
Button
type
=
"primary"
htmlType
=
"submit"
loading
=
{
loading
}
>
{
contentAction
===
'
create
'
?
'
创建
'
:
'
更新
'
}
</
Button
>
</
Space
>
</
Col
>
</
Row
>
</
Form
.
Item
>
</
Form
>
</
ProCard
>
</
PageContainer
>
)
}
const
getInitialValues
=
(
action
:
string
,
schema
:
SchemaV2
,
selectedContent
:
any
)
=>
{
const
initialValues
=
action
===
'
create
'
?
schema
?.
fields
?.
reduce
((
prev
,
field
)
=>
{
let
{
type
,
defaultValue
}
=
field
// 布尔值默认为 false
if
(
type
===
'
Boolean
'
&&
typeof
defaultValue
!==
'
boolean
'
)
{
defaultValue
=
false
}
return
{
...
prev
,
[
field
.
name
]:
defaultValue
,
}
},
{})
:
selectedContent
if
(
action
===
'
edit
'
)
{
schema
?.
fields
?.
forEach
((
field
)
=>
{
let
{
type
,
name
}
=
field
// 布尔值默认为 false
if
(
type
===
'
Boolean
'
&&
typeof
selectedContent
[
name
]
!==
'
boolean
'
)
{
selectedContent
[
name
]
=
false
}
})
}
return
initialValues
}
export
default
ContentEditor
This diff is collapsed.
Click to expand it.
packages/admin/src/pages/project/content/ContentTable.tsx
+
65
-
29
View file @
195829ff
import
React
,
{
useRef
,
useCallback
,
useState
,
useMemo
}
from
'
react
'
import
React
,
{
useRef
,
useCallback
,
useState
,
useMemo
,
useEffect
}
from
'
react
'
import
{
useConcent
}
from
'
concent
'
import
{
useParams
,
history
}
from
'
umi
'
import
ProTable
from
'
@ant-design/pro-table
'
...
...
@@ -14,17 +14,27 @@ import {
Upload
,
Progress
,
Alert
,
Typography
,
Select
,
}
from
'
antd
'
import
{
PlusOutlined
,
DeleteOutlined
,
FilterOutlined
,
InboxOutlined
}
from
'
@ant-design/icons
'
import
{
getContents
,
deleteContent
,
batchDeleteContent
}
from
'
@/services/content
'
import
{
getContents
,
deleteContent
,
batchDeleteContent
,
createMigrateJobs
,
}
from
'
@/services/content
'
import
{
random
,
uploadFile
}
from
'
@/utils
'
import
{
getTableColumns
}
from
'
./columns
'
import
{
ContentTableSearch
}
from
'
./SearchForm
'
import
'
./index.less
'
import
{
getFullDate
}
from
'
@/utils/date
'
// 不能支持搜索的类型
const
negativeTypes
=
[
'
File
'
,
'
Image
'
]
const
{
Dragger
}
=
Upload
const
{
Title
}
=
Typography
const
{
Option
}
=
Select
/**
* 内容展示表格
...
...
@@ -226,7 +236,7 @@ export const ContentTable: React.FC<{
>
新建
</
Button
>,
<
DataImport
key
=
"import"
/>,
<
DataImport
key
=
"import"
collectionName
=
{
currentSchema
.
collectionName
}
/>,
]
}
/>
</>
...
...
@@ -236,13 +246,19 @@ export const ContentTable: React.FC<{
/**
* 导入数据
*/
export
const
DataImport
:
React
.
FC
<
{}
>
=
()
=>
{
export
const
DataImport
:
React
.
FC
<
{
collectionName
:
string
}
>
=
({
collectionName
})
=>
{
const
{
projectId
}
=
useParams
<
any
>
()
const
[
visible
,
setVisible
]
=
useState
(
false
)
const
[
dataType
,
setDataType
]
=
useState
<
string
>
(
''
)
const
[
fileList
,
setFileList
]
=
useState
<
any
[]
>
()
const
[
percent
,
setPercent
]
=
useState
(
0
)
const
[
uploading
,
setUploading
]
=
useState
(
false
)
const
[
dataType
,
setDataType
]
=
useState
<
string
>
(
''
)
const
[
conflictMode
,
setConflictMode
]
=
useState
(
'
insert
'
)
useEffect
(()
=>
{
if
(
!
visible
)
{
setPercent
(
0
)
}
},
[
visible
])
return
(
<>
...
...
@@ -250,12 +266,17 @@ export const DataImport: React.FC<{}> = () => {
overlay
=
{
<
Menu
onClick
=
{
({
key
})
=>
{
if
(
key
===
'
record
'
)
{
history
.
push
(
`/
${
projectId
}
/content/migrate`
)
return
}
setDataType
(
key
as
string
)
setVisible
(
true
)
}
}
>
<
Menu
.
Item
key
=
"csv"
>
通过 CSV 导入
</
Menu
.
Item
>
<
Menu
.
Item
key
=
"json"
>
通过 JSON 导入
</
Menu
.
Item
>
<
Menu
.
Item
key
=
"record"
>
查看导入记录
</
Menu
.
Item
>
</
Menu
>
}
key
=
"search"
...
...
@@ -263,53 +284,68 @@ export const DataImport: React.FC<{}> = () => {
<
Button
type
=
"primary"
>
导入数据
</
Button
>
</
Dropdown
>
<
Modal
destroyOnClose
width
=
{
600
}
title
=
"导入数据"
footer
=
{
null
}
closable
=
{
true
}
visible
=
{
visible
}
onCancel
=
{
()
=>
setVisible
(
false
)
}
>
<
Alert
message
=
"JSON 数据不是数组,而是类似 JSON Lines,即各个记录对象之间使用 \n 分隔,而非逗号"
style
=
{
{
marginBottom
:
'
10px
'
}
}
/>
<
Alert
message
=
"CSV 格式的数据默认以第一行作为导入后的所有键名,余下的每一行则是与首行键名一一对应的键值记录"
/>
<
Title
level
=
{
4
}
>
注意事项
</
Title
>
{
dataType
===
'
json
'
&&
(
<
Alert
message
=
"JSON 数据不是数组,而是类似 JSON Lines,即各个记录对象之间使用 \n 分隔,而非逗号"
style
=
{
{
marginBottom
:
'
10px
'
}
}
/>
)
}
{
dataType
===
'
csv
'
&&
(
<
Alert
message
=
"CSV 格式的数据默认以第一行作为导入后的所有键名,余下的每一行则是与首行键名一一对应的键值记录"
/>
)
}
<
br
/>
<
Title
level
=
{
4
}
>
冲突处理模式
</
Title
>
<
Select
defaultValue
=
"insert"
onChange
=
{
setConflictMode
}
style
=
{
{
width
:
'
100%
'
,
marginBottom
:
'
10px
'
}
}
>
<
Option
value
=
"insert"
>
Insert(会在导入时总是插入新记录,出现 _id 冲突时会报错)
</
Option
>
<
Option
value
=
"upsert"
>
Upsert(会判断有无该条记录,如果有则更新记录,否则就插入一条新记录)
</
Option
>
</
Select
>
<
Dragger
accept
=
".csv,.json"
fileList
=
{
fileList
}
listType
=
"picture"
beforeUpload
=
{
(
file
)
=>
{
setUploading
(
true
)
setPercent
(
0
)
const
fileName
=
`
${
random
(
32
)}
-
${
file
.
name
}
`
// 文件路径
const
filePath
=
`data-import/
${
random
(
32
)}
-
${
file
.
name
}
`
// 上传文件
uploadFile
(
file
,
(
percent
)
=>
{
setPercent
(
percent
)
},
fileName
).
then
((
fileUrl
)
=>
{
setFileList
([
{
uid
:
fileUrl
,
name
:
file
.
name
,
status
:
'
done
'
,
},
])
setVisible
(
false
)
// TODO: 创建导入任务
console
.
log
(
fileName
)
message
.
success
(
'
上传文件成功,数据导入中
'
)
})
filePath
)
.
then
(()
=>
createMigrateJobs
(
projectId
,
collectionName
,
filePath
,
conflictMode
))
.
then
(()
=>
{
setVisible
(
false
)
message
.
success
(
'
上传文件成功,数据导入中
'
)
})
.
catch
((
e
)
=>
{
message
.
error
(
`导入文件失败:
${
e
.
message
}
`
)
setVisible
(
false
)
})
return
false
}
}
>
<
p
className
=
"ant-upload-drag-icon"
>
<
InboxOutlined
/>
</
p
>
<
p
className
=
"ant-upload-text"
>
点击或拖拽
文件
上传
</
p
>
<
p
className
=
"ant-upload-text"
>
点击或拖拽上传
文件,开始导入数据
</
p
>
</
Dragger
>
{
uploading
&&
<
Progress
style
=
{
{
paddingTop
:
'
10px
'
}
}
percent
=
{
percent
}
/>
}
</
Modal
>
...
...
This diff is collapsed.
Click to expand it.
packages/admin/src/pages/project/content/SearchForm.tsx
0 → 100644
+
162
-
0
View file @
195829ff
import
React
from
'
react
'
import
{
Form
,
Space
,
Button
,
Row
,
Col
,
Input
,
Switch
,
InputNumber
,
Select
}
from
'
antd
'
import
{
DeleteTwoTone
}
from
'
@ant-design/icons
'
import
{
IDatePicker
,
IConnectEditor
}
from
'
@/components/Fields
'
import
{
calculateFieldWidth
}
from
'
./utils
'
const
{
Option
}
=
Select
export
const
ContentTableSearch
:
React
.
FC
<
{
schema
:
SchemaV2
searchFields
:
SchemaFieldV2
[]
searchValues
:
any
onSearch
:
(
v
:
Record
<
string
,
any
>
)
=>
void
setSearchFields
:
(
fields
:
SchemaFieldV2
[])
=>
void
}
>
=
({
schema
,
onSearch
,
searchFields
,
searchValues
=
{},
setSearchFields
})
=>
{
// 删除字段
const
deleteField
=
(
field
:
SchemaFieldV2
)
=>
{
const
index
=
searchFields
.
findIndex
((
_
)
=>
_
.
id
===
field
.
id
)
const
fields
=
searchFields
.
slice
(
0
)
fields
.
splice
(
index
,
1
)
setSearchFields
(
fields
)
}
return
(
<
div
>
{
searchFields
.
length
?
(
<
Form
name
=
"basic"
layout
=
"inline"
initialValues
=
{
searchValues
}
onFinish
=
{
(
v
:
any
)
=>
onSearch
(
v
)
}
style
=
{
{
marginTop
:
'
15px
'
}
}
>
<
Row
>
{
searchFields
.
map
((
field
,
index
)
=>
(
<
Space
key
=
{
index
}
align
=
"center"
style
=
{
{
marginRight
:
'
15px
'
}
}
>
{
getSearchFieldItem
(
field
,
index
)
}
<
DeleteTwoTone
onClick
=
{
()
=>
deleteField
(
field
)
}
style
=
{
{
marginLeft
:
'
-15px
'
}
}
/>
</
Space
>
))
}
</
Row
>
<
Row
>
<
Col
flex
=
"1 1 auto"
style
=
{
{
textAlign
:
'
right
'
}
}
>
<
Form
.
Item
>
<
Space
>
<
Button
type
=
"primary"
onClick
=
{
()
=>
{
setSearchFields
([])
onSearch
({})
}
}
>
重置
</
Button
>
<
Button
type
=
"primary"
htmlType
=
"submit"
>
搜索
</
Button
>
</
Space
>
</
Form
.
Item
>
</
Col
>
</
Row
>
</
Form
>
)
:
null
}
</
div
>
)
}
/**
* 字段编辑
*/
export
function
getSearchFieldItem
(
field
:
SchemaFieldV2
,
key
:
number
)
{
const
{
name
,
type
,
min
,
max
,
displayName
,
enumElements
}
=
field
const
width
=
calculateFieldWidth
(
field
)
let
FormItem
switch
(
type
)
{
case
'
String
'
:
case
'
Url
'
:
case
'
Email
'
:
case
'
Tel
'
:
case
'
Markdown
'
:
case
'
RichText
'
:
case
'
MultiLineString
'
:
FormItem
=
(
<
Form
.
Item
key
=
{
key
}
name
=
{
name
}
label
=
{
displayName
}
>
<
Input
type
=
"text"
style
=
{
{
width
}
}
/>
</
Form
.
Item
>
)
break
case
'
Boolean
'
:
FormItem
=
(
<
Form
.
Item
key
=
{
key
}
name
=
{
name
}
label
=
{
displayName
}
valuePropName
=
"checked"
>
<
Switch
checkedChildren
=
"True"
unCheckedChildren
=
"False"
/>
</
Form
.
Item
>
)
break
case
'
Number
'
:
FormItem
=
(
<
Form
.
Item
key
=
{
key
}
name
=
{
name
}
label
=
{
displayName
}
>
<
InputNumber
min
=
{
min
}
max
=
{
max
}
style
=
{
{
width
}
}
/>
</
Form
.
Item
>
)
break
case
'
Date
'
:
FormItem
=
(
<
Form
.
Item
key
=
{
key
}
name
=
{
name
}
label
=
{
displayName
}
>
<
IDatePicker
type
=
"Date"
/>
</
Form
.
Item
>
)
break
case
'
DateTime
'
:
FormItem
=
(
<
Form
.
Item
key
=
{
key
}
name
=
{
name
}
label
=
{
displayName
}
>
<
IDatePicker
type
=
"DateTime"
/>
</
Form
.
Item
>
)
break
case
'
Enum
'
:
FormItem
=
(
<
Form
.
Item
key
=
{
key
}
name
=
{
name
}
label
=
{
displayName
}
>
<
Select
mode
=
"multiple"
style
=
{
{
width
}
}
>
{
enumElements
?.
length
?
(
enumElements
?.
map
((
ele
,
index
)
=>
(
<
Option
value
=
{
ele
.
value
}
key
=
{
index
}
>
{
ele
.
label
}
</
Option
>
))
)
:
(
<
Option
value
=
""
disabled
>
空
</
Option
>
)
}
</
Select
>
</
Form
.
Item
>
)
break
case
'
Connect
'
:
FormItem
=
(
<
Form
.
Item
key
=
{
key
}
name
=
{
name
}
label
=
{
displayName
}
>
<
IConnectEditor
field
=
{
field
}
/>
</
Form
.
Item
>
)
break
case
'
Array
'
:
FormItem
=
(
<
Form
.
Item
key
=
{
key
}
name
=
{
name
}
label
=
{
displayName
}
>
<
Input
placeholder
=
"目前只支持单个值搜索"
style
=
{
{
width
}
}
/>
</
Form
.
Item
>
)
break
default
:
FormItem
=
(
<
Form
.
Item
key
=
{
key
}
name
=
{
name
}
label
=
{
displayName
}
>
<
Input
style
=
{
{
width
}
}
/>
</
Form
.
Item
>
)
}
return
FormItem
}
This diff is collapsed.
Click to expand it.
packages/admin/src/pages/project/migrate/index.tsx
0 → 100644
+
127
-
0
View file @
195829ff
import
{
useParams
}
from
'
umi
'
import
React
,
{
useRef
}
from
'
react
'
import
{
PageContainer
}
from
'
@ant-design/pro-layout
'
import
ProTable
,
{
ProColumns
}
from
'
@ant-design/pro-table
'
import
{
getMigrateJobs
}
from
'
@/services/content
'
const
StatusMap
=
{
waiting
:
'
等待中
'
,
reading
:
'
读
'
,
writing
:
'
写
'
,
migrating
:
'
转移中
'
,
success
:
'
成功
'
,
fail
:
'
失败
'
,
}
interface
MigrateJobDto
{
// 项目 Id
projectId
:
string
// 任务 Id
jobId
:
number
// 导入文件路径
filePath
:
string
// 导入冲突处理模式
conflictMode
:
'
upsert
'
|
'
insert
'
createTime
:
number
collectionName
:
string
// 任务状态
// waiting:等待中,reading:读,writing:写,migrating:转移中,success:成功,fail:失败
status
:
string
}
const
MigrateJobColumns
:
ProColumns
<
MigrateJobDto
>
[]
=
[
{
title
:
'
JobId
'
,
dataIndex
:
'
jobId
'
,
},
{
title
:
'
处理冲突模式
'
,
dataIndex
:
'
conflictMode
'
,
},
{
title
:
'
数据集合
'
,
dataIndex
:
'
collectionName
'
,
},
{
width
:
'
200px
'
,
title
:
'
创建时间
'
,
dataIndex
:
'
createTime
'
,
valueType
:
'
dateTime
'
,
},
{
title
:
'
状态
'
,
dataIndex
:
'
collections
'
,
render
:
(
_
,
row
)
=>
StatusMap
[
row
.
status
],
},
]
const
columns
:
ProColumns
<
MigrateJobDto
>
[]
=
MigrateJobColumns
.
map
((
item
)
=>
({
...
item
,
align
:
'
center
'
,
}))
export
default
():
React
.
ReactNode
=>
{
const
{
projectId
}
=
useParams
<
any
>
()
const
tableRef
=
useRef
<
{
reload
:
(
resetPageIndex
?:
boolean
)
=>
void
reloadAndRest
:
()
=>
void
fetchMore
:
()
=>
void
reset
:
()
=>
void
clearSelected
:
()
=>
void
}
>
()
// 获取 jobs
const
tableRequest
=
async
(
params
:
{
pageSize
:
number
;
current
:
number
;
[
key
:
string
]:
any
},
sort
:
{
[
key
:
string
]:
'
ascend
'
|
'
descend
'
|
null
},
filter
:
{
[
key
:
string
]:
React
.
ReactText
[]
}
)
=>
{
const
{
current
,
pageSize
}
=
params
try
{
const
{
data
=
[],
total
}
=
await
getMigrateJobs
(
projectId
,
current
,
pageSize
)
return
{
data
,
total
,
success
:
true
,
}
}
catch
(
error
)
{
console
.
log
(
error
)
return
{
data
:
[],
total
:
0
,
success
:
true
,
}
}
}
return
(
<
PageContainer
title
=
"数据迁移"
className
=
"page-container"
>
<
ProTable
rowKey
=
"_id"
search
=
{
false
}
defaultData
=
{
[]
}
actionRef
=
{
tableRef
}
dateFormatter
=
"string"
scroll
=
{
{
x
:
1200
}
}
request
=
{
tableRequest
}
pagination
=
{
{
showSizeChanger
:
true
,
}
}
columns
=
{
columns
}
/>
</
PageContainer
>
)
}
This diff is collapsed.
Click to expand it.
packages/admin/src/services/content.ts
+
27
-
0
View file @
195829ff
...
...
@@ -105,3 +105,30 @@ export async function updateContent(
},
})
}
export
async
function
getMigrateJobs
(
projectId
:
string
,
page
=
1
,
pageSize
=
10
)
{
return
tcbRequest
(
`/projects/
${
projectId
}
/migrate`
,
{
method
:
'
GET
'
,
params
:
{
page
,
pageSize
,
},
})
}
export
async
function
createMigrateJobs
(
projectId
:
string
,
collectionName
:
string
,
filePath
:
string
,
conflictMode
:
string
)
{
return
tcbRequest
(
`/projects/
${
projectId
}
/migrate`
,
{
method
:
'
POST
'
,
data
:
{
filePath
,
projectId
,
conflictMode
,
collectionName
,
},
})
}
This diff is collapsed.
Click to expand it.
packages/admin/src/utils/cloudbase.ts
+
5
-
5
View file @
195829ff
import
moment
from
'
moment
'
import
{
request
,
history
}
from
'
umi
'
import
{
message
,
notification
}
from
'
antd
'
import
{
RequestOptionsInit
}
from
'
umi-request
'
import
{
codeMessage
}
from
'
@/constants
'
import
{
isDevEnv
,
random
}
from
'
./tool
'
import
defaultSettings
from
'
../../config/defaultSettings
'
import
{
getFullDate
}
from
'
./date
'
let
app
:
any
let
auth
:
any
...
...
@@ -105,17 +105,17 @@ export async function tcbRequest<T = any>(
export
async
function
uploadFile
(
file
:
File
,
onProgress
:
(
v
:
number
)
=>
void
,
file
Name
?:
string
file
Path
?:
string
):
Promise
<
string
>
{
const
app
=
await
getCloudBaseApp
()
const
day
=
moment
().
format
(
'
YYYY-MM-DD
'
)
const
day
=
getFullDate
(
)
// 文件名
const
uploadFile
Name
=
file
Name
||
`
${
random
(
32
)}
-
${
file
.
name
}
`
const
uploadFile
Path
=
file
Path
||
`
upload/
${
day
}
/
${
random
(
32
)}
-
${
file
.
name
}
`
const
result
=
await
app
.
uploadFile
({
filePath
:
file
,
cloudPath
:
`cloudbase-cms/
upload/
${
day
}
/
${
uploadFile
Name
}
`
,
cloudPath
:
`cloudbase-cms/
${
uploadFile
Path
}
`
,
onUploadProgress
:
(
progressEvent
:
ProgressEvent
)
=>
{
const
percentCompleted
=
Math
.
round
((
progressEvent
.
loaded
*
100
)
/
progressEvent
.
total
)
onProgress
(
percentCompleted
)
...
...
This diff is collapsed.
Click to expand it.
packages/admin/src/utils/date.ts
0 → 100644
+
6
-
0
View file @
195829ff
import
moment
from
'
moment
'
import
'
moment/locale/zh-cn
'
export
const
getFullDate
=
(
v
=
Date
.
now
())
=>
moment
(
v
).
format
(
'
YYYY-MM-DD
'
)
export
const
getDateValue
=
(
v
?:
string
|
number
)
=>
moment
(
v
).
valueOf
()
This diff is collapsed.
Click to expand it.
packages/admin/src/utils/index.ts
+
1
-
18
View file @
195829ff
export
*
from
'
./tool
'
export
*
from
'
./copy
'
export
*
from
'
./cloudbase
'
// 驼峰转换下划线
function
humpToLine
(
name
:
string
)
{
return
name
.
replace
(
/
([
A-Z
])
/g
,
'
_$1
'
).
toLowerCase
()
}
export
function
humpDataToLineData
(
rawData
:
Record
<
string
,
any
>
)
{
const
newData
=
{}
Object
.
keys
(
rawData
).
forEach
((
key
:
string
)
=>
{
if
(
typeof
rawData
[
key
]
===
'
object
'
)
{
newData
[
humpToLine
(
key
)]
=
humpDataToLineData
(
rawData
[
key
])
}
else
{
newData
[
humpToLine
(
key
)]
=
rawData
[
key
]
}
})
return
newData
}
export
*
from
'
./date
'
This diff is collapsed.
Click to expand it.
packages/service/src/modules/projects/migrate/migrate.controller.ts
0 → 100644
+
141
-
0
View file @
195829ff
import
_
from
'
lodash
'
import
{
Get
,
Post
,
Body
,
Param
,
Query
,
Request
,
UseGuards
,
Controller
,
UseInterceptors
,
ClassSerializerInterceptor
,
}
from
'
@nestjs/common
'
import
{
PermissionGuard
}
from
'
@/guards
'
import
{
CloudBaseService
}
from
'
@/services
'
import
{
IsNotEmpty
}
from
'
class-validator
'
import
{
getCloudBaseManager
}
from
'
@/utils
'
import
{
CollectionV2
}
from
'
@/constants
'
class
MigrateBody
{
@
IsNotEmpty
()
filePath
:
string
@
IsNotEmpty
()
collectionName
:
string
@
IsNotEmpty
()
conflictMode
:
'
insert
'
|
'
upsert
'
}
interface
MigrateJobDto
{
// 项目 Id
projectId
:
string
// 任务 Id
jobId
:
number
// 导入文件路径
filePath
:
string
// 导入冲突处理模式
conflictMode
:
'
upsert
'
|
'
insert
'
createTime
:
number
collectionName
:
string
// 任务状态
// waiting:等待中,reading:读,writing:写,migrating:转移中,success:成功,fail:失败
status
?:
string
}
@
UseGuards
(
PermissionGuard
(
'
content
'
,
[
'
administrator
'
]))
@
UseInterceptors
(
ClassSerializerInterceptor
)
@
Controller
(
'
projects/:projectId/migrate
'
)
export
class
MigrateController
{
constructor
(
public
cloudbaseService
:
CloudBaseService
)
{}
@
Get
()
async
listMigrateJobs
(
@
Param
(
'
projectId
'
)
projectId
,
@
Query
()
query
:
{
page
:
number
;
pageSize
:
number
}
)
{
const
{
page
=
1
,
pageSize
=
10
}
=
query
const
dbQuery
=
this
.
collection
()
.
where
({
projectId
,
})
.
limit
(
Number
(
pageSize
))
.
skip
(
Number
(
page
-
1
)
*
pageSize
)
const
{
total
}
=
await
dbQuery
.
count
()
const
{
data
}
=
await
dbQuery
.
get
()
const
manager
=
getCloudBaseManager
()
if
(
!
data
?.
length
)
{
return
{
total
:
0
,
data
:
[],
}
}
const
requests
=
data
.
map
(
async
(
job
:
MigrateJobDto
)
=>
{
const
{
Status
}
=
await
manager
.
database
.
migrateStatus
(
job
.
jobId
)
return
{
...
job
,
status
:
Status
,
}
})
const
jobs
=
await
Promise
.
all
(
requests
)
return
{
data
:
jobs
,
total
,
}
}
/**
* 创建迁移任务
*/
@
Post
()
async
createMigrateJob
(
@
Param
(
'
projectId
'
)
projectId
,
@
Body
()
body
:
MigrateBody
,
@
Request
()
req
:
AuthRequest
)
{
const
{
filePath
,
collectionName
,
conflictMode
}
=
body
const
manager
=
getCloudBaseManager
()
const
fileKey
=
`cloudbase-cms/
${
filePath
}
`
// 导入数据
const
{
JobId
}
=
await
manager
.
database
.
import
(
collectionName
,
{
ObjectKey
:
fileKey
,
},
{
StopOnError
:
true
,
ConflictMode
:
conflictMode
,
}
)
const
jobRecord
:
MigrateJobDto
=
{
projectId
,
conflictMode
,
collectionName
,
jobId
:
JobId
,
filePath
:
fileKey
,
createTime
:
Date
.
now
(),
}
// 添加记录
return
this
.
collection
().
add
(
jobRecord
)
}
collection
(
collection
=
CollectionV2
.
DataMigrateTasks
)
{
return
this
.
cloudbaseService
.
collection
(
collection
)
}
}
This diff is collapsed.
Click to expand it.
packages/service/src/modules/projects/projects.controller.ts
+
2
-
2
View file @
195829ff
...
...
@@ -64,7 +64,7 @@ export class ProjectsController {
}
const
dbQuery
=
this
.
collection
().
where
(
filter
)
const
countRes
=
await
dbQuery
.
count
()
const
{
total
}
=
await
dbQuery
.
count
()
const
{
data
}
=
await
dbQuery
.
skip
(
Number
(
page
-
1
)
*
Number
(
pageSize
))
...
...
@@ -73,7 +73,7 @@ export class ProjectsController {
return
{
data
,
total
:
countRes
.
total
,
total
,
}
}
...
...
This diff is collapsed.
Click to expand it.
packages/service/src/modules/projects/projects.module.ts
+
8
-
1
View file @
195829ff
...
...
@@ -6,9 +6,16 @@ import { SchemasController } from './schemas/schema.controller'
import
{
WebhooksController
}
from
'
./webhooks/webhooks.controller
'
import
{
ContentsService
}
from
'
./contents/contents.service
'
import
{
ContentsController
}
from
'
./contents/contents.controller
'
import
{
MigrateController
}
from
'
./migrate/migrate.controller
'
@
Module
({
controllers
:
[
SchemasController
,
WebhooksController
,
ContentsController
,
ProjectsController
],
controllers
:
[
SchemasController
,
WebhooksController
,
ContentsController
,
ProjectsController
,
MigrateController
,
],
providers
:
[
SchemasService
,
ContentsService
,
WebhooksService
],
})
export
class
ProjectsModule
{}
This diff is collapsed.
Click to expand it.
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment
Menu
Projects
Groups
Snippets
Help