diff --git a/src/views/modules/statisticsBusiness/vipStatistics/monthVip.vue b/src/views/modules/statisticsBusiness/vipStatistics/monthVip.vue new file mode 100644 index 0000000..923b0e4 --- /dev/null +++ b/src/views/modules/statisticsBusiness/vipStatistics/monthVip.vue @@ -0,0 +1,356 @@ + + + + + + + + + 刷新 + + + 下载报表 + + + + + + + {{ card.title }} + + + {{ line.label }} + {{ line.value }}{{ line.unit || '' }} + + + + + + + + 月度VIP明细({{ dataList.length }}) + + + + + + + + + + + + + {{ formatAmountCell(scope.row.currentAmount) }} + + + + + + + + + + + + + + + diff --git a/src/views/modules/statisticsBusiness/vipStatistics/monthVipDataHelper.js b/src/views/modules/statisticsBusiness/vipStatistics/monthVipDataHelper.js new file mode 100644 index 0000000..90c4475 --- /dev/null +++ b/src/views/modules/statisticsBusiness/vipStatistics/monthVipDataHelper.js @@ -0,0 +1,191 @@ +function toNumber(value) { + const num = Number(value) + return Number.isNaN(num) ? 0 : num +} + +function getAmount(value) { + return toNumber(value).toFixed(2).replace(/\.00$/, '') +} + +function formatRate(current, previous) { + const currentValue = toNumber(current) + const previousValue = toNumber(previous) + + if (previousValue === 0) { + return currentValue === 0 ? '0.00%' : '--' + } + + return (((currentValue - previousValue) / previousValue) * 100).toFixed(2) + '%' +} + +function formatYear(year) { + if (year === '' || year === null || year === undefined) { + return '--' + } + return year + '年' +} + +function formatText(value) { + if (value === '' || value === null || value === undefined) { + return '--' + } + + const text = String(value).trim() + return text || '--' +} + +function getDynamicKeys(...sources) { + const keys = [] + + sources.forEach((source) => { + Object.keys(source || {}).forEach((key) => { + const text = formatText(key) + if (text !== '--' && !keys.includes(text)) { + keys.push(text) + } + }) + }) + + return keys +} + +function buildCountLines(totalLabel, totalValue, counts) { + const dynamicLines = getDynamicKeys(counts).map((key) => ({ + label: key, + value: toNumber(counts[key]), + unit: '人' + })) + + return [{ label: totalLabel, value: toNumber(totalValue), unit: '人' }].concat(dynamicLines) +} + +function buildRenewalLines(currentData) { + const dynamicKeys = getDynamicKeys(currentData.banCounts, currentData.yanCounts) + + return dynamicKeys.map((key) => ({ + label: key, + value: '开发中', + unit: '' + })) +} + +function buildGrowthLines(currentData, previousData) { + return [ + { + label: '办理总人数环比增长率', + value: formatRate(currentData.banTotalCount, previousData.banTotalCount) + }, + { + label: '延期总人数环比增长率', + value: formatRate(currentData.yanTotalCount, previousData.yanTotalCount) + }, + { + label: '办理金额环比增长率', + value: formatRate(currentData.banTotalPrice, previousData.banTotalPrice) + }, + { + label: '延期金额环比增长率', + value: formatRate(currentData.yanTotalPrice, previousData.yanTotalPrice) + } + ] +} + +function buildCards(currentData, previousData) { + return [ + { + title: '办理统计', + lines: buildCountLines('办理总人数', currentData.banTotalCount, currentData.banCounts) + }, + { + title: '延期统计', + lines: buildCountLines('延期总人数', currentData.yanTotalCount, currentData.yanCounts) + }, + { + title: '金额统计', + lines: [ + { label: '总办理金额', value: getAmount(currentData.banTotalPrice), unit: '元' }, + { label: '总延期金额', value: getAmount(currentData.yanTotalPrice), unit: '元' }, + { + label: '合计总金额', + value: getAmount(toNumber(currentData.banTotalPrice) + toNumber(currentData.yanTotalPrice)), + unit: '元' + } + ] + }, + // { + // title: '续费率', + // lines: buildRenewalLines(currentData) + // }, + { + title: '环比增长率', + lines: buildGrowthLines(currentData, previousData) + } + ] +} + +function buildTableRows(list) { + return (list || []).map((item) => { + return { + time: formatText(item.payTime), + name: formatText(item.name), + tel: formatText(item.tel), + vipType: formatText(item.vipType), + isYan: formatText(item.isYan), + currentYear: formatYear(item.year), + currentAmount: getAmount(item.price), + currentStartTime: formatText(item.uvlStartTime || item.startTime), + currentEndTime: formatText(item.uvlEndTime || item.endTime), + endTime: formatText(item.endTime || item.uvlEndTime), + isReBuy: formatText(item.isReBuy) + } + }) +} + +export function getCurrentMonthValue() { + const now = new Date() + const year = now.getFullYear() + const month = String(now.getMonth() + 1).padStart(2, '0') + return `${year}-${month}` +} + +export function normalizeMonthValue(value) { + if (!value) { + return getCurrentMonthValue() + } + + if (Object.prototype.toString.call(value) === '[object Date]' && !Number.isNaN(value.getTime())) { + const year = value.getFullYear() + const month = String(value.getMonth() + 1).padStart(2, '0') + return `${year}-${month}` + } + + const text = String(value).trim() + const matched = text.match(/^(\d{4})-(\d{1,2})/) + if (matched) { + return `${matched[1]}-${String(matched[2]).padStart(2, '0')}` + } + + return getCurrentMonthValue() +} + +export function getPreviousMonthValue(monthValue) { + const normalizedMonthValue = normalizeMonthValue(monthValue) + const parts = normalizedMonthValue.split('-') + if (parts.length !== 2) { + return getCurrentMonthValue() + } + + const year = Number(parts[0]) + const month = Number(parts[1]) + const date = new Date(year, month - 2, 1) + const prevYear = date.getFullYear() + const prevMonth = String(date.getMonth() + 1).padStart(2, '0') + return `${prevYear}-${prevMonth}` +} + +export function buildMonthVipViewData(currentData = {}, previousData = {}) { + return { + cards: buildCards(currentData, previousData), + tableRows: buildTableRows(currentData.allResultList) + } +} diff --git a/src/views/modules/statisticsBusiness/vipStatistics/yearVip.vue b/src/views/modules/statisticsBusiness/vipStatistics/yearVip.vue new file mode 100644 index 0000000..a93c656 --- /dev/null +++ b/src/views/modules/statisticsBusiness/vipStatistics/yearVip.vue @@ -0,0 +1,348 @@ + + + + + + + + + 刷新 + + + 下载报表 + + + + + + + {{ card.title }} + + + {{ line.label }} + {{ line.value }}{{ line.unit || '' }} + + + + + + + VIP办理情况统计 + + + + + + + + + + + + + + + VIP分类年度办理人数占比 + + + + + + + + 每月明细 + + + + + + + {{ scope.row.amount }}元 + + + + + + + + + + diff --git a/src/views/modules/statisticsBusiness/vipStatistics/yearVipDataHelper.js b/src/views/modules/statisticsBusiness/vipStatistics/yearVipDataHelper.js new file mode 100644 index 0000000..a6073f9 --- /dev/null +++ b/src/views/modules/statisticsBusiness/vipStatistics/yearVipDataHelper.js @@ -0,0 +1,247 @@ +const TYPE_ORDER = [ + '中医学', + '中西汇通学', + '肿瘤学', + '针灸学', + '心理学', + '国学', + '医学超级', + '心理国学超级' +] + +const TYPE_ALIAS_MAP = { + 国学心理学超级: '心理国学超级' +} + +function toNumber(value) { + const num = Number(value) + return Number.isNaN(num) ? 0 : num +} + +function formatAmount(value) { + return toNumber(value).toFixed(2).replace(/\.00$/, '') +} + +function normalizeTypeName(name) { + return TYPE_ALIAS_MAP[name] || name +} + +function getCurrentYearValue() { + return String(new Date().getFullYear()) +} + +function normalizeYearValue(value) { + if (!value) { + return getCurrentYearValue() + } + + if (Object.prototype.toString.call(value) === '[object Date]' && !Number.isNaN(value.getTime())) { + return String(value.getFullYear()) + } + + const matched = String(value).trim().match(/^(\d{4})/) + if (matched) { + return matched[1] + } + + return getCurrentYearValue() +} + +function getPreviousYearValue(yearValue) { + return String(Number(normalizeYearValue(yearValue)) - 1) +} + +function getDynamicTypeKeys(...sources) { + const keys = [] + + sources.forEach((source) => { + Object.keys(source || {}).forEach((key) => { + const normalizedKey = normalizeTypeName(key) + if (!keys.includes(normalizedKey)) { + keys.push(normalizedKey) + } + }) + }) + + const orderedKeys = TYPE_ORDER.filter((key) => keys.includes(key)) + const restKeys = keys.filter((key) => !TYPE_ORDER.includes(key)) + return orderedKeys.concat(restKeys) +} + +function getMappedCount(source, typeName, yearKey) { + const originalKey = Object.keys(source || {}).find((key) => normalizeTypeName(key) === typeName) + if (!originalKey) { + return 0 + } + return toNumber(((source || {})[originalKey] || {})[yearKey]) +} + +function getMappedValue(source, typeName) { + const originalKey = Object.keys(source || {}).find((key) => normalizeTypeName(key) === typeName) + if (!originalKey) { + return 0 + } + return toNumber((source || {})[originalKey]) +} + +function buildCountLines(totalLabel, totalValue, counts) { + return [{ label: totalLabel, value: toNumber(totalValue), unit: '人' }].concat( + getDynamicTypeKeys(counts).map((typeName) => ({ + label: typeName, + value: getMappedValue(counts, typeName), + unit: '人' + })) + ) +} + +function formatYearRate(current, previous) { + const currentValue = toNumber(current) + const previousValue = toNumber(previous) + + if (currentValue === 0) { + return '0.00%' + } + + return `${(((currentValue - previousValue) / currentValue) * 100).toFixed(2)}%` +} + +function buildSummaryCards(currentData, previousData) { + return [ + { + title: '办理统计', + lines: buildCountLines('办理总人数', currentData.banTotalCount, currentData.banTypeTotalCounts) + }, + { + title: '延期统计', + lines: buildCountLines('延期总人数', currentData.yanTotalCount, currentData.yanTypeTotalCounts) + }, + { + title: '金额统计', + lines: [ + { label: '总办理金额', value: formatAmount(currentData.banTotalPrice), unit: '元' }, + { label: '总延期金额', value: formatAmount(currentData.yanTotalPrice), unit: '元' }, + { + label: '合计总金额', + value: formatAmount(toNumber(currentData.banTotalPrice) + toNumber(currentData.yanTotalPrice)), + unit: '元' + } + ] + }, + { + title: '同比增长率', + lines: [ + { + label: '办理总人数同比增长率', + value: formatYearRate(currentData.banTotalCount, previousData.banTotalCount) + }, + { + label: '延期总人数同比增长率', + value: formatYearRate(currentData.yanTotalCount, previousData.yanTotalCount) + }, + { + label: '办理金额同比增长率', + value: formatYearRate(currentData.banTotalPrice, previousData.banTotalPrice) + }, + { + label: '延期金额同比增长率', + value: formatYearRate(currentData.yanTotalPrice, previousData.yanTotalPrice) + } + ] + } + ] +} + +function buildHandleTableRows(currentData) { + const typeKeys = getDynamicTypeKeys(currentData.banCounts, currentData.yanCounts) + const rows = typeKeys.map((typeName) => { + const ban3 = getMappedCount(currentData.banCounts, typeName, '3') + const ban4 = getMappedCount(currentData.banCounts, typeName, '4') + const banOther = getMappedCount(currentData.banCounts, typeName, '其他') + const yan1 = getMappedCount(currentData.yanCounts, typeName, '1') + const yan3 = getMappedCount(currentData.yanCounts, typeName, '3') + const yan4 = getMappedCount(currentData.yanCounts, typeName, '4') + const yanOther = getMappedCount(currentData.yanCounts, typeName, '其他') + + return { + typeName, + ban3, + ban4, + banOther, + yan1, + yan3, + yan4, + yanOther, + total: ban3 + ban4 + banOther + yan1 + yan3 + yan4 + yanOther + } + }) + + const totalRow = rows.reduce((acc, item) => { + acc.ban3 += item.ban3 + acc.ban4 += item.ban4 + acc.banOther += item.banOther + acc.yan1 += item.yan1 + acc.yan3 += item.yan3 + acc.yan4 += item.yan4 + acc.yanOther += item.yanOther + acc.total += item.total + return acc + }, { + typeName: '合计', + ban3: 0, + ban4: 0, + banOther: 0, + yan1: 0, + yan3: 0, + yan4: 0, + yanOther: 0, + total: 0 + }) + + return rows.concat(totalRow) +} + +function buildRatioTableRows(currentData) { + return getDynamicTypeKeys(currentData.banTypeRatios).map((typeName) => { + const originalKey = Object.keys(currentData.banTypeRatios || {}).find((key) => normalizeTypeName(key) === typeName) + return { + typeName, + ratio: (currentData.banTypeRatios || {})[originalKey] || '0%' + } + }) +} + +function buildMonthTableRows(currentData) { + const rows = Array.from({ length: 12 }, (_, index) => { + const month = String(index + 1) + return { + monthLabel: `${month}月`, + banCount: toNumber((currentData.banMonthCounts || {})[month]), + yanCount: toNumber((currentData.yanMonthCounts || {})[month]), + amount: formatAmount((currentData.monthAmounts || {})[month]) + } + }) + + rows.push({ + monthLabel: '合计', + banCount: toNumber(currentData.banTotalCount), + yanCount: toNumber(currentData.yanTotalCount), + amount: formatAmount(currentData.banTotalPrice) + }) + + return rows +} + +export { + getCurrentYearValue, + getPreviousYearValue, + normalizeYearValue +} + +export function buildYearVipViewData(currentData = {}, previousData = {}) { + return { + summaryCards: buildSummaryCards(currentData, previousData), + handleTableRows: buildHandleTableRows(currentData), + ratioTableRows: buildRatioTableRows(currentData), + monthTableRows: buildMonthTableRows(currentData) + } +}