import * as Excel from 'exceljs' import * as moment from 'moment' import * as config from 'config' import Base from './base' import * as _ from 'lodash' import i18n from '../../../service/i18n' import { App, Context } from '../../../types' export default class ProjectClusterExportFactory extends Base { private _customfieldIds: string[] private _lang: string private _projects: any [] private _customfields: any [] private _title: string private _sheetName: string private _fileName: string private _headers: any [] private _projectCount: number set customfieldIds (customfieldIds: string []) { this._customfieldIds = customfieldIds } get customfieldIds () { return this._customfieldIds } set lang (lang: string) { this._lang = lang } get lang () { return this._lang } set title (title: string) { this._title = title } get title () { return this._title } set fileName (fileName: string) { this._fileName = fileName } get fileName () { return this._fileName } set sheetName (sheetName: string) { this._sheetName = sheetName } get sheetName () { return this._sheetName } set projects (projects: any) { this._projects = projects } get projects () { return this._projects } set customfields (customfields: any) { this._customfields = customfields } get customfields () { return this._customfields } set headers (headers: any []) { this._headers = headers } get headers () { return this._headers } set projectCount (projectCount: number) { this._projectCount = projectCount } get projectCount () { return this._projectCount } async build () { const xlsx = new XLSX(this.app, this.ctx) xlsx.title = this.title xlsx.fileName = this.fileName xlsx.sheetName = this.sheetName xlsx.projectCount = this.projectCount xlsx.lang = this.lang await xlsx.setContent(this.projects, this.customfields, this.headers) .execute() } } // 导出为 xlsx class XLSX { protected app: App protected ctx: Context private projects: any [] private customfields: any [] private _sheetName: string private _fileName: string private _title: string private _lang: string private _projectCount: number private headers: any [] // 初始headers constructor (app: App, ctx: Context) { this.app = app this.ctx = ctx } set projectCount (projectCount: number) { this._projectCount = projectCount } get projectCount () { return this._projectCount } set fileName (fileName: string) { this._fileName = fileName } set sheetName (_sheetName: string) { this._sheetName = _sheetName } set title (title: string) { this._title = title } get title () { return this._title } set lang (lang: string) { this._lang = lang } get lang () { return this._lang } setContent (projects: any, customfields: any, headers: any []) { this.projects = projects this.customfields = customfields this.headers = headers return this } async execute () { i18n.setLocale(this.lang) if (this._projectCount > config.EXPORT_LIMIT.PROJECT_LIMIT) { this.ctx.throw(`ExceedLimit: ${config.EXPORT_LIMIT.PROJECT_LIMIT}`, 400) } const workbook: Excel.Workbook = new Excel.Workbook() const excelheaders = this.buildHeaders() let worksheet: Excel.Worksheet worksheet = workbook.addWorksheet(this._sheetName) worksheet.columns = this.buildTitle() worksheet.addRow(excelheaders.map((ele) => ele.value)) this.projects.map((project) => { const row = this.generateExcelRow(project, excelheaders) worksheet.addRow(row) }) // 样式 worksheet.eachRow(function(row) { row.font = { name: 'Arial', size: 12 } }) excelheaders.map(function (header, index) { const { key } = header if (key === 'project.name') { worksheet.getColumn(index + 1).font = { name: 'Arial', size: 12, color: { argb: '0000FF' } } } }) worksheet.getRow(1).font = { name: 'Arial', color: { argb: '000000' }, size: 13, bold: true } worksheet.getRow(2).font = { name: 'Arial', color: { argb: '000000' }, size: 13, bold: true } workbook.xlsx.createInputStream() const fileName: string = `${this._fileName}.xlsx` this.ctx.response.set('Set-Cookie', 'fileDownload=true; path=/') this.ctx.status = 200 this.ctx.response.attachment(fileName) await workbook.xlsx.write(this.ctx.res) this.ctx.res.end() } buildHeaders() { i18n.setLocale(this.lang) const headers = [] const self = this const getItem = function (key: string, type: string) { if (!key) { return { key: null, type, value: null } } if (type === 'basic') { return { key, type, value: i18n.__(key) } } else { // 自定义字段 const customfield = self.customfields.find(function (item: any) { return item && `${item._id}` === `${key}` }) if (!customfield) { return { key: null, type, value: null } } return { key, type, value: customfield.name } } } let hasTaskTotal = false this.headers.map((header) => { const item = header.split(':') const type = item[0] const key = item[1] // 基础字段 if (type === 'basic') { switch (key) { case 'project.status.degree': headers.push(getItem(key, type), getItem('project.status.update.time', type), getItem('project.status.content', type)) break case 'project.task.done.degree': if (!hasTaskTotal) { hasTaskTotal = true headers.push(getItem('project.report.task.all', type)) } headers.push(getItem('project.report.task.done', type), getItem('project.task.done.degree', type)) break case 'project.task.overdue.degree': if (!hasTaskTotal) { hasTaskTotal = true headers.push(getItem('project.report.task.all', type)) } headers.push(getItem('project.report.task.overdue', type), getItem('project.task.overdue.degree', type)) break case 'project.name': case 'project.member.owners': case 'project.report.task.unallocated': case 'project.create.time': case 'project.duration.time': headers.push(getItem(key, type)) break default: headers.push(getItem(null, type)) } } else { // 自定义字段 headers.push(getItem(key, type)) } }) return headers } buildTitle () { const titles = [] this.headers.map(() => { titles.push(this._formatCell(null, 20)) }) titles[0] = this._formatCell(this.title, 20) return titles } private generateExcelRow (project: any, excelheaders: any []) { i18n.setLocale(this.lang) const row = [] if (!project) { return row } excelheaders.map((header) => { const {key, type} = header // 基础字段 if (type === 'basic') { switch (key) { case 'project.name': row.push({ text: project.name, hyperlink: `${config.TEAMBITION.CORE.DOMAIN}/project/${project._id}`, }) break case 'project.status.degree': if (project.statusInfo && project.statusInfo.degree) { row.push(i18n.__(project.statusInfo.degree)) } else { row.push(i18n.__('no.status')) } break case 'project.status.update.time': if (project.statusInfo && project.statusInfo.updated) { row.push(moment(project.statusInfo.updated).format('YYYY/MM/DD HH:MM')) } else { row.push(null) } break case 'project.status.content': if (project.statusInfo) { row.push(project.statusInfo.content) } else { row.push(i18n.__('no.status')) } break case 'project.report.task.all': if (project.report) { row.push(project.report.all || 0) } else { row.push(0) } break case 'project.report.task.done': if (project.report) { row.push(project.report.done || 0) } else { row.push(0) } break case 'project.member.owners': if (project.members && project.members.length) { const members = project.members.map(function (member) { return `${member.user.name}(${member.user.email})` }) row.push(members.join(' | ')) } else { row.push(null) } break case 'project.report.task.unallocated': if (project.report) { row.push(project.report.unallocated || 0) } else { row.push(0) } break case 'project.report.task.overdue': if (project.report) { row.push(project.report.overdue || 0) } else { row.push(0) } break case 'project.task.done.degree': if (project.report) { const done = project.report.done const all = project.report.all if (all) { const doneDegree = (done / all) * 100 row.push(`${doneDegree.toFixed()}%`) } else { row.push('0%') } } break case 'project.task.overdue.degree': if (project.report) { const overdue = project.report.overdue const all = project.report.all if (all) { const overdueDegree = (overdue / all) * 100 row.push(`${overdueDegree.toFixed()}%`) } else { row.push('0%') } } break case 'project.create.time': if (project.created) { row.push(moment(project.created).format('YYYY/MM/DD HH:MM')) } else { row.push(null) } break case 'project.duration.time': if (project.created) { const days = Math.ceil(moment.duration(moment().diff(moment(project.created)), 'ms').asDays()) row.push(`${days}${i18n.__('day')}`) } else { row.push(null) } break default: row.push(null) return } } else { // 自定义字段 if (!key) { row.push(null) return } const customfield = this.customfields.find(function (item) { return item && `${item._id}` === `${key}` }) const customfieldValue = project.customfields.find(function (item) { return item && `${item._customfieldId}` === `${key}` }) if (!customfield || !customfieldValue) { row.push(null) return } const value = this._getCustomFieldValue(customfield, customfieldValue) row.push(value) } }) return row } private _getCustomFieldValue (customfield: any, customfieldValue: any) { const { value = [] } = customfieldValue switch (customfield.type) { case 'dropDown': case 'multipleChoice': return _.chain(value) .map((choiceId) => { const choices = customfield.choices || [] const choice = choices.find(function (choice) { return `${choice._id}` === `${choiceId}` }) return choice && choice.value }) .compact() .value() .join(', ') case 'date': const dateTime = value[0] if (!dateTime) { return '' } return moment(dateTime).format('YYYY/MM/DD') case 'number': case 'text': return value[0] || '' case 'commongroup': const commonGroup = customfieldValue.value[0] || {} // 通用分组需要前端二次验证 value 是否存在 choices 中 return customfield.choices.find(function (ele) { return ele._id === commonGroup._id }) case 'lookup': return this._getValueForAdvanceCustomfield(customfieldValue.value) case 'work': return '' } } /** * @description 格式化列的内容及属性 */ private _formatCell(content: string, width: number, horizontal = 'left') { return { header: content, width, outlineLevel: 1, style: { alignment: { horizontal } } } } private _getValueForAdvanceCustomfield (value: any) { if (value && _.get(value, 'length')) { return value.map((ele: any) => _.get(ele, 'title', '')).join(' | ') } else { return '' } } }