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

导出活动详情及修改活动

parent 65527759
import { saveAs } from 'file-saver' 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 { function createHeading(text: string): Paragraph {
return new Paragraph({ return new Paragraph({
...@@ -19,24 +26,52 @@ function createText(text: string): 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) { function arrayToString(e: unknown) {
if (Array.isArray(e)) return e.join(',') if (Array.isArray(e)) return e.join(',')
return e return e
} }
return new Document({ return new Document({
sections: data.map((item) => { sections: await Promise.all(
const keys = Object.keys(item) data.map(async (item) => {
return { const keys = Object.keys(item)
children: [ return {
createHeading(item[labelKey]), children: [
...keys.map((key) => { createHeading(item[labelKey]),
if (key === '居住地址') return createText('') ...(await Promise.all(
return createText(`${key}${arrayToString(item[key])}`) 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) { ...@@ -46,12 +81,12 @@ function createDocument(data: any[], labelKey: string) {
* @param labelKey 每个section的title取值 * @param labelKey 每个section的title取值
* @param defaultFileName 默认导出名字(可选) * @param defaultFileName 默认导出名字(可选)
*/ */
export default function useExportFile( export default async function useExportFile(
data: any[], data: any[],
labelKey: string, labelKey: string,
defaultFileName: string = '导出', defaultFileName: string = '导出',
) { ) {
const doc = createDocument(data, labelKey) const doc = await createDocument(data, labelKey)
Packer.toBlob(doc).then((blob) => { Packer.toBlob(doc).then((blob) => {
saveAs(blob, `${defaultFileName}.docx`) saveAs(blob, `${defaultFileName}.docx`)
}) })
......
...@@ -35,6 +35,13 @@ export async function usePostActivity(params: QueryProps) { ...@@ -35,6 +35,13 @@ export async function usePostActivity(params: QueryProps) {
}) })
return res && res.data && res.data.result 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) { export async function useFetchArea(params: QueryProps) {
const res = await ajax.get({ const res = await ajax.get({
url: api.AREA, url: api.AREA,
......
...@@ -124,7 +124,10 @@ const activityList = ref([]) ...@@ -124,7 +124,10 @@ const activityList = ref([])
const fetchList = useDebounce(async (query?: string) => { const fetchList = useDebounce(async (query?: string) => {
activityList.value = activityList.value =
(await useFetchActivity({ q: query }))?.map((item: any) => item.extra) || [] (await useFetchActivity({ q: query }))?.map((item: any) => ({
id: item.id,
...item.extra,
})) || []
}) })
onMounted(() => { onMounted(() => {
fetchList() fetchList()
...@@ -224,7 +227,17 @@ const columns = [ ...@@ -224,7 +227,17 @@ const columns = [
}, },
}, },
{ title: '活动地址', key: '活动地址' }, { title: '活动地址', key: '活动地址' },
{ title: '出席率', key: '出席率' }, {
title: '出席率',
key: '出席率',
render(row: any) {
return h(
'span',
{},
{ default: () => (row['出席率'] ? `${row['出席率']}%` : '') },
)
},
},
{ title: '参与人数', key: '实际参与人数' }, { title: '参与人数', key: '实际参与人数' },
{ {
title: ' ', title: ' ',
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<n-button type="error" size="small" @click="mode = 'modify'"> <n-button type="error" size="small" @click="mode = 'modify'">
编辑 编辑
</n-button> </n-button>
<n-button type="primary" size="small"> <n-button type="primary" size="small" @click="handleExport">
<template #icon> <template #icon>
<n-icon size=".12rem"> <n-icon size=".12rem">
<svg-icon :data="exportIcon" original /> <svg-icon :data="exportIcon" original />
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
</n-space> </n-space>
</div> </div>
</template> </template>
<div class="content"> <div class="content" :class="{ 'view-mode': mode === 'view' }">
<div> <div>
<p class="title">基本信息</p> <p class="title">基本信息</p>
<n-form <n-form
...@@ -60,7 +60,9 @@ ...@@ -60,7 +60,9 @@
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi <n-form-item-gi
:span="24" :span="24"
label="党组织名称(下拉或输入关键字选择)" :label="`党组织名称${
mode === 'view' ? '' : '(下拉或输入关键字选择)'
}`"
path="orgName" path="orgName"
> >
<p v-if="mode === 'view'"> <p v-if="mode === 'view'">
...@@ -82,13 +84,16 @@ ...@@ -82,13 +84,16 @@
<n-date-picker <n-date-picker
v-else v-else
v-model:value="basicData.date" v-model:value="basicData.date"
:disabled="mode === 'view'"
style="width: 100%" style="width: 100%"
/> />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi <n-form-item-gi
:span="24" :span="24"
label="活动地址(请具体填写,如“XX区XX路XX号XX中心”)" :label="`活动地址${
mode === 'view'
? ''
: '(请具体填写,如“XX区XX路XX号XX中心”)'
}`"
path="address" path="address"
> >
<p v-if="mode === 'view'">{{ basicData.address }}</p> <p v-if="mode === 'view'">{{ basicData.address }}</p>
...@@ -98,7 +103,13 @@ ...@@ -98,7 +103,13 @@
</n-form> </n-form>
</div> </div>
<div> <div>
<p class="title">人员参与情况</p> <p class="title">
人员参与情况
<span>
党员总数:
<span>{{ totalPerson }}人</span>
</span>
</p>
<n-form <n-form
ref="memberRef" ref="memberRef"
:model="memberData" :model="memberData"
...@@ -114,7 +125,7 @@ ...@@ -114,7 +125,7 @@
v-model:value="memberData.count" v-model:value="memberData.count"
size="small" size="small"
:show-button="false" :show-button="false"
:min="0" :validator="checkMemberCount"
> >
<template #suffix></template> <template #suffix></template>
</n-input-number> </n-input-number>
...@@ -123,8 +134,6 @@ ...@@ -123,8 +134,6 @@
:span="12" :span="12"
label="不计入参与活动党员人数" label="不计入参与活动党员人数"
path="excludeCount" path="excludeCount"
:min="0"
:max="memberData.count"
> >
<p v-if="mode === 'view'">{{ memberData.excludeCount }}</p> <p v-if="mode === 'view'">{{ memberData.excludeCount }}</p>
<n-input-number <n-input-number
...@@ -132,14 +141,16 @@ ...@@ -132,14 +141,16 @@
v-model:value="memberData.excludeCount" v-model:value="memberData.excludeCount"
size="small" size="small"
:show-button="false" :show-button="false"
:min="0" :validator="checkMemberExcludeCount"
> >
<template #suffix></template> <template #suffix></template>
</n-input-number> </n-input-number>
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi <n-form-item-gi
:span="24" :span="24"
label="电子表格或签到照片(大小20M以内)" :label="`电子表格或签到照片${
mode === 'view' ? '' : '(大小20M以内)'
}`"
path="fileType" path="fileType"
> >
<p v-if="mode === 'view'"> <p v-if="mode === 'view'">
...@@ -155,22 +166,33 @@ ...@@ -155,22 +166,33 @@
placeholder="请选择上传类型" placeholder="请选择上传类型"
/> />
</n-form-item-gi> </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 <n-upload
v-model:file-list="memberData.attachment"
style="width: 100%" style="width: 100%"
accept=".docx,.doc,.xlsx,.xls,.csv,.txt" 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-button> 上传附件 </n-button>
</n-upload> </n-upload>
</n-form-item-gi> </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 <n-upload
style="width: 100%" style="width: 100%"
action="http://www.mocky.io/v2/5e4bafc63100007100d8b70f" :default-file-list="memberData.attachment"
:default-file-list="previewFileList"
accept="image/jpg,image/jpeg,image/png,image/img" accept="image/jpg,image/jpeg,image/png,image/img"
list-type="image-card" list-type="image-card"
:show-remove-button="mode !== 'view'"
:on-change="(e) => onChange(e, 'memberData')"
@preview="handlePreview" @preview="handlePreview"
> >
点击上传 点击上传
...@@ -191,16 +213,16 @@ ...@@ -191,16 +213,16 @@
<n-grid :cols="24" :x-gap="12"> <n-grid :cols="24" :x-gap="12">
<n-form-item-gi <n-form-item-gi
:span="24" :span="24"
label="活动内容描述或描述照片(大小20M以内)" :label="`活动内容描述或描述照片${
mode === 'view' ? '' : '(大小20M以内)'
}`"
path="describeType" path="describeType"
> >
<p v-if="mode === 'view'"> <template v-if="mode === 'view'">
{{ <p v-if="detailData.describeType === 'text'">
describeOptions.find( {{ detailData.attachment }}
(e) => e.value === detailData.describeType, </p>
)?.label </template>
}}
</p>
<n-select <n-select
v-else v-else
v-model:value="detailData.describeType" v-model:value="detailData.describeType"
...@@ -209,7 +231,7 @@ ...@@ -209,7 +231,7 @@
/> />
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi <n-form-item-gi
v-if="detailData.describeType === 'text'" v-if="detailData.describeType === 'text' && mode !== 'view'"
:span="24" :span="24"
> >
<n-input <n-input
...@@ -220,13 +242,15 @@ ...@@ -220,13 +242,15 @@
<n-form-item-gi <n-form-item-gi
v-if="detailData.describeType === 'photo'" v-if="detailData.describeType === 'photo'"
:span="24" :span="24"
path="attachment"
> >
<n-upload <n-upload
style="width: 100%" style="width: 100%"
action="http://www.mocky.io/v2/5e4bafc63100007100d8b70f" :default-file-list="detailData.attachment"
:default-file-list="previewFileList"
accept="image/jpg,image/jpeg,image/png,image/img" accept="image/jpg,image/jpeg,image/png,image/img"
list-type="image-card" list-type="image-card"
:show-remove-button="mode !== 'view'"
:on-change="(e) => onChange(e, 'detailData')"
@preview="handlePreview" @preview="handlePreview"
> >
点击上传 点击上传
...@@ -234,15 +258,20 @@ ...@@ -234,15 +258,20 @@
</n-form-item-gi> </n-form-item-gi>
<n-form-item-gi <n-form-item-gi
:span="24" :span="24"
label="活动照片(格式为jpg、jpeg、png、img, 大小20M以内)" :label="`活动照片${
path="photo" mode === 'view'
? ''
: '(格式为jpg、jpeg、png、img, 大小20M以内)'
}`"
path="photoList"
> >
<n-upload <n-upload
style="width: 100%" style="width: 100%"
action="http://www.mocky.io/v2/5e4bafc63100007100d8b70f" :default-file-list="detailData.photoList"
:default-file-list="previewFileList"
accept="image/jpg,image/jpeg,image/png,image/img" accept="image/jpg,image/jpeg,image/png,image/img"
list-type="image-card" list-type="image-card"
:show-remove-button="mode !== 'view'"
:on-change="(e) => onChange(e, 'photoList')"
@preview="handlePreview" @preview="handlePreview"
> >
点击上传 点击上传
...@@ -270,9 +299,10 @@ import { computed, onMounted, PropType, ref, watch } from 'vue' ...@@ -270,9 +299,10 @@ import { computed, onMounted, PropType, ref, watch } from 'vue'
import { FormRules, NForm, useMessage } from 'naive-ui' import { FormRules, NForm, useMessage } from 'naive-ui'
import exportIcon from '@images/export.svg' import exportIcon from '@images/export.svg'
import { activity } from '@/util/tags' import { activity } from '@/util/tags'
import { useFetchOrg, usePostActivity } from '@/hooks/useFetch' import { useFetchOrg, usePostActivity, usePutActivity } from '@/hooks/useFetch'
import dayjs from '@/util/dayjs' import dayjs from '@/util/dayjs'
import useAliOss from '@/hooks/useAliOss' import useAliOss from '@/hooks/useAliOss'
import useExportFile from '@/hooks/useExportFile'
const message = useMessage() const message = useMessage()
const props = defineProps({ const props = defineProps({
...@@ -340,6 +370,7 @@ watch( ...@@ -340,6 +370,7 @@ watch(
detailData.value = {} detailData.value = {}
return return
} }
getPersonNum(data['党组织名称'])
basicData.value = { basicData.value = {
name: data['活动名称'], name: data['活动名称'],
type: data['标签类别'], type: data['标签类别'],
...@@ -358,11 +389,11 @@ watch( ...@@ -358,11 +389,11 @@ watch(
fileType === 'file' fileType === 'file'
? data['签到表文件'] ? data['签到表文件']
: fileType === 'photo' : fileType === 'photo'
? data['签到表照片'] ? (data['签到表照片'] || []).map((url: string) => ({ url }))
: [], : [],
} }
const isText = data['活动内容'] const isText = data['活动内容描述']
const isImage = data['台账记录照片'] && data['台账记录照片'].length > 0 const isImage = data['台账记录照片'] && data['台账记录照片'].length > 0
const describeType = isText ? 'text' : isImage ? 'photo' : null const describeType = isText ? 'text' : isImage ? 'photo' : null
detailData.value = { detailData.value = {
...@@ -371,9 +402,9 @@ watch( ...@@ -371,9 +402,9 @@ watch(
describeType === 'text' describeType === 'text'
? data['活动内容描述'] ? data['活动内容描述']
: describeType === 'photo' : describeType === 'photo'
? data['台账记录照片'] ? (data['台账记录照片'] || []).map((url: string) => ({ url }))
: [], : [],
photoList: data['活动照片'] || [], photoList: (data['活动照片'] || []).map((url: string) => ({ url })) || [],
} }
mode.value = data._mode || null mode.value = data._mode || null
}, },
...@@ -407,18 +438,34 @@ const rules: FormRules = { ...@@ -407,18 +438,34 @@ const rules: FormRules = {
trigger: ['blur', 'input'], trigger: ['blur', 'input'],
message: '请输入活动地址', message: '请输入活动地址',
}, },
count: { count: [
type: 'number', {
required: true, type: 'number',
trigger: ['blur', 'input'], required: true,
message: '请输入实际参与人数', trigger: ['blur', 'input'],
}, message: '请输入实际参与人数',
excludeCount: { },
type: 'number', {
required: true, type: 'number',
trigger: ['blur', 'input'], validator: (_, val) => checkMemberCount(val),
message: '请输入不计入参与活动党员人数', message: '不能大于党员总数',
}, trigger: ['blur', 'input'],
},
],
excludeCount: [
{
type: 'number',
required: true,
trigger: ['blur', 'input'],
message: '请输入不计入参与活动党员人数',
},
{
type: 'number',
validator: (_, val) => checkMemberExcludeCount(val),
message: '不能大于实际参与人数',
trigger: ['blur', 'input'],
},
],
fileType: { fileType: {
required: true, required: true,
trigger: ['blur', 'change'], trigger: ['blur', 'change'],
...@@ -438,21 +485,34 @@ const submit = async () => { ...@@ -438,21 +485,34 @@ const submit = async () => {
detailRef.value?.validate((errors: any) => errors && (noMistake = false)) detailRef.value?.validate((errors: any) => errors && (noMistake = false))
if (!noMistake) return if (!noMistake) return
console.log(basicData.value, memberData.value, detailData.value) 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') { if (mode.value === 'modify') {
await usePutActivity({
separate: [
{
id: props.data.id,
extra: data,
},
],
})
closeDrawer()
props.cb && props.cb()
message.success('活动修改成功')
} else { } 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({ await usePostActivity({
extra: data, extra: data,
}) })
...@@ -462,20 +522,11 @@ const submit = async () => { ...@@ -462,20 +522,11 @@ const submit = async () => {
} }
} }
const previewFileList = ref([ // const previewFileList = ref([
{ // {
id: 'react', // url: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg',
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 previewImageUrl = ref('') const previewImageUrl = ref('')
const showPhoto = ref(false) const showPhoto = ref(false)
function handlePreview(file: any) { function handlePreview(file: any) {
...@@ -484,9 +535,97 @@ function handlePreview(file: any) { ...@@ -484,9 +535,97 @@ function handlePreview(file: any) {
showPhoto.value = true showPhoto.value = true
} }
async function onChange(options: any) { async function onChange(options: any, type: string) {
console.log('change', options) console.log('change', options, type)
await useAliOss(options.file.file) 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> </script>
...@@ -524,6 +663,13 @@ async function onChange(options: any) { ...@@ -524,6 +663,13 @@ async function onChange(options: any) {
height .14rem height .14rem
background $red background $red
margin-right .06rem margin-right .06rem
>span
flex 1
text-align right
font-family $font-ping
font-size .1rem
>span
color $blue
</style> </style>
<style lang="stylus"> <style lang="stylus">
@import '../../components/MyComponent/main.styl' @import '../../components/MyComponent/main.styl'
...@@ -539,4 +685,12 @@ async function onChange(options: any) { ...@@ -539,4 +685,12 @@ async function onChange(options: any) {
background #f3f4f7 background #f3f4f7
border-left .01rem solid $light-gray border-left .01rem solid $light-gray
padding 0 .1rem 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> </style>
<template> <template>
<Map ref="map" /> <!-- <Map ref="map" /> -->
<NavBar @focus="showTag = false" @blur="showTag = true" /> <NavBar @focus="showTag = false" @blur="showTag = true" />
<BasicInfo :visible="showTag" /> <BasicInfo :visible="showTag" />
<div v-if="showReset" class="reset" @click="resetMap"> <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