| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482 |
- /** 大屏首页 ECharts 配置(深色主题,与 screen-theme 一致) */
- const AXIS_LABEL = { color: '#9fb0c3', fontSize: 10 }
- /** 12 个月横轴:不旋转,略缩小字号以适配窄图 */
- const MONTH_AXIS_LABEL = { color: '#9fb0c3', fontSize: 9, margin: 6 }
- const AXIS_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.35)' } }
- const SPLIT_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.12)' } }
- const GRID = { left: 22, right: 8, top: 24, bottom: 6, containLabel: true }
- function emptyOption(text = '暂无数据') {
- return {
- title: {
- text,
- left: 'center',
- top: 'middle',
- textStyle: { color: '#6a9f90', fontSize: 13, fontWeight: 'normal' }
- }
- }
- }
- function pickNumber(row, fields) {
- for (const key of fields) {
- const raw = row?.[key]
- if (raw != null && raw !== '') {
- const n = Number(raw)
- if (!Number.isNaN(n)) {
- return n
- }
- }
- }
- return 0
- }
- /** 滚动最近 12 月:按 statYear、month 升序 */
- function sortRollingMonths(list) {
- return [...(list || [])].sort((a, b) => {
- const ak = (Number(a.statYear) || 0) * 100 + (Number(a.month) || 0)
- const bk = (Number(b.statYear) || 0) * 100 + (Number(b.month) || 0)
- return ak - bk
- })
- }
- function rollingMonthLabel(row) {
- return `${Number(row.month)}月`
- }
- function rollingTooltipTitle(row) {
- if (!row) {
- return ''
- }
- return `${row.statYear}年${Number(row.month)}月`
- }
- function buildRollingAxis(list) {
- const rows = sortRollingMonths(list)
- return {
- rows,
- categories: rows.map(rollingMonthLabel),
- titleAt: (dataIndex) => rollingTooltipTitle(rows[dataIndex])
- }
- }
- function rollingSeries(list, field, altFields = []) {
- const fields = [field, ...altFields]
- return sortRollingMonths(list).map((row) => pickNumber(row, fields))
- }
- /** 牦牛存栏变动:公牛 / 母牛折线(滚动 12 月) */
- export function buildInventoryTrendOption(monthlyTrend) {
- if (!monthlyTrend?.length) {
- return emptyOption()
- }
- const axis = buildRollingAxis(monthlyTrend)
- return {
- color: ['#5ef0c8', '#ecd27b'],
- tooltip: {
- trigger: 'axis',
- formatter: (params) => {
- if (!Array.isArray(params) || !params.length) {
- return ''
- }
- const title = axis.titleAt(params[0].dataIndex)
- const lines = params.map((p) => `${p.marker}${p.seriesName} ${p.value} 头`)
- return `${title}<br/>${lines.join('<br/>')}`
- }
- },
- legend: {
- top: 0,
- right: 8,
- textStyle: { color: '#a8d4c8', fontSize: 10 },
- itemWidth: 12,
- itemHeight: 8
- },
- grid: GRID,
- xAxis: {
- type: 'category',
- data: axis.categories,
- axisLabel: { ...MONTH_AXIS_LABEL, interval: 0 },
- axisLine: AXIS_LINE,
- axisTick: { show: false }
- },
- yAxis: {
- type: 'value',
- axisLabel: AXIS_LABEL,
- axisLine: AXIS_LINE,
- splitLine: SPLIT_LINE
- },
- series: [
- {
- name: '公牛',
- type: 'line',
- smooth: true,
- symbol: 'circle',
- symbolSize: 4,
- data: rollingSeries(monthlyTrend, 'bullCount')
- },
- {
- name: '母牛',
- type: 'line',
- smooth: true,
- symbol: 'circle',
- symbolSize: 4,
- data: rollingSeries(monthlyTrend, 'cowCount')
- }
- ]
- }
- }
- /** 年龄四档配色(母牛分龄:1岁以下 / 1~3岁 / 3~12岁 / 12岁以上) */
- const AGE_BAND_COLOR = {
- COW_U1: '#45c997',
- COW_1_3: '#6eb5ff',
- COW_3_12: '#547233',
- COW_O12: '#ecd27b'
- }
- const AGE_BAND_ORDER = ['COW_U1', 'COW_1_3', 'COW_3_12', 'COW_O12']
- function sortAgeBands(ageStructure) {
- const map = new Map((ageStructure || []).map((item) => [item.bandCode, item]))
- const ordered = AGE_BAND_ORDER.map((code) => map.get(code)).filter(Boolean)
- return ordered.length ? ordered : [...(ageStructure || [])]
- }
- function formatAgeRatio(ratio, percent) {
- if (ratio != null && ratio !== '') {
- return `${Number(ratio).toFixed(0)}%`
- }
- return `${Math.round(percent)}%`
- }
- /** 牦牛年龄结构(环形图 + 右侧图例,固定四档) */
- export function buildAgeStructureOption(ageStructure) {
- if (!ageStructure?.length) {
- return emptyOption()
- }
- const bands = sortAgeBands(ageStructure)
- const data = bands.map((item) => ({
- name: item.bandLabel || item.bandCode,
- value: pickNumber(item, ['count']),
- ratio: item.ratio,
- itemStyle: {
- color: AGE_BAND_COLOR[item.bandCode] || '#45f0b8',
- shadowBlur: 8,
- shadowColor: 'rgba(0, 0, 0, 0.45)',
- shadowOffsetY: 2
- }
- }))
- const hasSlice = data.some((d) => d.value > 0)
- const centerGraphic = hasSlice
- ? []
- : [
- {
- type: 'text',
- left: '26%',
- top: '46%',
- style: {
- text: '暂无存栏',
- fill: '#6a9f90',
- fontSize: 11,
- textAlign: 'center'
- }
- }
- ]
- return {
- color: Object.values(AGE_BAND_COLOR),
- tooltip: {
- trigger: 'item',
- formatter: (p) => {
- const row = bands[p.dataIndex]
- const ratio =
- row?.ratio != null ? `${Number(row.ratio).toFixed(1)}%` : `${p.percent?.toFixed(1)}%`
- return `${p.name}<br/>${p.value} 头 (${ratio})`
- }
- },
- legend: {
- orient: 'vertical',
- right: '2%',
- top: 'middle',
- icon: 'circle',
- itemWidth: 8,
- itemHeight: 8,
- itemGap: 10,
- textStyle: { color: '#e8eef5', fontSize: 10 }
- },
- graphic: [
- {
- type: 'circle',
- left: '22%',
- top: '38%',
- shape: { r: 52 },
- style: {
- fill: 'transparent',
- stroke: 'rgba(61, 217, 176, 0.22)',
- lineWidth: 1
- },
- z: 0
- },
- {
- type: 'circle',
- left: '20%',
- top: '36%',
- shape: { r: 58 },
- style: {
- fill: 'transparent',
- stroke: 'rgba(61, 217, 176, 0.12)',
- lineWidth: 1
- },
- z: 0
- },
- ...centerGraphic
- ],
- series: [
- {
- type: 'pie',
- radius: ['42%', '68%'],
- center: ['34%', '54%'],
- avoidLabelOverlap: true,
- stillShowZeroSum: false,
- itemStyle: {
- borderColor: 'rgba(2, 22, 18, 0.9)',
- borderWidth: 2
- },
- label: {
- show: hasSlice,
- position: 'inside',
- color: '#fff',
- fontSize: 10,
- fontWeight: 'bold',
- formatter: (p) => formatAgeRatio(p.data.ratio, p.percent)
- },
- labelLine: { show: false },
- emphasis: {
- scale: true,
- scaleSize: 5
- },
- data: hasSlice ? data : []
- }
- ]
- }
- }
- /** 牦牛存栏结构 — 内环大类配色 */
- const YAK_CATEGORY_COLOR = {
- BULL: '#5ef0c8',
- COW: '#6eb5ff',
- INSURED: '#ecd27b'
- }
- /** 牦牛存栏结构 — 外环细分配色(对齐科技特派员双层饼风格) */
- const YAK_DETAIL_COLOR = {
- BREEDING_BULL: '#5ef0c8',
- BULL_CASTRATED: '#3b9e84',
- COW_YOUNG: '#6eb5ff',
- COW_ADULT: '#98e9aa',
- INSURED: '#ecd27b'
- }
- /**
- * 牦牛存栏结构(内外双层饼,样式对齐畜牧资源「科技特派员」):
- * 内环大类 BULL/COW/INSURED,外环 details 细分。
- * @param {Array} structure yakInventoryStructure[]
- */
- export function buildYakInventoryStructureOption(structure) {
- if (!structure?.length) {
- return emptyOption()
- }
- const categoryTotal = structure.reduce((sum, item) => sum + pickNumber(item, ['count']), 0)
- const detailRows = []
- for (const cat of structure) {
- const categoryLabel = cat?.categoryLabel || cat?.categoryCode || ''
- const details = cat?.details
- if (details?.length) {
- for (const d of details) {
- detailRows.push({
- ...d,
- categoryLabel
- })
- }
- } else if (cat) {
- detailRows.push({
- detailCode: cat.categoryCode,
- detailLabel: cat.categoryLabel,
- count: cat.count,
- ratio: cat.ratio,
- categoryLabel
- })
- }
- }
- const detailTotal = detailRows.reduce((sum, item) => sum + pickNumber(item, ['count']), 0)
- if (categoryTotal <= 0 && detailTotal <= 0) {
- return emptyOption()
- }
- const fallbackColors = ['#5ef0c8', '#6eb5ff', '#ecd27b', '#98e9aa', '#3b9e84']
- const innerData = structure.map((item, i) => {
- const code = item.categoryCode || ''
- const color = YAK_CATEGORY_COLOR[code] || fallbackColors[i % fallbackColors.length]
- return {
- name: item.categoryLabel || code || '未知',
- value: pickNumber(item, ['count']),
- ratio: item.ratio,
- itemStyle: { color }
- }
- })
- const outerData = detailRows.map((item, i) => {
- const code = item.detailCode || ''
- const color = YAK_DETAIL_COLOR[code] || fallbackColors[i % fallbackColors.length]
- return {
- name: item.detailLabel || code || '未知',
- value: pickNumber(item, ['count']),
- ratio: item.ratio,
- categoryLabel: item.categoryLabel || '',
- itemStyle: { color },
- label: { color },
- labelLine: { lineStyle: { color } }
- }
- })
- const pieBorder = {
- borderColor: 'rgba(8, 48, 42, 0.88)',
- borderWidth: 2
- }
- return {
- title: { show: false },
- tooltip: {
- trigger: 'item',
- formatter: (p) => {
- const ratio =
- p.data?.ratio != null ? `${Number(p.data.ratio).toFixed(1)}%` : `${p.percent?.toFixed(1)}%`
- const seriesName =
- p.seriesName === '存栏细分' && p.data?.categoryLabel
- ? `${p.seriesName}(${p.data.categoryLabel})`
- : p.seriesName
- return `${seriesName}<br/>${p.marker}${p.name}: ${p.value ?? 0} 头 (${ratio})`
- }
- },
- legend: {
- show: false
- },
- series: [
- // 外环:细分(种公牛 / 阉畜 / 幼畜 / 成畜 / 保险)
- {
- name: '存栏细分',
- type: 'pie',
- radius: ['58%', '74%'],
- center: ['50%', '52%'],
- avoidLabelOverlap: true,
- itemStyle: pieBorder,
- label: {
- show: true,
- fontSize: 11,
- formatter: (p) => p.name
- },
- labelLine: {
- show: true,
- length: 10,
- length2: 12
- },
- data: outerData
- },
- // 内饼:公牛 / 母牛 / 保险牛
- {
- name: '存栏大类',
- type: 'pie',
- radius: [0, '46%'],
- center: ['50%', '52%'],
- avoidLabelOverlap: true,
- itemStyle: pieBorder,
- label: {
- show: true,
- position: 'inside',
- color: '#fff',
- fontSize: 12,
- fontWeight: 600,
- formatter: (p) => p.name
- },
- labelLine: { show: false },
- data: innerData
- }
- ]
- }
- }
- /** 出栏柱图金色渐变 */
- const OUTBOUND_BAR_GRADIENT = {
- type: 'linear',
- x: 0,
- y: 1,
- x2: 0,
- y2: 0,
- colorStops: [
- { offset: 0, color: '#6b4f18' },
- { offset: 0.45, color: '#b8923a' },
- { offset: 1, color: '#f7d547' }
- ]
- }
- const OUTBOUND_GRID = { left: 36, right: 8, top: 28, bottom: 6, containLabel: true }
- const OUTBOUND_SPLIT_LINE = {
- lineStyle: { color: 'rgba(120, 140, 160, 0.28)', type: 'dashed' }
- }
- /** 牦牛出栏按月(纯柱状图,滚动 12 月) */
- export function buildOutboundOption(outboundMonthly) {
- if (!outboundMonthly?.length) {
- return emptyOption()
- }
- const axis = buildRollingAxis(outboundMonthly)
- const values = rollingSeries(outboundMonthly, 'outboundCount', ['outbound_count', 'count'])
- return {
- tooltip: {
- trigger: 'axis',
- axisPointer: { type: 'shadow' },
- formatter: (params) => {
- const p = Array.isArray(params) ? params[0] : params
- if (!p) {
- return ''
- }
- const title = axis.titleAt(p.dataIndex)
- return `${title}<br/>出栏 ${p.value} 头`
- }
- },
- grid: OUTBOUND_GRID,
- xAxis: {
- type: 'category',
- data: axis.categories,
- axisLabel: { ...MONTH_AXIS_LABEL, interval: 0 },
- axisLine: { lineStyle: { color: 'rgba(150, 170, 190, 0.45)' } },
- axisTick: { show: false }
- },
- yAxis: {
- type: 'value',
- minInterval: 1,
- axisLabel: AXIS_LABEL,
- axisLine: { show: false },
- axisTick: { show: false },
- splitLine: OUTBOUND_SPLIT_LINE
- },
- series: [
- {
- name: '出栏',
- type: 'bar',
- barWidth: '46%',
- itemStyle: {
- color: OUTBOUND_BAR_GRADIENT,
- borderRadius: [10, 10, 10, 10]
- },
- data: values
- }
- ]
- }
- }
- /** 牲畜出栏六畜配色(偏暗,保证白色文字可读) */
- const LIVESTOCK_OUTBOUND_COLOR = {
- YAK: '#2a9d7a',
- CATTLE: '#b8923a',
- SHEEP: '#3d7ab8',
- GOAT: '#2f4f9e',
- HORSE_MULE: '#c47a1a',
- DONKEY: '#2f8a6a'
- }
- /** 树图过小扇区的布局下限:至少占总量约 4%,保证 1 头也能看见 */
- const LIVESTOCK_TREEMAP_MIN_SHARE = 0.04
- /**
- * 牲畜出栏统计(矩形树图):livestockOutboundStructure[]
- * 项含 speciesCode / speciesLabel / count / ratio / unit
- */
- export function buildLivestockOutboundStructureOption(structure) {
- if (!structure?.length) {
- return emptyOption()
- }
- const rows = structure
- .map((item, i) => {
- const code = item.speciesCode || ''
- const count = pickNumber(item, ['count'])
- return {
- name: item.speciesLabel || code || '未知',
- realCount: count,
- ratio: item.ratio,
- unit: item.unit || '头',
- itemStyle: {
- color: LIVESTOCK_OUTBOUND_COLOR[code] || Object.values(LIVESTOCK_OUTBOUND_COLOR)[i % 6],
- borderColor: 'rgba(2, 22, 18, 0.85)',
- borderWidth: 2,
- borderRadius: 4
- }
- }
- })
- .filter((d) => d.realCount > 0)
- if (!rows.length) {
- return emptyOption()
- }
- const total = rows.reduce((sum, d) => sum + d.realCount, 0)
- const minVisual = Math.max(total * LIVESTOCK_TREEMAP_MIN_SHARE, 1)
- const data = rows.map((d) => ({
- ...d,
- value: Math.max(d.realCount, minVisual)
- }))
- return {
- tooltip: {
- trigger: 'item',
- formatter: (p) => {
- const unit = p.data?.unit || '头'
- const count = p.data?.realCount ?? p.value
- const ratio =
- p.data?.ratio != null
- ? `${Number(p.data.ratio).toFixed(1)}%`
- : total > 0
- ? `${((Number(count) / total) * 100).toFixed(1)}%`
- : '-'
- return `${p.name}<br/>${count}${unit} (${ratio})`
- }
- },
- series: [
- {
- type: 'treemap',
- width: '96%',
- height: '92%',
- left: '2%',
- top: '4%',
- roam: false,
- nodeClick: false,
- breadcrumb: { show: false },
- squareRatio: 0.75 * (1 + Math.sqrt(5)),
- label: {
- show: true,
- formatter: (p) => {
- const unit = p.data?.unit || '头'
- const count = p.data?.realCount ?? p.value
- const ratioText =
- p.data?.ratio != null
- ? `${Number(p.data.ratio).toFixed(0)}%`
- : ''
- return ratioText
- ? `{name|${p.name}}\n{val|${count}${unit}}\n{pct|${ratioText}}`
- : `{name|${p.name}}\n{val|${count}${unit}}`
- },
- rich: {
- name: { color: '#ffffff', fontSize: 11, fontWeight: 600, lineHeight: 16 },
- val: { color: 'rgba(255,255,255,0.92)', fontSize: 10, lineHeight: 14 },
- pct: { color: 'rgba(232,238,245,0.85)', fontSize: 10, lineHeight: 14 }
- }
- },
- upperLabel: { show: false },
- itemStyle: {
- gapWidth: 3,
- borderColor: 'rgba(2, 22, 18, 0.9)'
- },
- emphasis: {
- itemStyle: {
- shadowBlur: 12,
- shadowColor: 'rgba(0, 0, 0, 0.45)'
- }
- },
- levels: [
- {
- itemStyle: {
- borderWidth: 0,
- gapWidth: 3
- }
- }
- ],
- data
- }
- ]
- }
- }
- /** 自食情况柱图:上绿下蓝渐变,顶部圆角 */
- const SELF_CONSUMPTION_BAR_GRADIENT = {
- type: 'linear',
- x: 0,
- y: 0,
- x2: 0,
- y2: 1,
- colorStops: [
- { offset: 0, color: '#3ecf9a' },
- { offset: 0.45, color: '#2a9d9a' },
- { offset: 1, color: '#2f5fad' }
- ]
- }
- /**
- * 自食情况统计(柱状图):selfConsumption.items[]
- * 项含 itemCode / itemLabel / count,单位:头
- */
- export function buildSelfConsumptionOption(selfConsumption) {
- const items = selfConsumption?.items
- if (!items?.length) {
- return emptyOption()
- }
- const categories = items.map((item) => item.itemLabel || item.itemCode || '')
- const values = items.map((item) => pickNumber(item, ['count']))
- if (!values.some((v) => v > 0)) {
- return emptyOption()
- }
- return {
- tooltip: {
- trigger: 'axis',
- axisPointer: { type: 'shadow' },
- formatter: (params) => {
- const p = Array.isArray(params) ? params[0] : params
- if (!p) {
- return ''
- }
- return `${p.name}<br/>自食 ${p.value} 头`
- }
- },
- grid: { left: 36, right: 10, top: 28, bottom: 8, containLabel: true },
- xAxis: {
- type: 'category',
- data: categories,
- axisLabel: { color: '#9fb0c3', fontSize: 11 },
- axisLine: { lineStyle: { color: 'rgba(150, 170, 190, 0.45)' } },
- axisTick: { show: false }
- },
- yAxis: {
- type: 'value',
- minInterval: 1,
- name: '头',
- nameTextStyle: { color: '#9fb0c3', fontSize: 10, padding: [0, 28, 0, 0] },
- axisLabel: AXIS_LABEL,
- axisLine: { show: false },
- axisTick: { show: false },
- splitLine: {
- lineStyle: { color: 'rgba(120, 140, 160, 0.28)', type: 'dashed' }
- }
- },
- series: [
- {
- name: '自食',
- type: 'bar',
- barWidth: '28%',
- itemStyle: {
- color: SELF_CONSUMPTION_BAR_GRADIENT,
- borderRadius: [10, 10, 0, 0]
- },
- label: {
- show: true,
- position: 'top',
- color: '#c8e6d8',
- fontSize: 10,
- formatter: '{c}'
- },
- data: values
- }
- ]
- }
- }
- /** 农牧户分类柱图:上金下青渐变,顶部圆角(区别于自食绿蓝) */
- const FARMER_CLASS_BAR_GRADIENT = {
- type: 'linear',
- x: 0,
- y: 0,
- x2: 0,
- y2: 1,
- colorStops: [
- { offset: 0, color: '#e0b84a' },
- { offset: 0.45, color: '#3aa8a0' },
- { offset: 1, color: '#1e6a9e' }
- ]
- }
- /**
- * 农牧户分类(柱状图):farmerHouseholdClassification[]
- * 项含 itemCode / itemLabel / count,单位:个
- */
- export function buildFarmerHouseholdClassificationOption(structure) {
- if (!structure?.length) {
- return emptyOption()
- }
- const categories = structure.map((item) => item.itemLabel || item.itemCode || '')
- const values = structure.map((item) => pickNumber(item, ['count']))
- if (!values.some((v) => v > 0)) {
- return emptyOption()
- }
- return {
- tooltip: {
- trigger: 'axis',
- axisPointer: { type: 'shadow' },
- formatter: (params) => {
- const p = Array.isArray(params) ? params[0] : params
- if (!p) {
- return ''
- }
- return `${p.name}<br/>${p.value} 个`
- }
- },
- grid: { left: 28, right: 8, top: 28, bottom: 8, containLabel: true },
- xAxis: {
- type: 'category',
- data: categories,
- axisLabel: {
- color: '#9fb0c3',
- fontSize: 9,
- interval: 0,
- hideOverlap: false
- },
- axisLine: { lineStyle: { color: 'rgba(150, 170, 190, 0.45)' } },
- axisTick: { show: false }
- },
- yAxis: {
- type: 'value',
- minInterval: 1,
- name: '个',
- nameTextStyle: { color: '#9fb0c3', fontSize: 10, padding: [0, 28, 0, 0] },
- axisLabel: AXIS_LABEL,
- axisLine: { show: false },
- axisTick: { show: false },
- splitLine: {
- lineStyle: { color: 'rgba(120, 140, 160, 0.28)', type: 'dashed' }
- }
- },
- series: [
- {
- name: '补助奖励资金',
- type: 'bar',
- barWidth: '36%',
- itemStyle: {
- color: FARMER_CLASS_BAR_GRADIENT,
- borderRadius: [10, 10, 0, 0]
- },
- label: {
- show: true,
- position: 'top',
- color: '#d8e8f0',
- fontSize: 9,
- formatter: '{c}'
- },
- data: values
- }
- ]
- }
- }
- /** 企业财务柱图配色(偏暗,白字可读) */
- const ENTERPRISE_FINANCE_BAR_COLOR = {
- INCOME: '#2a9d7a',
- EXPENSE: '#3d7ab8',
- PROFIT: '#b8923a',
- SUBSIDY: '#2f5fad'
- }
- /**
- * 企业年度收入与补助(柱状图):familyRanchFinance / largeLivestockFarmerFinance
- * items[] 含 itemCode / itemLabel / amountWan,单位:万元
- * @param {object|null} finance
- * @param {string} axisName 横轴下方名称(家庭牧场 / 养殖大户)
- */
- export function buildEnterpriseFinanceOption(finance, axisName) {
- const items = finance?.items
- if (!items?.length) {
- return emptyOption()
- }
- const categories = items.map((item) => item.itemLabel || item.itemCode || '')
- const data = items.map((item, i) => {
- const code = item.itemCode || ''
- return {
- value: pickNumber(item, ['amountWan', 'amount_wan', 'amount']),
- itemStyle: {
- color:
- ENTERPRISE_FINANCE_BAR_COLOR[code] ||
- Object.values(ENTERPRISE_FINANCE_BAR_COLOR)[i % 4],
- borderRadius: [6, 6, 0, 0]
- }
- }
- })
- return {
- title: {
- text: axisName || '',
- left: 'center',
- top: 0,
- textStyle: { color: '#89D9AD', fontSize: 11, fontWeight: 600 }
- },
- tooltip: {
- trigger: 'axis',
- axisPointer: { type: 'shadow' },
- formatter: (params) => {
- const p = Array.isArray(params) ? params[0] : params
- if (!p) {
- return ''
- }
- const prefix = axisName ? `${axisName}<br/>` : ''
- return `${prefix}${p.name}:${p.value} 万元`
- }
- },
- grid: { left: 8, right: 8, top: 28, bottom: 8, containLabel: true },
- xAxis: {
- type: 'category',
- data: categories,
- axisLabel: {
- color: '#9fb0c3',
- fontSize: 9,
- interval: 0,
- hideOverlap: false
- },
- axisLine: { lineStyle: { color: 'rgba(150, 170, 190, 0.45)' } },
- axisTick: { show: false }
- },
- yAxis: {
- type: 'value',
- name: '万元',
- nameTextStyle: { color: '#9fb0c3', fontSize: 9, padding: [0, 0, 0, 0] },
- axisLabel: { color: '#9fb0c3', fontSize: 9 },
- axisLine: { show: false },
- axisTick: { show: false },
- splitLine: {
- lineStyle: { color: 'rgba(120, 140, 160, 0.28)', type: 'dashed' }
- }
- },
- series: [
- {
- name: axisName || '金额',
- type: 'bar',
- barWidth: '36%',
- data
- }
- ]
- }
- }
- /** 合作社经营模式配色(偏暗) */
- const COOP_MODEL_COLOR = {
- WHOLE_VILLAGE: '#2a9d7a',
- SHARE: '#3d7ab8',
- JOINT: '#b8923a',
- MULTI_BUSINESS: '#2f5fad',
- COMPANY_TYPE: '#5a8a4a',
- OTHER: '#6a7a8a'
- }
- /**
- * 合作社经营模式分类(饼图):cooperativeBusinessModel[]
- * 项含 itemCode / itemLabel / count / ratio,单位:个
- */
- export function buildCooperativeBusinessModelOption(structure) {
- if (!structure?.length) {
- return emptyOption()
- }
- const hasData = structure.some((item) => pickNumber(item, ['count']) > 0)
- if (!hasData) {
- return emptyOption()
- }
- const data = structure.map((item, i) => {
- const code = item.itemCode || ''
- return {
- name: item.itemLabel || code || '未知',
- value: pickNumber(item, ['count']),
- ratio: item.ratio,
- itemStyle: {
- color: COOP_MODEL_COLOR[code] || Object.values(COOP_MODEL_COLOR)[i % 6],
- shadowBlur: 8,
- shadowColor: 'rgba(0, 0, 0, 0.4)',
- shadowOffsetY: 2
- }
- }
- })
- return {
- color: Object.values(COOP_MODEL_COLOR),
- tooltip: {
- trigger: 'item',
- formatter: (p) => {
- const row = structure[p.dataIndex]
- const ratio =
- row?.ratio != null ? `${Number(row.ratio).toFixed(1)}%` : `${p.percent?.toFixed(1)}%`
- return `${p.name}<br/>${p.value} 个 (${ratio})`
- }
- },
- legend: {
- orient: 'vertical',
- right: '1%',
- top: 'middle',
- icon: 'circle',
- itemWidth: 8,
- itemHeight: 8,
- itemGap: 6,
- textStyle: { color: '#e8eef5', fontSize: 9 }
- },
- series: [
- {
- type: 'pie',
- radius: '64%',
- center: ['32%', '52%'],
- avoidLabelOverlap: true,
- itemStyle: {
- borderColor: 'rgba(2, 22, 18, 0.9)',
- borderWidth: 2
- },
- label: {
- show: true,
- position: 'inside',
- color: '#fff',
- fontSize: 9,
- fontWeight: 'bold',
- formatter: (p) => {
- if (p.data.ratio != null && p.data.ratio !== '') {
- return `${Number(p.data.ratio).toFixed(0)}%`
- }
- return `${Math.round(p.percent)}%`
- }
- },
- labelLine: { show: false },
- emphasis: {
- scale: true,
- scaleSize: 4,
- itemStyle: { shadowBlur: 12, shadowColor: 'rgba(0, 0, 0, 0.5)' }
- },
- data
- }
- ]
- }
- }
- /** 草场退化五档配色 */
- const GRASSLAND_DEGRADATION_COLOR = {
- 1: '#3b66d1',
- 2: '#5dc3a1',
- 3: '#547233',
- 4: '#f7b538',
- 5: '#3e5388'
- }
- /** 畜群存栏结构物种配色 */
- const HERD_SPECIES_COLOR = {
- YAK: '#5ef0c8',
- DAIRY: '#ecd27b',
- SHEEP: '#3b66d1'
- }
- function formatGrasslandRatio(ratio, percent) {
- if (ratio != null && ratio !== '') {
- return `${Number(ratio).toFixed(0)}%`
- }
- return `${Math.round(percent)}%`
- }
- /**
- * 畜群存栏结构(环形饼图):herdInventoryStructure[]
- * 项含 speciesCode / speciesLabel / count / ratio
- */
- export function buildHerdInventoryStructureOption(structure) {
- if (!structure?.length) {
- return emptyOption()
- }
- const hasData = structure.some(
- (item) => pickNumber(item, ['count']) > 0 || pickNumber(item, ['ratio']) > 0
- )
- if (!hasData) {
- return emptyOption()
- }
- const data = structure.map((item) => {
- const code = item.speciesCode || ''
- return {
- name: item.speciesLabel || code || '未知',
- value: pickNumber(item, ['count']),
- ratio: item.ratio,
- itemStyle: {
- color: HERD_SPECIES_COLOR[code] || '#45f0b8',
- shadowBlur: 10,
- shadowColor: 'rgba(0, 0, 0, 0.5)',
- shadowOffsetY: 3
- }
- }
- })
- return {
- color: Object.values(HERD_SPECIES_COLOR),
- tooltip: {
- trigger: 'item',
- formatter: (p) => {
- const row = structure[p.dataIndex]
- const ratio =
- row?.ratio != null ? `${Number(row.ratio).toFixed(1)}%` : `${p.percent?.toFixed(1)}%`
- return `${p.name}<br/>${p.value}头 (${ratio})`
- }
- },
- legend: {
- orient: 'vertical',
- right: '2%',
- top: 'middle',
- icon: 'circle',
- itemWidth: 8,
- itemHeight: 8,
- itemGap: 10,
- textStyle: { color: '#e8eef5', fontSize: 10 }
- },
- series: [
- {
- type: 'pie',
- radius: ['42%', '68%'],
- center: ['34%', '54%'],
- avoidLabelOverlap: true,
- itemStyle: {
- borderColor: 'rgba(2, 22, 18, 0.9)',
- borderWidth: 2
- },
- label: {
- show: true,
- position: 'inside',
- color: '#fff',
- fontSize: 10,
- fontWeight: 'bold',
- formatter: (p) => formatGrasslandRatio(p.data.ratio, p.percent)
- },
- labelLine: { show: false },
- emphasis: {
- scale: true,
- scaleSize: 5,
- itemStyle: { shadowBlur: 14, shadowColor: 'rgba(0, 0, 0, 0.55)' }
- },
- data
- }
- ]
- }
- }
- /** 草场退化占比(环形图 + 右侧图例 + 扇区内占比) */
- export function buildGrasslandOption(grassland) {
- const degradationStats = grassland?.degradationStats
- if (!degradationStats?.length) {
- return emptyOption()
- }
- const hasData = degradationStats.some(
- (item) => pickNumber(item, ['count']) > 0 || pickNumber(item, ['ratio']) > 0
- )
- if (!hasData) {
- return emptyOption()
- }
- const data = degradationStats.map((item) => ({
- name: item.degradationLabel || `等级${item.degradationLevel}`,
- value: pickNumber(item, ['count']),
- ratio: item.ratio,
- itemStyle: {
- color: GRASSLAND_DEGRADATION_COLOR[item.degradationLevel] || '#45f0b8',
- shadowBlur: 10,
- shadowColor: 'rgba(0, 0, 0, 0.5)',
- shadowOffsetY: 3
- }
- }))
- return {
- color: Object.values(GRASSLAND_DEGRADATION_COLOR),
- tooltip: {
- trigger: 'item',
- formatter: (p) => {
- const row = degradationStats[p.dataIndex]
- const ratio =
- row?.ratio != null ? `${Number(row.ratio).toFixed(1)}%` : `${p.percent?.toFixed(1)}%`
- return `${p.name}<br/>${p.value}个 (${ratio})`
- }
- },
- legend: {
- orient: 'vertical',
- right: '2%',
- top: 'middle',
- icon: 'circle',
- itemWidth: 8,
- itemHeight: 8,
- itemGap: 10,
- textStyle: { color: '#e8eef5', fontSize: 10 }
- },
- series: [
- {
- type: 'pie',
- radius: ['42%', '68%'],
- center: ['34%', '54%'],
- avoidLabelOverlap: true,
- itemStyle: {
- borderColor: 'rgba(2, 22, 18, 0.9)',
- borderWidth: 2
- },
- label: {
- show: true,
- position: 'inside',
- color: '#fff',
- fontSize: 10,
- fontWeight: 'bold',
- formatter: (p) => formatGrasslandRatio(p.data.ratio, p.percent)
- },
- labelLine: { show: false },
- emphasis: {
- scale: true,
- scaleSize: 5,
- itemStyle: { shadowBlur: 14, shadowColor: 'rgba(0, 0, 0, 0.55)' }
- },
- data
- }
- ]
- }
- }
- const APPOINTMENT_ORG_GRADIENT = {
- type: 'linear',
- x: 0,
- y: 1,
- x2: 0,
- y2: 0,
- colorStops: [
- { offset: 0, color: '#1e479b' },
- { offset: 1, color: '#3a6bc4' }
- ]
- }
- const APPOINTMENT_EXPERT_GRADIENT = {
- type: 'linear',
- x: 0,
- y: 1,
- x2: 0,
- y2: 0,
- colorStops: [
- { offset: 0, color: '#8e763d' },
- { offset: 1, color: '#d4a84a' }
- ]
- }
- const APPOINTMENT_GRID = { left: 44, right: 8, top: 26, bottom: 6, containLabel: true }
- const APPOINTMENT_SPLIT_LINE = {
- lineStyle: { color: 'rgba(120, 140, 160, 0.28)', type: 'dashed' }
- }
- /** 线下服务预约堆叠柱(滚动 12 月) */
- export function buildAppointmentStackedOption(monthlyStacked) {
- if (!monthlyStacked?.length) {
- return emptyOption()
- }
- const axis = buildRollingAxis(monthlyStacked)
- return {
- tooltip: {
- trigger: 'axis',
- axisPointer: { type: 'shadow' },
- formatter: (params) => {
- if (!Array.isArray(params) || !params.length) {
- return ''
- }
- const title = axis.titleAt(params[0].dataIndex)
- const lines = params.map((p) => `${p.marker}${p.seriesName} ${p.value} 人次`)
- return `${title}<br/>${lines.join('<br/>')}`
- }
- },
- legend: {
- top: 4,
- right: 8,
- itemWidth: 10,
- itemHeight: 8,
- textStyle: { color: '#e8eef5', fontSize: 10 },
- data: ['诊疗机构', '兽医', '专家']
- },
- grid: APPOINTMENT_GRID,
- xAxis: {
- type: 'category',
- data: axis.categories,
- axisLabel: { ...MONTH_AXIS_LABEL, interval: 0 },
- axisLine: { lineStyle: { color: 'rgba(150, 170, 190, 0.45)' } },
- axisTick: { show: false }
- },
- yAxis: {
- type: 'value',
- minInterval: 1,
- axisLabel: AXIS_LABEL,
- axisLine: { show: false },
- axisTick: { show: false },
- splitLine: APPOINTMENT_SPLIT_LINE
- },
- series: [
- {
- name: '诊疗机构',
- type: 'bar',
- stack: 'apt',
- barMaxWidth: 10,
- itemStyle: { color: APPOINTMENT_ORG_GRADIENT },
- data: rollingSeries(monthlyStacked, 'orgCount', ['org_count'])
- },
- {
- name: '兽医',
- type: 'bar',
- stack: 'apt',
- barMaxWidth: 10,
- itemStyle: { color: '#52b89f' },
- data: rollingSeries(monthlyStacked, 'vetCount', ['vet_count'])
- },
- {
- name: '专家',
- type: 'bar',
- stack: 'apt',
- barMaxWidth: 10,
- itemStyle: { color: APPOINTMENT_EXPERT_GRADIENT },
- data: rollingSeries(monthlyStacked, 'expertCount', ['expert_count'])
- }
- ]
- }
- }
- /** 三类柱图配色 */
- const STACK_BAR_COLORS = [APPOINTMENT_ORG_GRADIENT, '#52b89f', APPOINTMENT_EXPERT_GRADIENT]
- /** 惠农补贴(万元) */
- export function buildSubsidyOption(typeStats, hasSubsidyData) {
- if (!hasSubsidyData || !typeStats?.length) {
- return emptyOption('暂无补贴数据')
- }
- const categories = typeStats.map(
- (item) => item.subsidyTypeName || `类型${item.subsidyType}`
- )
- return {
- tooltip: {
- trigger: 'axis',
- axisPointer: { type: 'shadow' },
- formatter: (params) => {
- const p = Array.isArray(params) ? params[0] : params
- if (!p) {
- return ''
- }
- const val = p.value != null ? Number(p.value) : 0
- return `${p.name}<br/>补贴金额 ${val.toLocaleString('zh-CN', {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2
- })} 万元`
- }
- },
- grid: GRID,
- xAxis: {
- type: 'category',
- data: categories,
- axisLabel: { ...AXIS_LABEL, interval: 0, fontSize: 9, margin: 6 },
- axisLine: AXIS_LINE,
- axisTick: { show: false }
- },
- yAxis: {
- type: 'value',
- name: '万元',
- nameTextStyle: { color: '#6a9f90', fontSize: 10 },
- axisLabel: AXIS_LABEL,
- axisLine: AXIS_LINE,
- splitLine: SPLIT_LINE
- },
- series: [
- {
- name: '补贴金额',
- type: 'bar',
- barMaxWidth: 28,
- data: typeStats.map((item, index) => ({
- value: Number(item.amountWan ?? 0),
- itemStyle: { color: STACK_BAR_COLORS[index % STACK_BAR_COLORS.length] }
- }))
- }
- ]
- }
- }
- const TRAINING_ACTIVITY_LINE = '#ff9f43'
- const TRAINING_ACTIVITY_AREA = {
- type: 'linear',
- x: 0,
- y: 0,
- x2: 0,
- y2: 1,
- colorStops: [
- { offset: 0, color: 'rgba(255, 159, 67, 0.55)' },
- { offset: 1, color: 'rgba(255, 159, 67, 0.04)' }
- ]
- }
- const TRAINING_ENROLL_LINE = '#40e0d0'
- const TRAINING_ENROLL_AREA = {
- type: 'linear',
- x: 0,
- y: 0,
- x2: 0,
- y2: 1,
- colorStops: [
- { offset: 0, color: 'rgba(64, 224, 208, 0.5)' },
- { offset: 1, color: 'rgba(64, 224, 208, 0.04)' }
- ]
- }
- const TRAINING_GRID = { left: 36, right: 36, top: 30, bottom: 6, containLabel: true }
- const TRAINING_SPLIT_LINE = {
- lineStyle: { color: 'rgba(120, 140, 160, 0.28)', type: 'dashed' }
- }
- /**
- * 人均收入(双折线):perCapitaIncomeTrend[]
- * 项含 statYear / ruralDisposableIncome / povertyNetIncome,单位:元
- */
- export function buildPerCapitaIncomeTrendOption(trend) {
- if (!trend?.length) {
- return emptyOption()
- }
- const years = trend.map((item) => String(item.statYear ?? ''))
- const rural = trend.map((item) =>
- pickNumber(item, ['ruralDisposableIncome', 'rural_disposable_income'])
- )
- const poverty = trend.map((item) =>
- pickNumber(item, ['povertyNetIncome', 'poverty_net_income'])
- )
- if (!rural.some((v) => v > 0) && !poverty.some((v) => v > 0)) {
- return emptyOption()
- }
- return {
- color: ['#3ecf9a', '#e0b84a'],
- tooltip: {
- trigger: 'axis',
- formatter: (params) => {
- if (!Array.isArray(params) || !params.length) {
- return ''
- }
- const title = `${params[0].axisValue}年`
- const lines = params.map((p) => `${p.marker}${p.seriesName} ${p.value ?? 0} 元`)
- return `${title}<br/>${lines.join('<br/>')}`
- }
- },
- legend: {
- top: 2,
- left: 'center',
- itemWidth: 14,
- itemHeight: 4,
- textStyle: { color: '#e8eef5', fontSize: 10 }
- },
- grid: { left: 12, right: 12, top: 32, bottom: 6, containLabel: true },
- xAxis: {
- type: 'category',
- data: years,
- boundaryGap: false,
- axisLabel: { color: '#9fb0c3', fontSize: 10 },
- axisLine: { lineStyle: { color: 'rgba(150, 170, 190, 0.45)' } },
- axisTick: { show: false }
- },
- yAxis: {
- type: 'value',
- name: '元',
- nameTextStyle: { color: '#9fb0c3', fontSize: 10 },
- axisLabel: AXIS_LABEL,
- axisLine: { show: false },
- axisTick: { show: false },
- splitLine: TRAINING_SPLIT_LINE
- },
- series: [
- {
- name: '农村居民人均可支配收入',
- type: 'line',
- smooth: true,
- symbol: 'circle',
- symbolSize: 5,
- lineStyle: { width: 2, color: '#3ecf9a' },
- itemStyle: { color: '#3ecf9a' },
- data: rural
- },
- {
- name: '脱贫人口人均纯收入',
- type: 'line',
- smooth: true,
- symbol: 'circle',
- symbolSize: 5,
- lineStyle: { width: 2, color: '#e0b84a' },
- itemStyle: { color: '#e0b84a' },
- data: poverty
- }
- ]
- }
- }
- /** 实战培训报名趋势(双面积折线,滚动 12 月) */
- export function buildTrainingOption(trainingTrend) {
- if (!trainingTrend?.hasTrainingData) {
- return emptyOption('暂无培训数据')
- }
- const monthly = trainingTrend.monthly || []
- if (!monthly.length) {
- return emptyOption('暂无培训数据')
- }
- const axis = buildRollingAxis(monthly)
- return {
- tooltip: {
- trigger: 'axis',
- formatter: (params) => {
- if (!Array.isArray(params) || !params.length) {
- return ''
- }
- const title = axis.titleAt(params[0].dataIndex)
- const lines = params.map((p) => {
- const unit = p.seriesName === '活动数' ? '个' : '人'
- return `${p.marker}${p.seriesName} ${p.value ?? 0} ${unit}`
- })
- return `${title}<br/>${lines.join('<br/>')}`
- }
- },
- legend: {
- top: 4,
- left: 'center',
- itemWidth: 14,
- itemHeight: 4,
- textStyle: { color: '#e8eef5', fontSize: 10 }
- },
- grid: TRAINING_GRID,
- xAxis: {
- type: 'category',
- data: axis.categories,
- axisLabel: { ...MONTH_AXIS_LABEL, interval: 0, color: '#e8eef5' },
- axisLine: { lineStyle: { color: 'rgba(150, 170, 190, 0.45)' } },
- axisTick: { show: false }
- },
- yAxis: [
- {
- type: 'value',
- name: '个',
- nameTextStyle: { color: '#9fb0c3', fontSize: 10 },
- minInterval: 1,
- axisLabel: { ...AXIS_LABEL, color: '#9fb0c3' },
- axisLine: { show: false },
- axisTick: { show: false },
- splitLine: TRAINING_SPLIT_LINE
- },
- {
- type: 'value',
- name: '人',
- nameTextStyle: { color: '#9fb0c3', fontSize: 10 },
- minInterval: 1,
- axisLabel: { ...AXIS_LABEL, color: '#9fb0c3' },
- axisLine: { show: false },
- axisTick: { show: false },
- splitLine: { show: false }
- }
- ],
- series: [
- {
- name: '活动数',
- type: 'line',
- smooth: true,
- symbol: 'none',
- yAxisIndex: 0,
- lineStyle: {
- color: TRAINING_ACTIVITY_LINE,
- width: 2,
- shadowColor: 'rgba(255, 159, 67, 0.65)',
- shadowBlur: 10
- },
- areaStyle: { color: TRAINING_ACTIVITY_AREA },
- data: rollingSeries(monthly, 'activityCount', ['activity_count'])
- },
- {
- name: '报名人数',
- type: 'line',
- yAxisIndex: 1,
- smooth: true,
- symbol: 'none',
- lineStyle: {
- color: TRAINING_ENROLL_LINE,
- width: 2,
- shadowColor: 'rgba(64, 224, 208, 0.65)',
- shadowBlur: 10
- },
- areaStyle: { color: TRAINING_ENROLL_AREA },
- data: rollingSeries(monthly, 'enrolledCount', ['enrolled_count'])
- }
- ]
- }
- }
|