Commit 1b0fe358 authored by 郭铭瑶's avatar 郭铭瑶 🤘

导出活动详情及修改活动

parent 65527759
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`)
})
......
......@@ -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,
......
......@@ -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: ' ',
......
......@@ -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>
<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">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment