| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- <template>
- <div class="middle-wrapper">
- <div class="building-chart">
- <SectionHeader
- layout="spread"
- title="各集团建筑面积排名(万m²)"
- />
- <div class="building-bar-chart">
- <BarChart
- clickable
- :style="isSmallScreen ? smallMainBarStyle : 'width: 100%; height: 100%; min-height: 320px'"
- :xAxisData="xAxisDataList"
- :seriesData="seriesDataList"
- @bar-click="onMainBarClick"
- />
- </div>
- </div>
- <div class="middle-group">
- <div class="middle-group-top">
- <div v-for="item in pieChartList" :key="item.name" class="chart-card">
- <SectionHeader :title="item.text" />
- <BarChart
- v-if="isSmallScreen"
- :xAxisData="pieToBar(item.data).xAxisData"
- :seriesData="pieToBar(item.data).seriesData"
- :style="smallBarStyle"
- />
- <PieChart
- v-else
- :data="item.data"
- :legend-head-count="item.legendHeadCount"
- :legend-grid-no-padding-right="item.legendGridNoPaddingRight"
- value-unit="万m²"
- />
- </div>
- </div>
- <div class="middle-group-bottom">
- <TopicCard
- v-for="item in topicCardList"
- :key="item.name"
- :text="item.text"
- :introduction="item.introduction"
- :route="item.route"
- />
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import BarChart from '@/components/chart/BarChart.vue';
- import SectionHeader from '@/components/base/SectionHeader.vue';
- import TopicCard from '@/components/stats/TopicCard.vue';
- import PieChart from '@/components/chart/PieChart.vue';
- import { ref, onMounted, onUnmounted, computed } from 'vue';
- import { useRouter } from 'vue-router';
- import { useEnterpriseStore } from '@/store/enterprise';
- import { useFilterStore } from '@/store/filter';
- import { sumFieldToWan,toWanSquareMeters } from '@/utils/areaMath';
- const router = useRouter();
- const filterStore = useFilterStore();
- const enterpriseStore = useEnterpriseStore();
- const mq = window.matchMedia('(max-width: 1440px)');
- const isSmallScreen = ref(mq.matches);
- const smallBarStyle = 'width: 100%; height: 218px';
- const smallMainBarStyle = 'width: 100%; height: 393px';
- const onBreakpointChange = (e) => {
- isSmallScreen.value = e.matches;
- };
- const pieToBar = (pieData) => {
- const data = pieData ?? [];
- return {
- xAxisData: data.map((item) => item.name),
- seriesData: data.map((item) => item.value),
- };
- };
- /** 首页主柱图点击:按集团筛选并跳转专题一 */
- const onMainBarClick = (groupName) => {
- filterStore.setGroup(groupName);
- router.push('/topic1');
- };
- const filteredList = computed(() =>
- enterpriseStore.filteredByGroup(filterStore.selectGroup, filterStore.selectDate)
- );
- //柱状图数据
- const groupChartData = computed(() => {
- const list = enterpriseStore.filteredByGroup('全部', filterStore.selectDate);
- const groupMap = list.reduce((acc, item) => {
- const name = (item.c_ssqpjt ?? '其他').trim()
- acc[name] = (acc[name] ?? 0) + Number(item.c_sjjzmj || 0)
- return acc
- }, {})
- return Object.entries(groupMap).map(([name, val]) => ({
- name,
- value: toWanSquareMeters(val),
- }))
- })
- const xAxisDataList = computed(()=>{
- return groupChartData.value.map(item => item.name)
- })
- const seriesDataList = computed(()=>{
- return groupChartData.value.map(item => item.value)
- })
- //使用状态分布环形图数据
- const USAGE_PIE_CONFIG = [
- { name: '出租', field: 'c_czmj', color: 'rgba(1, 118, 255, 1)' },
- { name: '自用', field: 'c_zymj', color: 'rgba(255, 121, 98, 1)' },
- { name: '出借', field: 'c_cjmj', color: 'rgba(104, 221, 169, 1)' },
- { name: '闲置', field: 'c_xzmj', color: 'rgba(255, 232, 119, 1)' },
- ]
- const usageStatusList = computed(() =>
- USAGE_PIE_CONFIG.map(({ name, field, color }) => ({
- name,
- color,
- value: sumFieldToWan(filteredList.value, field),
- }))
- )
- //房龄分布环形图数据
- const ageDistributionList = computed(() => {
- const origin = filteredList.value || []
- const groupConfig = [
- {
- name: '0-10年',
- color: 'rgba(1, 118, 255, 1)',
- filter: val => Number(val) <= 10
- },
- {
- name: '10-20年',
- color: 'rgba(255, 121, 98, 1)',
- filter: val => Number(val) > 10 && Number(val) <= 20
- },
- {
- name: '20年以上',
- color: 'rgba(104, 221, 169, 1)',
- filter: val => (Number(val) > 20) || val === '30年以上'
- },
- {
- name: '不确定',
- color: 'rgba(255, 232, 119, 1)',
- filter: () => true
- }
- ]
- const result = groupConfig.map(item => ({
- name: item.name,
- value: 0,
- color: item.color
- }))
- origin.forEach(item => {
- const fieldVal = item.c_fl
- const target = result.find((_, idx) => groupConfig[idx].filter(fieldVal))
- if(target) {
- const area = Number(item.c_sjjzmj || 0)
- target.value += area
- }
- })
- return result
- .map(item => {
- const wan = Number((item.value / 10000).toFixed(2))
- return { ...item, value: wan }
- })
- .filter(item => item.value > 0)
- })
- //按用途分类建筑面积(万m²)
- const hashString = (str) => {
- let hash = 0
- for (let i = 0; i < str.length; i++) {
- hash = (hash * 31 + str.charCodeAt(i)) >>> 0
- }
- return hash
- }
- const getColorByName = (name) => {
- const hash = hashString(name)
- const GOLDEN_ANGLE = 137.508
- const hue = (hash * GOLDEN_ANGLE) % 360
- const saturation = 58 + (hash % 4) * 9
- const lightness = 44 + ((hash >> 3) % 5) * 5
- return `hsla(${Math.round(hue)}, ${saturation}%, ${lightness}%, 1)`
- }
- const PURPOSE_TOP_N = 5
- const EMPTY_LABEL = '未知'
- const OTHER_LABEL = '其他'
- const OTHER_COLOR = 'rgba(178, 190, 195, 1)'
- const purposeListFull = computed(() => {
- const purposeMap = (filteredList.value ?? []).reduce((acc, item) => {
- const name = (item.c_tdqk_yt1 ?? EMPTY_LABEL).trim() || EMPTY_LABEL;
- acc[name] = (acc[name] ?? 0) + Number(item.c_zdmj || 0);
- return acc;
- }, {});
- return Object.entries(purposeMap)
- .map(([name, sumSqm]) => ({
- name,
- value: toWanSquareMeters(sumSqm),
- color: getColorByName(name),
- }))
- .filter((item) => item.value > 0)
- .sort((a, b) => b.value - a.value);
- });
- const purposeListCompact = computed(() => {
- const all = purposeListFull.value;
- if (all.length <= PURPOSE_TOP_N) return all;
- const top = all.slice(0, PURPOSE_TOP_N);
- const restValue = all
- .slice(PURPOSE_TOP_N)
- .reduce((sum, item) => sum + item.value, 0);
- return [
- ...top,
- {
- name: OTHER_LABEL,
- value: Number(restValue.toFixed(2)),
- color: OTHER_COLOR,
- },
- ];
- });
- const pieChartList = computed(() => [
- {
- name: '图表1',
- text: '土地类型面积',
- data: isSmallScreen.value ? purposeListCompact.value : purposeListFull.value,
- legendHeadCount: isSmallScreen.value ? 0 : PURPOSE_TOP_N,
- },
- {
- name: '图表2',
- text: '房屋使用状态',
- data: usageStatusList.value,
- },
- {
- name: '图表3',
- text: '房龄分布',
- data: ageDistributionList.value,
- legendGridNoPaddingRight: true,
- },
- ]);
- const topicCardList = ref([
- { name: '专题1', text: '专题一·资产结构与分布', introduction: '用途、龄段、集团维度结构洞察', route: '/topic1' },
- { name: '专题2', text: '专题二·资产权属与合规', introduction: '办证进度、分线台账与集团对比' ,route: '/topic2'},
- { name: '专题3', text: '专题三·低效闲置资产盘活', introduction: '闲置率、闲置时长与盘活状态' ,route: '/topic3'},
- ]);
- onMounted(() => {
- mq.addEventListener('change', onBreakpointChange);
- });
- onUnmounted(() => {
- mq.removeEventListener('change', onBreakpointChange);
- });
- </script>
- <style scoped>
- .middle-wrapper {
- width: 100%;
- max-width: var(--content-max-width);
- display: grid;
- grid-template-columns: minmax(0, 708fr) minmax(0, 1150fr);
- gap: 14px;
- align-items: start;
- }
- .building-chart {
- width: 100%;
- min-width: 0;
- min-height: 479px;
- border-radius: var(--radius-card);
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- gap: 12px;
- padding: var(--card-padding);
- background: var(--color-surface);
- border: 1px solid var(--color-accent-border);
- }
- .building-bar-chart {
- width: 100%;
- flex: 1;
- min-height: 320px;
- display: flex;
- }
- .middle-group {
- width: 100%;
- min-width: 0;
- display: grid;
- grid-template-rows: auto auto;
- gap: 14px;
- align-content: start;
- }
- .middle-group-top {
- width: 100%;
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 16px;
- }
- .middle-group-bottom {
- width: 100%;
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 16px;
- }
- .chart-card {
- width: 100%;
- min-width: 0;
- border-radius: var(--radius-card);
- padding: var(--card-padding);
- display: flex;
- flex-direction: column;
- gap: 12px;
- background: var(--color-surface);
- border: 1px solid var(--color-accent-border);
- }
- .chart-card :deep(.pie-chart-main--index) {
- width: 100%;
- max-width: 100%;
- height: auto;
- padding: 0 4px;
- margin-top: 4px;
- box-sizing: border-box;
- }
- .chart-card :deep(.donut-chart) {
- width: clamp(90px, 32%, 118px);
- height: clamp(90px, 32%, 118px);
- margin-bottom: 16px;
- }
- .chart-card :deep(.legend-group--grid) {
- width: 100%;
- max-width: 100%;
- height: auto;
- column-gap: clamp(10px, 3.5%, 58px);
- padding: 0 4px;
- box-sizing: border-box;
- }
- .chart-card :deep(.legend-group--grid-no-pr) {
- column-gap: clamp(6px, 2.5%, 34px);
- padding: 0 4px;
- }
- .chart-card :deep(.legend-group--grid .legend-item),
- .chart-card :deep(.legend-group--grid-no-pr .legend-item) {
- width: 100%;
- max-width: none;
- min-width: 0;
- }
- .middle-group-bottom :deep(.topic-card) {
- width: 100%;
- min-width: 0;
- height: auto;
- min-height: 141px;
- }
- .middle-group-bottom :deep(.topic-card-title),
- .middle-group-bottom :deep(.topic-card-Introduction),
- .middle-group-bottom :deep(.topic-card-push) {
- width: 100%;
- max-width: none;
- }
- @media screen and (max-width: 1440px) {
- .middle-wrapper {
- grid-template-columns: 1fr;
- }
- .middle-group {
- display: contents;
- }
- .middle-group-top {
- order: 1;
- }
- .building-chart {
- order: 2;
- min-height: 479px;
- }
- .middle-group-bottom {
- order: 3;
- }
- .chart-card {
- min-height: 280px;
- }
- }
- </style>
|