Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in / Register
Toggle navigation
H
huamu
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
郭铭瑶
huamu
Commits
1b0fe358
Commit
1b0fe358
authored
Sep 14, 2021
by
郭铭瑶
🤘
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
导出活动详情及修改活动
parent
65527759
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
306 additions
and
97 deletions
+306
-97
useExportFile.ts
src/hooks/useExportFile.ts
+51
-16
useFetch.ts
src/hooks/useFetch.ts
+7
-0
activity-list-modal.vue
src/view/components/activity-list-modal.vue
+15
-2
new-activity-drawer.vue
src/view/components/new-activity-drawer.vue
+232
-78
main.vue
src/view/main.vue
+1
-1
No files found.
src/hooks/useExportFile.ts
View file @
1b0fe358
import
{
saveAs
}
from
'file-saver'
import
{
Packer
,
Document
,
Paragraph
,
HeadingLevel
,
TextRun
}
from
'docx'
import
{
Packer
,
Document
,
Paragraph
,
HeadingLevel
,
TextRun
,
ImageRun
,
}
from
'docx'
function
createHeading
(
text
:
string
):
Paragraph
{
return
new
Paragraph
({
...
...
@@ -19,24 +26,52 @@ function createText(text: string): Paragraph {
})
}
function
createDocument
(
data
:
any
[],
labelKey
:
string
)
{
async
function
createImage
(
key
:
string
,
urls
:
string
[])
{
const
blobs
=
await
Promise
.
all
(
urls
.
map
(
async
(
url
)
=>
await
fetch
(
url
).
then
((
r
:
any
)
=>
r
.
blob
())),
)
return
new
Paragraph
({
children
:
[
new
TextRun
({
text
:
`
${
key
}
:`
}),
...
blobs
.
map
(
(
blob
)
=>
new
ImageRun
({
data
:
blob
,
transformation
:
{
width
:
100
,
height
:
100
,
},
}),
),
],
})
}
async
function
createDocument
(
data
:
any
[],
labelKey
:
string
)
{
function
arrayToString
(
e
:
unknown
)
{
if
(
Array
.
isArray
(
e
))
return
e
.
join
(
','
)
return
e
}
return
new
Document
({
sections
:
data
.
map
((
item
)
=>
{
const
keys
=
Object
.
keys
(
item
)
return
{
children
:
[
createHeading
(
item
[
labelKey
]),
...
keys
.
map
((
key
)
=>
{
if
(
key
===
'居住地址'
)
return
createText
(
''
)
return
createText
(
`
${
key
}
:
${
arrayToString
(
item
[
key
])}
`
)
}),
],
}
}),
sections
:
await
Promise
.
all
(
data
.
map
(
async
(
item
)
=>
{
const
keys
=
Object
.
keys
(
item
)
return
{
children
:
[
createHeading
(
item
[
labelKey
]),
...(
await
Promise
.
all
(
keys
.
map
(
async
(
key
)
=>
{
if
(
key
===
'居住地址'
)
return
createText
(
''
)
if
(
key
.
startsWith
(
'_'
))
return
createText
(
''
)
if
(
key
.
includes
(
'照片'
))
return
await
createImage
(
key
,
item
[
key
])
return
createText
(
`
${
key
}
:
${
arrayToString
(
item
[
key
])}
`
)
}),
)),
],
}
}),
),
})
}
...
...
@@ -46,12 +81,12 @@ function createDocument(data: any[], labelKey: string) {
* @param labelKey 每个section的title取值
* @param defaultFileName 默认导出名字(可选)
*/
export
default
function
useExportFile
(
export
default
async
function
useExportFile
(
data
:
any
[],
labelKey
:
string
,
defaultFileName
:
string
=
'导出'
,
)
{
const
doc
=
createDocument
(
data
,
labelKey
)
const
doc
=
await
createDocument
(
data
,
labelKey
)
Packer
.
toBlob
(
doc
).
then
((
blob
)
=>
{
saveAs
(
blob
,
`
${
defaultFileName
}
.docx`
)
})
...
...
src/hooks/useFetch.ts
View file @
1b0fe358
...
...
@@ -35,6 +35,13 @@ export async function usePostActivity(params: QueryProps) {
})
return
res
&&
res
.
data
&&
res
.
data
.
result
}
export
async
function
usePutActivity
(
params
:
QueryProps
)
{
const
res
=
await
ajax
.
put
({
url
:
api
.
ACTIVITY
,
params
,
})
return
res
&&
res
.
data
&&
res
.
data
.
result
}
export
async
function
useFetchArea
(
params
:
QueryProps
)
{
const
res
=
await
ajax
.
get
({
url
:
api
.
AREA
,
...
...
src/view/components/activity-list-modal.vue
View file @
1b0fe358
...
...
@@ -124,7 +124,10 @@ const activityList = ref([])
const
fetchList
=
useDebounce
(
async
(
query
?:
string
)
=>
{
activityList
.
value
=
(
await
useFetchActivity
({
q
:
query
}))?.
map
((
item
:
any
)
=>
item
.
extra
)
||
[]
(
await
useFetchActivity
({
q
:
query
}))?.
map
((
item
:
any
)
=>
({
id
:
item
.
id
,
...
item
.
extra
,
}))
||
[]
})
onMounted
(()
=>
{
fetchList
()
...
...
@@ -224,7 +227,17 @@ const columns = [
},
},
{
title
:
'活动地址'
,
key
:
'活动地址'
},
{
title
:
'出席率'
,
key
:
'出席率'
},
{
title
:
'出席率'
,
key
:
'出席率'
,
render
(
row
:
any
)
{
return
h
(
'span'
,
{},
{
default
:
()
=>
(
row
[
'出席率'
]
?
`
${
row
[
'出席率'
]}
%`
:
''
)
},
)
},
},
{
title
:
'参与人数'
,
key
:
'实际参与人数'
},
{
title
:
' '
,
...
...
src/view/components/new-activity-drawer.vue
View file @
1b0fe358
...
...
@@ -16,7 +16,7 @@
<n-button
type=
"error"
size=
"small"
@
click=
"mode = 'modify'"
>
编辑
</n-button>
<n-button
type=
"primary"
size=
"small"
>
<n-button
type=
"primary"
size=
"small"
@
click=
"handleExport"
>
<template
#
icon
>
<n-icon
size=
".12rem"
>
<svg-icon
:data=
"exportIcon"
original
/>
...
...
@@ -31,7 +31,7 @@
</n-space>
</div>
</template>
<div
class=
"content"
>
<div
class=
"content"
:class=
"{ 'view-mode': mode === 'view' }"
>
<div>
<p
class=
"title"
>
基本信息
</p>
<n-form
...
...
@@ -60,7 +60,9 @@
</n-form-item-gi>
<n-form-item-gi
:span=
"24"
label=
"党组织名称(下拉或输入关键字选择)"
:label=
"`党组织名称${
mode === 'view' ? '' : '(下拉或输入关键字选择)'
}`"
path=
"orgName"
>
<p
v-if=
"mode === 'view'"
>
...
...
@@ -82,13 +84,16 @@
<n-date-picker
v-else
v-model:value=
"basicData.date"
:disabled=
"mode === 'view'"
style=
"width: 100%"
/>
</n-form-item-gi>
<n-form-item-gi
:span=
"24"
label=
"活动地址(请具体填写,如“XX区XX路XX号XX中心”)"
:label=
"`活动地址${
mode === 'view'
? ''
: '(请具体填写,如“XX区XX路XX号XX中心”)'
}`"
path=
"address"
>
<p
v-if=
"mode === 'view'"
>
{{ basicData.address }}
</p>
...
...
@@ -98,7 +103,13 @@
</n-form>
</div>
<div>
<p
class=
"title"
>
人员参与情况
</p>
<p
class=
"title"
>
人员参与情况
<span>
党员总数:
<span>
{{ totalPerson }}人
</span>
</span>
</p>
<n-form
ref=
"memberRef"
:model=
"memberData"
...
...
@@ -114,7 +125,7 @@
v-model:value=
"memberData.count"
size=
"small"
:show-button=
"false"
:
min=
"0
"
:
validator=
"checkMemberCount
"
>
<
template
#
suffix
>
人
</
template
>
</n-input-number>
...
...
@@ -123,8 +134,6 @@
:span=
"12"
label=
"不计入参与活动党员人数"
path=
"excludeCount"
:min=
"0"
:max=
"memberData.count"
>
<p
v-if=
"mode === 'view'"
>
{{ memberData.excludeCount }}
</p>
<n-input-number
...
...
@@ -132,14 +141,16 @@
v-model:value=
"memberData.excludeCount"
size=
"small"
:show-button=
"false"
:
min=
"0
"
:
validator=
"checkMemberExcludeCount
"
>
<
template
#
suffix
>
人
</
template
>
</n-input-number>
</n-form-item-gi>
<n-form-item-gi
:span=
"24"
label=
"电子表格或签到照片(大小20M以内)"
:label=
"`电子表格或签到照片${
mode === 'view' ? '' : '(大小20M以内)'
}`"
path=
"fileType"
>
<p
v-if=
"mode === 'view'"
>
...
...
@@ -155,22 +166,33 @@
placeholder=
"请选择上传类型"
/>
</n-form-item-gi>
<n-form-item-gi
v-if=
"memberData.fileType === 'file'"
:span=
"24"
>
<n-form-item-gi
v-if=
"memberData.fileType === 'file'"
:span=
"24"
path=
"attachment"
>
<n-upload
v-model:file-list=
"memberData.attachment"
style=
"width: 100%"
accept=
".docx,.doc,.xlsx,.xls,.csv,.txt"
:on-change=
"onChange"
:on-change=
"(e) => onChange(e, 'memberData')"
:show-remove-button=
"mode !== 'view'"
>
<n-button>
上传附件
</n-button>
</n-upload>
</n-form-item-gi>
<n-form-item-gi
v-if=
"memberData.fileType === 'photo'"
:span=
"24"
>
<n-form-item-gi
v-if=
"memberData.fileType === 'photo'"
:span=
"24"
path=
"attachment"
>
<n-upload
style=
"width: 100%"
action=
"http://www.mocky.io/v2/5e4bafc63100007100d8b70f"
:default-file-list=
"previewFileList"
:default-file-list=
"memberData.attachment"
accept=
"image/jpg,image/jpeg,image/png,image/img"
list-type=
"image-card"
:show-remove-button=
"mode !== 'view'"
:on-change=
"(e) => onChange(e, 'memberData')"
@
preview=
"handlePreview"
>
点击上传
...
...
@@ -191,16 +213,16 @@
<n-grid
:cols=
"24"
:x-gap=
"12"
>
<n-form-item-gi
:span=
"24"
label=
"活动内容描述或描述照片(大小20M以内)"
:label=
"`活动内容描述或描述照片${
mode === 'view' ? '' : '(大小20M以内)'
}`"
path=
"describeType"
>
<p
v-if=
"mode === 'view'"
>
{{
describeOptions.find(
(e) => e.value === detailData.describeType,
)?.label
}}
</p>
<
template
v-if=
"mode === 'view'"
>
<p
v-if=
"detailData.describeType === 'text'"
>
{{
detailData
.
attachment
}}
</p>
</
template
>
<n-select
v-else
v-model:value=
"detailData.describeType"
...
...
@@ -209,7 +231,7 @@
/>
</n-form-item-gi>
<n-form-item-gi
v-if=
"detailData.describeType === 'text'"
v-if=
"detailData.describeType === 'text'
&& mode !== 'view'
"
:span=
"24"
>
<n-input
...
...
@@ -220,13 +242,15 @@
<n-form-item-gi
v-if=
"detailData.describeType === 'photo'"
:span=
"24"
path=
"attachment"
>
<n-upload
style=
"width: 100%"
action=
"http://www.mocky.io/v2/5e4bafc63100007100d8b70f"
:default-file-list=
"previewFileList"
:default-file-list=
"detailData.attachment"
accept=
"image/jpg,image/jpeg,image/png,image/img"
list-type=
"image-card"
:show-remove-button=
"mode !== 'view'"
:on-change=
"(e) => onChange(e, 'detailData')"
@
preview=
"handlePreview"
>
点击上传
...
...
@@ -234,15 +258,20 @@
</n-form-item-gi>
<n-form-item-gi
:span=
"24"
label=
"活动照片(格式为jpg、jpeg、png、img, 大小20M以内)"
path=
"photo"
:label=
"`活动照片${
mode === 'view'
? ''
: '(格式为jpg、jpeg、png、img, 大小20M以内)'
}`"
path=
"photoList"
>
<n-upload
style=
"width: 100%"
action=
"http://www.mocky.io/v2/5e4bafc63100007100d8b70f"
:default-file-list=
"previewFileList"
:default-file-list=
"detailData.photoList"
accept=
"image/jpg,image/jpeg,image/png,image/img"
list-type=
"image-card"
:show-remove-button=
"mode !== 'view'"
:on-change=
"(e) => onChange(e, 'photoList')"
@
preview=
"handlePreview"
>
点击上传
...
...
@@ -270,9 +299,10 @@ import { computed, onMounted, PropType, ref, watch } from 'vue'
import
{
FormRules
,
NForm
,
useMessage
}
from
'naive-ui'
import
exportIcon
from
'@images/export.svg'
import
{
activity
}
from
'@/util/tags'
import
{
useFetchOrg
,
usePostActivity
}
from
'@/hooks/useFetch'
import
{
useFetchOrg
,
usePostActivity
,
usePutActivity
}
from
'@/hooks/useFetch'
import
dayjs
from
'@/util/dayjs'
import
useAliOss
from
'@/hooks/useAliOss'
import
useExportFile
from
'@/hooks/useExportFile'
const
message
=
useMessage
()
const
props
=
defineProps
({
...
...
@@ -340,6 +370,7 @@ watch(
detailData
.
value
=
{}
return
}
getPersonNum
(
data
[
'党组织名称'
])
basicData
.
value
=
{
name
:
data
[
'活动名称'
],
type
:
data
[
'标签类别'
],
...
...
@@ -358,11 +389,11 @@ watch(
fileType
===
'file'
?
data
[
'签到表文件'
]
:
fileType
===
'photo'
?
data
[
'签到表照片'
]
?
(
data
[
'签到表照片'
]
||
[]).
map
((
url
:
string
)
=>
({
url
}))
:
[],
}
const
isText
=
data
[
'活动内容'
]
const
isText
=
data
[
'活动内容
描述
'
]
const
isImage
=
data
[
'台账记录照片'
]
&&
data
[
'台账记录照片'
].
length
>
0
const
describeType
=
isText
?
'text'
:
isImage
?
'photo'
:
null
detailData
.
value
=
{
...
...
@@ -371,9 +402,9 @@ watch(
describeType
===
'text'
?
data
[
'活动内容描述'
]
:
describeType
===
'photo'
?
data
[
'台账记录照片'
]
?
(
data
[
'台账记录照片'
]
||
[]).
map
((
url
:
string
)
=>
({
url
}))
:
[],
photoList
:
data
[
'活动照片'
]
||
[],
photoList
:
(
data
[
'活动照片'
]
||
[]).
map
((
url
:
string
)
=>
({
url
}))
||
[],
}
mode
.
value
=
data
.
_mode
||
null
},
...
...
@@ -407,18 +438,34 @@ const rules: FormRules = {
trigger
:
[
'blur'
,
'input'
],
message
:
'请输入活动地址'
,
},
count
:
{
type
:
'number'
,
required
:
true
,
trigger
:
[
'blur'
,
'input'
],
message
:
'请输入实际参与人数'
,
},
excludeCount
:
{
type
:
'number'
,
required
:
true
,
trigger
:
[
'blur'
,
'input'
],
message
:
'请输入不计入参与活动党员人数'
,
},
count
:
[
{
type
:
'number'
,
required
:
true
,
trigger
:
[
'blur'
,
'input'
],
message
:
'请输入实际参与人数'
,
},
{
type
:
'number'
,
validator
:
(
_
,
val
)
=>
checkMemberCount
(
val
),
message
:
'不能大于党员总数'
,
trigger
:
[
'blur'
,
'input'
],
},
],
excludeCount
:
[
{
type
:
'number'
,
required
:
true
,
trigger
:
[
'blur'
,
'input'
],
message
:
'请输入不计入参与活动党员人数'
,
},
{
type
:
'number'
,
validator
:
(
_
,
val
)
=>
checkMemberExcludeCount
(
val
),
message
:
'不能大于实际参与人数'
,
trigger
:
[
'blur'
,
'input'
],
},
],
fileType
:
{
required
:
true
,
trigger
:
[
'blur'
,
'change'
],
...
...
@@ -438,21 +485,34 @@ const submit = async () => {
detailRef
.
value
?.
validate
((
errors
:
any
)
=>
errors
&&
(
noMistake
=
false
))
if
(
!
noMistake
)
return
console
.
log
(
basicData
.
value
,
memberData
.
value
,
detailData
.
value
)
const
{
name
,
type
,
orgName
,
date
,
address
}
=
basicData
.
value
const
{
count
,
excludeCount
}
=
memberData
.
value
const
{
attachment
}
=
detailData
.
value
const
data
=
{
活动名称
:
name
,
标签类别
:
type
,
党组织名称
:
orgName
,
活动日期
:
+
(
date
+
''
).
slice
(
0
,
10
),
// 截掉10位后的10,不然这接口会认为不是时间戳
活动地址
:
address
,
实际参与人数
:
count
,
不计入参与活动党员人数
:
excludeCount
,
活动内容描述
:
attachment
,
出席率
:
Math
.
round
(((
count
-
excludeCount
)
/
totalPerson
.
value
)
*
100
),
}
if
(
mode
.
value
===
'modify'
)
{
await
usePutActivity
({
separate
:
[
{
id
:
props
.
data
.
id
,
extra
:
data
,
},
],
})
closeDrawer
()
props
.
cb
&&
props
.
cb
()
message
.
success
(
'活动修改成功'
)
}
else
{
const
{
name
,
type
,
orgName
,
date
,
address
}
=
basicData
.
value
const
{
count
,
excludeCount
}
=
memberData
.
value
const
{
attachment
}
=
detailData
.
value
const
data
=
{
活动名称
:
name
,
标签类别
:
type
,
党组织名称
:
orgName
,
活动日期
:
+
(
date
+
''
).
slice
(
0
,
10
),
// 截掉10位后的10,不然这接口会认为不是时间戳
活动地址
:
address
,
实际参与人数
:
count
,
不计入参与活动党员人数
:
excludeCount
,
活动内容描述
:
attachment
,
}
await
usePostActivity
({
extra
:
data
,
})
...
...
@@ -462,20 +522,11 @@ const submit = async () => {
}
}
const
previewFileList
=
ref
([
{
id
:
'react'
,
name
:
'我是react.png'
,
status
:
'finished'
,
url
:
'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg'
,
},
{
id
:
'vue'
,
name
:
'我是vue.png'
,
status
:
'finished'
,
url
:
'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg'
,
},
])
// const previewFileList = ref([
// {
// url: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg',
// },
// ])
const
previewImageUrl
=
ref
(
''
)
const
showPhoto
=
ref
(
false
)
function
handlePreview
(
file
:
any
)
{
...
...
@@ -484,9 +535,97 @@ function handlePreview(file: any) {
showPhoto
.
value
=
true
}
async
function
onChange
(
options
:
any
)
{
console
.
log
(
'change'
,
options
)
await
useAliOss
(
options
.
file
.
file
)
async
function
onChange
(
options
:
any
,
type
:
string
)
{
console
.
log
(
'change'
,
options
,
type
)
if
(
!
options
||
!
options
.
fileList
||
options
.
fileList
.
length
===
0
)
return
const
urls
=
await
Promise
.
all
(
options
.
fileList
.
map
(
async
(
item
:
any
)
=>
await
useAliOss
(
item
.
file
)),
)
switch
(
type
)
{
case
'memberData'
:
memberData
.
value
.
attachment
=
urls
break
case
'detailData'
:
detailData
.
value
.
attachment
=
urls
break
case
'photoList'
:
detailData
.
value
.
photoList
=
urls
break
default
:
break
}
}
const
totalPerson
=
ref
(
0
)
async
function
getPersonNum
(
name
:
string
)
{
if
(
!
name
)
return
0
return
(
(
await
useFetchOrg
({
keys
:
'党员数量'
,
q
:
`paths @ "党组织名称" && string == "
${
name
}
"`
,
})
)?.[
0
]?.[
'党员数量'
]
||
0
)
}
watch
(
()
=>
basicData
.
value
.
orgName
,
async
(
name
)
=>
{
totalPerson
.
value
=
await
getPersonNum
(
name
)
},
)
function
checkMemberCount
(
val
:
number
)
{
return
val
>=
0
&&
val
<=
totalPerson
.
value
}
function
checkMemberExcludeCount
(
val
:
number
)
{
return
val
>=
0
&&
val
<=
memberData
.
value
.
count
}
const
handleExport
=
async
()
=>
{
const
{
name
,
type
,
orgName
,
date
,
address
}
=
basicData
.
value
const
{
count
,
excludeCount
}
=
memberData
.
value
const
{
attachment
}
=
detailData
.
value
const
data
=
[
{
活动名称
:
name
,
标签类别
:
type
,
党组织名称
:
orgName
,
活动日期
:
dayjs
(
date
).
format
(
'll'
),
// 截掉10位后的10,不然这接口会认为不是时间戳
活动地址
:
address
,
},
{
实际参与人数
:
count
,
不计入参与活动党员人数
:
excludeCount
,
出席率
:
Math
.
round
(((
count
-
excludeCount
)
/
totalPerson
.
value
)
*
100
),
},
{
活动内容描述
:
attachment
,
// TODO 测试图片
活动照片
:
[
'https://raw.githubusercontent.com/dolanmiu/docx/master/demo/images/cat.jpg'
,
'https://raw.githubusercontent.com/dolanmiu/docx/master/demo/images/cat.jpg'
,
],
},
]
await
useExportFile
(
[
{
_title
:
'基本信息'
,
...
data
[
0
],
},
{
_title
:
'人员参与情况'
,
...
data
[
1
],
},
{
_title
:
'活动详情'
,
...
data
[
2
],
},
],
'_title'
,
basicData
.
value
.
name
,
)
}
</
script
>
...
...
@@ -524,6 +663,13 @@ async function onChange(options: any) {
height .14rem
background $red
margin-right .06rem
>span
flex 1
text-align right
font-family $font-ping
font-size .1rem
>span
color $blue
</
style
>
<
style
lang=
"stylus"
>
@import '../../components/MyComponent/main.styl'
...
...
@@ -539,4 +685,12 @@ async function onChange(options: any) {
background #f3f4f7
border-left .01rem solid $light-gray
padding 0 .1rem
.content
.n-upload
.n-upload-file-list.n-upload-file-list--grid
min-height .9rem
&.view-mode
.n-upload__trigger.n-upload__trigger--image-card
display none !important
</
style
>
src/view/main.vue
View file @
1b0fe358
<
template
>
<
Map
ref=
"map"
/
>
<
!--
<Map
ref=
"map"
/>
--
>
<NavBar
@
focus=
"showTag = false"
@
blur=
"showTag = true"
/>
<BasicInfo
:visible=
"showTag"
/>
<div
v-if=
"showReset"
class=
"reset"
@
click=
"resetMap"
>
...
...
Write
Preview
Markdown
is supported
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