| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617 |
- <template>
- <PageHeader />
- <div class="main">
- <TopicPageHeader :topic-id="1" />
- <QueryFilter />
- <div class="topic1-lower">
- <div class="topic1-charts">
- <div class="pie-chart-group">
- <div
- v-for="item in topPieChartsList"
- :key="item.id"
- class="pie-chart-card"
- :class="{
- 'pie-chart-card--usage': item.id === '2',
- 'pie-chart-card--building': item.id === '1',
- 'pie-chart-card--age': item.id === '4',
- }"
- >
- <SectionHeader :title="item.text" />
- <PieChart
- variant="topic1"
- clickable
- :data="item.data"
- :active-slice="item.activeSlice"
- :value-unit="item.valueUnit"
- @slice-click="item.onSliceClick"
- />
- </div>
- </div>
- <div class="bar-chart-group">
- <div class="land-purpose-pie">
- <SectionHeader title="土地类型面积" />
- <div class="chart-card__body chart-card__body--pie">
- <PieChart
- variant="topic1"
- size="lg"
- clickable
- :data="purposeList"
- :active-slice="listFilters.landUsage"
- value-unit="万m²"
- @slice-click="onLandPurposeSliceClick"
- />
- </div>
- </div>
- <div class="asset-structure">
- <SectionHeader title="各集团建筑面积与已使用面积对比" />
- <div class="chart-card__body">
- <GroupAreaCompareBarChart
- clickable
- :height="300"
- :active-group="activeGroupBar"
- @group-click="onGroupBarClick"
- />
- </div>
- </div>
- </div>
- </div>
- <FullDataList
- ref="fullListRef"
- :filters="listFilters"
- :rows="fullRows"
- :fw-usage-options="fwUsageFilterOptions"
- :land-type-options="landTypeFilterOptions"
- @update:filters="listFilters = $event"
- @reset="resetListFilters"
- @row-action="openAssetDetail"
- />
- </div>
- </div>
- </template>
- <script setup>
- import { showWarning } from '@/utils/message';
- import { ref, computed, watch } from 'vue';
- import { useRouter } from 'vue-router';
- import PageHeader from '@/components/layout/PageHeader.vue';
- import TopicPageHeader from '@/components/stats/TopicPageHeader.vue';
- import QueryFilter from '@/components/stats/QueryFilter.vue';
- import SectionHeader from '@/components/base/SectionHeader.vue';
- import PieChart from '@/components/chart/PieChart.vue';
- import GroupAreaCompareBarChart from '@/components/chart/GroupAreaCompareBarChart.vue';
- import FullDataList from '@/components/stats/FullDataList.vue';
- import { useEnterpriseStore } from '@/store/enterprise';
- import { useFilterStore } from '@/store/filter';
- import { toWanSquareMeters, sumFieldToWan } from '@/utils/areaMath';
- import {
- buildLandTypeFilterOptions,
- buildFwUsageFilterOptions,
- filterTopic1Assets,
- mapTopic1Row,
- } from '@/utils/topic1AssetList';
- import { GROUP_DISPLAY_NAMES } from '@/utils/aggregateReportRecords';
- const enterpriseStore = useEnterpriseStore();
- const filterStore = useFilterStore();
- const router = useRouter();
- const fullListRef = ref(null);
- const DISPLAY_TO_GROUP = Object.fromEntries(
- Object.entries(GROUP_DISPLAY_NAMES).map(([group, short]) => [short, group])
- );
- const AGE_SLICE_TO_KEY = {
- '0-10年': '0-10',
- '10-20年': '10-20',
- '20年以上': '20+',
- };
- 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 createDefaultFilters = () => ({
- keyword: '',
- purpose: '全部',
- group: filterStore.selectGroup,
- age: '全部',
- util: '全部',
- fwUsage: '',
- landType: '',
- landUsage: '',
- });
- const listFilters = ref(createDefaultFilters());
- const filteredList = computed(() =>
- enterpriseStore.filteredByGroup(filterStore.selectGroup, filterStore.selectDate)
- );
- const OTHER_LABEL = '其他';
- const EMPTY_LABEL = '未知';
- 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 hue = (hash * 137.508) % 360;
- const saturation = 58 + (hash % 4) * 9;
- const lightness = 44 + ((hash >> 3) % 5) * 5;
- return `hsla(${Math.round(hue)}, ${saturation}%, ${lightness}%, 1)`;
- };
- const buildAllSlices = (entries, toValue = (v) => v) =>
- entries
- .map(([name, raw]) => ({
- name,
- value: toValue(raw),
- color: getColorByName(name),
- }))
- .filter((item) => item.value > 0)
- .sort((a, b) => b.value - a.value);
- const buildingAreaChart = computed(() => {
- const purposeMap = (filteredList.value ?? []).reduce((acc, item) => {
- const name = (item.c_fwyt_yt1 ?? OTHER_LABEL).trim() || OTHER_LABEL;
- acc[name] = (acc[name] ?? 0) + Number(item.c_sjjzmj || 0);
- return acc;
- }, {});
- const slices = buildAllSlices(Object.entries(purposeMap), (sumSqm) =>
- toWanSquareMeters(sumSqm)
- );
- return { slices };
- });
- const buildingAreaStructureList = computed(() => buildingAreaChart.value.slices);
- const usageStatusList = computed(() =>
- USAGE_PIE_CONFIG.map(({ name, field, color }) => ({
- name,
- color,
- value: sumFieldToWan(filteredList.value, field),
- }))
- );
- const landPurposeChart = 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;
- }, {});
- const slices = buildAllSlices(Object.entries(purposeMap), (sumSqm) =>
- toWanSquareMeters(sumSqm)
- );
- return { slices };
- });
- const purposeList = computed(() => landPurposeChart.value.slices);
- const landTypeFilterOptions = computed(() =>
- buildLandTypeFilterOptions(filteredList.value)
- );
- const fwUsageFilterOptions = computed(() =>
- buildFwUsageFilterOptions(filteredList.value)
- );
- 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) {
- target.value += Number(item.c_sjjzmj || 0);
- }
- });
- return result
- .map((item) => ({
- ...item,
- value: toWanSquareMeters(item.value),
- }))
- .filter((item) => item.value > 0);
- });
- const listFilterContext = computed(() => ({
- ...listFilters.value,
- }));
- const fullRows = computed(() =>
- filterTopic1Assets(filteredList.value, listFilterContext.value).map(mapTopic1Row)
- );
- const scrollToFullList = () => {
- fullListRef.value?.scrollIntoView();
- };
- const applyListFilters = (patch) => {
- listFilters.value = { ...listFilters.value, ...patch };
- scrollToFullList();
- };
- const resetListFilters = () => {
- listFilters.value = createDefaultFilters();
- };
- watch(() => filterStore.selectGroup, resetListFilters);
- watch(
- () => listFilters.value.group,
- (group) => {
- if (filterStore.selectGroup !== group) {
- filterStore.setGroup(group);
- }
- }
- );
- const onBuildingSliceClick = (name) => {
- const next = listFilters.value.fwUsage === name ? '' : name;
- applyListFilters({
- ...createDefaultFilters(),
- fwUsage: next,
- });
- };
- const onUsageSliceClick = (name) => {
- const next = listFilters.value.util === name ? '全部' : name;
- applyListFilters({
- ...createDefaultFilters(),
- util: next,
- });
- };
- const onLandPurposeSliceClick = (name) => {
- const next = listFilters.value.landUsage === name ? '' : name;
- applyListFilters({
- ...createDefaultFilters(),
- landUsage: next,
- });
- };
- const onAgeSliceClick = (name) => {
- const key = AGE_SLICE_TO_KEY[name];
- if (!key) return;
- const next = listFilters.value.age === key ? '全部' : key;
- applyListFilters({
- ...createDefaultFilters(),
- age: next,
- });
- };
- const onGroupBarClick = (shortName) => {
- const group = DISPLAY_TO_GROUP[shortName] || shortName;
- const next = listFilters.value.group === group ? '全部' : group;
- applyListFilters({
- ...createDefaultFilters(),
- group: next,
- });
- };
- const openAssetDetail = (row) => {
- const id = row.id ?? row.code;
- if (!id || id === '—') {
- showWarning('无法打开详情:缺少资产标识');
- return;
- }
- router.push({
- path: `/manage/asset/${encodeURIComponent(String(id))}`,
- query: { from: 'stats' },
- });
- };
- const activeGroupBar = computed(() =>
- listFilters.value.group === '全部'
- ? ''
- : (GROUP_DISPLAY_NAMES[listFilters.value.group] ?? listFilters.value.group)
- );
- const activeAgeSlice = computed(() => {
- if (listFilters.value.age === '全部') return '';
- const map = { '0-10': '0-10年', '10-20': '10-20年', '20+': '20年以上' };
- return map[listFilters.value.age] ?? '';
- });
- const activeUsageSlice = computed(() =>
- listFilters.value.util === '全部' ? '' : listFilters.value.util
- );
- const topPieChartsList = computed(() => [
- {
- id: '2',
- text: '房屋使用状态',
- data: usageStatusList.value,
- activeSlice: activeUsageSlice.value,
- onSliceClick: onUsageSliceClick,
- valueUnit: '万m²',
- },
- {
- id: '1',
- text: '房屋用途',
- data: buildingAreaStructureList.value,
- activeSlice: listFilters.value.fwUsage,
- onSliceClick: onBuildingSliceClick,
- valueUnit: '万m²',
- },
- {
- id: '4',
- text: '房龄分布',
- data: ageDistributionList.value,
- activeSlice: activeAgeSlice.value,
- onSliceClick: onAgeSliceClick,
- valueUnit: '万m²',
- },
- ]);
- </script>
- <style scoped>
- .main {
- width: 100%;
- max-width: var(--page-max-width);
- padding: 24px;
- padding-bottom: 32px;
- display: flex;
- flex-direction: column;
- gap: 12px;
- position: relative;
- }
- .pie-chart-group {
- width: 100%;
- max-width: var(--content-max-width);
- min-height: 280px;
- display: flex;
- align-items: stretch;
- gap: 14px;
- }
- .pie-chart-card {
- flex: 1;
- min-width: 0;
- min-height: 280px;
- display: flex;
- flex-direction: column;
- gap: 24px;
- border-radius: var(--radius-card);
- padding: var(--card-padding);
- background: var(--color-surface);
- border: 1px solid var(--color-accent-border);
- }
- .pie-chart-card :deep(.pie-chart-main--topic1) {
- margin-top: 4px;
- width: 100%;
- justify-content: center;
- }
- .topic1-lower {
- width: 100%;
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
- .topic1-charts {
- width: 100%;
- display: flex;
- flex-direction: column;
- gap: 14px;
- }
- .bar-chart-group {
- width: 100%;
- display: flex;
- gap: 14px;
- align-items: stretch;
- }
- .land-purpose-pie {
- width: 613px;
- display: flex;
- flex-direction: column;
- gap: 8px;
- border-radius: var(--radius-card);
- padding: var(--card-padding);
- background: var(--color-surface);
- border: 1px solid var(--color-accent-border);
- }
- .land-purpose-pie :deep(.pie-chart-main--topic1-lg) {
- width: 100%;
- justify-content: center;
- }
- .asset-structure {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 8px;
- border-radius: var(--radius-card);
- padding: var(--card-padding);
- background: var(--color-surface);
- border: 1px solid var(--color-accent-border);
- }
- .chart-card__body {
- flex: 1;
- height: 300px;
- min-height: 300px;
- }
- .chart-card__body--pie {
- display: flex;
- align-items: center;
- justify-content: center;
- }
- @media screen and (max-width: 1440px) {
- .main {
- width: 100%;
- max-width: 1440px;
- }
- .pie-chart-group,
- .bar-chart-group,
- .topic1-lower,
- .topic1-charts {
- width: 100%;
- }
- .topic1-charts {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- grid-template-areas:
- 'usage building'
- 'age land'
- 'bar bar';
- gap: 14px;
- align-items: stretch;
- }
- .pie-chart-group,
- .bar-chart-group {
- display: contents;
- }
- .pie-chart-card--usage {
- grid-area: usage;
- }
- .pie-chart-card--building {
- grid-area: building;
- }
- .pie-chart-card--age {
- grid-area: age;
- }
- .land-purpose-pie {
- grid-area: land;
- width: 100%;
- min-width: 0;
- }
- .land-purpose-pie .chart-card__body--pie {
- height: auto;
- min-height: 240px;
- }
- .asset-structure {
- grid-area: bar;
- width: 100%;
- }
- .pie-chart-card,
- .land-purpose-pie {
- overflow: visible;
- min-width: 0;
- }
- .topic1-charts .pie-chart-card :deep(.pie-chart-main--topic1),
- .land-purpose-pie :deep(.pie-chart-main--topic1-lg) {
- flex-wrap: wrap;
- justify-content: center;
- align-items: flex-start;
- height: auto;
- min-height: 196px;
- max-width: 100%;
- gap: 12px;
- }
- .topic1-charts :deep(.donut-chart) {
- width: 120px;
- height: 120px;
- }
- .pie-chart-card :deep(.legend-group--topic1-grid),
- .pie-chart-card--age :deep(.legend-group--topic1-grid),
- .land-purpose-pie :deep(.legend-group--topic1-grid) {
- flex: 1;
- width: auto;
- min-width: 0;
- max-width: 100%;
- height: auto;
- padding: 0;
- row-gap: 6px;
- column-gap: 12px;
- }
- .pie-chart-card--usage :deep(.legend-group--topic1-grid),
- .pie-chart-card--building :deep(.legend-group--topic1-grid) {
- min-height: 88px;
- grid-template-rows: repeat(6, auto);
- }
- .pie-chart-card--age :deep(.legend-group--topic1-grid) {
- min-height: 88px;
- grid-template-rows: repeat(2, auto);
- row-gap: 8px;
- }
- .land-purpose-pie :deep(.legend-group--topic1-grid) {
- min-height: 220px;
- grid-template-rows: repeat(6, auto);
- }
- .pie-chart-card :deep(.legend-item__text),
- .pie-chart-card--age :deep(.legend-item__text),
- .land-purpose-pie :deep(.legend-item__text) {
- font-size: 12px;
- line-height: 14px;
- }
- .pie-chart-card :deep(.legend-item__percent),
- .pie-chart-card--age :deep(.legend-item__percent),
- .land-purpose-pie :deep(.legend-item__percent) {
- font-size: 12px;
- line-height: 14px;
- margin-left: 4px;
- }
- .pie-chart-card :deep(.legend-item__text span:last-child),
- .pie-chart-card--age :deep(.legend-item__text span:last-child),
- .land-purpose-pie :deep(.legend-item__text span:last-child) {
- white-space: normal;
- overflow: visible;
- text-overflow: unset;
- }
- }
- </style>
|