2 Commits 7f8a21e80b ... 7d2056f8fa

Autore SHA1 Messaggio Data
  gongtianxiao 7d2056f8fa 修改专题页资产列表布局 1 mese fa
  gongtianxiao 8b1c42569f 新增首页饼图点击跳转 1 mese fa

+ 16 - 0
src/components/stats/ChartSpecial.vue

@@ -21,16 +21,20 @@
           <SectionHeader :title="item.text" />
           <BarChart
             v-if="isSmallScreen"
+            clickable
             :xAxisData="pieToBar(item.data).xAxisData"
             :seriesData="pieToBar(item.data).seriesData"
             :style="smallBarStyle"
+            @bar-click="item.onSliceClick"
           />
           <PieChart
             v-else
+            clickable
             :data="item.data"
             :legend-head-count="item.legendHeadCount"
             :legend-grid-no-padding-right="item.legendGridNoPaddingRight"
             value-unit="万m²"
+            @slice-click="item.onSliceClick"
           />
         </div>
       </div>
@@ -57,6 +61,7 @@ import { useRouter } from 'vue-router';
 import { useEnterpriseStore } from '@/store/enterprise';
 import { useFilterStore } from '@/store/filter';
 import { sumFieldToWan,toWanSquareMeters } from '@/utils/areaMath';
+import { buildTopic1ChartQuery } from '@/utils/topic1ChartNavigation';
 
 const router = useRouter();
 const filterStore = useFilterStore();
@@ -86,6 +91,11 @@ const onMainBarClick = (groupName) => {
   router.push('/topic1');
 };
 
+const navigateToTopic1FromPie = (chartKey, sliceName) => {
+  const query = buildTopic1ChartQuery(chartKey, sliceName);
+  router.push(Object.keys(query).length ? { path: '/topic1', query } : '/topic1');
+};
+
 const filteredList = computed(() =>
   enterpriseStore.filteredByGroup(filterStore.selectGroup, filterStore.selectDate)
 );
@@ -241,19 +251,25 @@ const pieChartList = computed(() => [
   {
     name: '图表1',
     text: '土地类型面积',
+    chartKey: 'landUsage',
     data: isSmallScreen.value ? purposeListCompact.value : purposeListFull.value,
     legendHeadCount: isSmallScreen.value ? 0 : PURPOSE_TOP_N,
+    onSliceClick: (name) => navigateToTopic1FromPie('landUsage', name),
   },
   {
     name: '图表2',
     text: '房屋使用状态',
+    chartKey: 'util',
     data: usageStatusList.value,
+    onSliceClick: (name) => navigateToTopic1FromPie('util', name),
   },
   {
     name: '图表3',
     text: '房龄分布',
+    chartKey: 'age',
     data: ageDistributionList.value,
     legendGridNoPaddingRight: true,
+    onSliceClick: (name) => navigateToTopic1FromPie('age', name),
   },
 ]);
 

+ 98 - 32
src/components/stats/ComplianceAssetTable.vue

@@ -1,42 +1,55 @@
 <template>
   <div class="compliance-table" :class="{ 'compliance-table--fluid': fluid }">
-    <div class="compliance-table__header">
-      <div
-        v-for="column in columns"
-        :key="column.key"
-        class="compliance-table__cell compliance-table__cell--header"
-        :style="cellStyle(column)"
-      >
-        {{ column.label }}
+    <div :class="{ 'compliance-table__sticky-chrome': stickyChrome }">
+      <div v-if="stickyChrome" class="compliance-table__chrome-content">
+        <slot name="chrome" />
       </div>
-    </div>
-    <div class="compliance-table__body">
-      <template v-if="paginatedRows.length">
-        <div
-          v-for="(row, rowIndex) in paginatedRows"
-          :key="row.id ?? rowIndex"
-          class="compliance-table__row"
-        >
+      <div ref="headScrollRef" class="compliance-table-head-scroll">
+        <div class="compliance-table__header">
           <div
             v-for="column in columns"
             :key="column.key"
-            class="compliance-table__cell"
-            :class="{ 'compliance-table__cell--action': column.type === 'action' }"
+            class="compliance-table__cell compliance-table__cell--header"
             :style="cellStyle(column)"
           >
-            <button
-              v-if="column.type === 'action'"
-              type="button"
-              class="compliance-table__link"
-              @click="$emit('row-action', row)"
-            >
-              {{ column.actionLabel || '查看' }}
-            </button>
-            <template v-else>{{ row[column.key] ?? '暂无' }}</template>
+            {{ column.label }}
           </div>
         </div>
-      </template>
-      <div v-else class="compliance-table__empty">暂无符合条件的数据</div>
+      </div>
+    </div>
+    <div
+      class="compliance-table-body-scroll"
+      :class="{ 'compliance-table-body-scroll--sticky-chrome': stickyChrome }"
+      @scroll="syncTableScroll"
+    >
+      <div class="compliance-table__body">
+        <template v-if="paginatedRows.length">
+          <div
+            v-for="(row, rowIndex) in paginatedRows"
+            :key="row.id ?? rowIndex"
+            class="compliance-table__row"
+          >
+            <div
+              v-for="column in columns"
+              :key="column.key"
+              class="compliance-table__cell"
+              :class="{ 'compliance-table__cell--action': column.type === 'action' }"
+              :style="cellStyle(column)"
+            >
+              <button
+                v-if="column.type === 'action'"
+                type="button"
+                class="compliance-table__link"
+                @click="$emit('row-action', row)"
+              >
+                {{ column.actionLabel || '查看' }}
+              </button>
+              <template v-else>{{ row[column.key] ?? '暂无' }}</template>
+            </div>
+          </div>
+        </template>
+        <div v-else class="compliance-table__empty">暂无符合条件的数据</div>
+      </div>
     </div>
     <div class="compliance-table__pagination">
       <button type="button" :disabled="currentPage === 1" @click="prevPage">
@@ -51,7 +64,7 @@
 </template>
 
 <script setup>
-import { ref, computed, watch, defineProps, defineEmits} from 'vue';
+import { ref, computed, watch, defineProps, defineEmits } from 'vue';
 
 const props = defineProps({
   columns: {
@@ -70,10 +83,24 @@ const props = defineProps({
     type: Boolean,
     default: false,
   },
+  stickyChrome: {
+    type: Boolean,
+    default: false,
+  },
 });
 
 defineEmits(['row-action']);
 
+const headScrollRef = ref(null);
+
+const syncTableScroll = (event) => {
+  const head = headScrollRef.value;
+  const scrollLeft = event.target.scrollLeft;
+  if (head && head.scrollLeft !== scrollLeft) {
+    head.scrollLeft = scrollLeft;
+  }
+};
+
 const cellStyle = (column) => {
   if (props.fluid) {
     if (column.type === 'action') {
@@ -137,6 +164,46 @@ watch(totalPages, (pages) => {
   gap: 4px;
 }
 
+/* 与 PageHeader 高度一致(layout/PageHeader.vue) */
+.compliance-table__sticky-chrome {
+  position: sticky;
+  top: 64px;
+  z-index: 50;
+  background: var(--color-surface);
+  display: flex;
+  flex-direction: column;
+  gap: var(--section-gap);
+  margin: 0 calc(-1 * var(--card-padding));
+  padding: 0 var(--card-padding);
+  box-shadow: 0 4px 12px rgba(17, 17, 17, 0.06);
+}
+
+.compliance-table__chrome-content {
+  display: flex;
+  flex-direction: column;
+  gap: var(--section-gap);
+}
+
+.compliance-table-head-scroll {
+  width: 100%;
+  overflow: hidden;
+  flex-shrink: 0;
+  border-bottom: 1px solid var(--color-divider);
+  padding-right: 8px;
+  box-sizing: border-box;
+}
+
+.compliance-table-body-scroll {
+  width: 100%;
+  max-height: min(720px, calc(100vh - 280px));
+  overflow: auto;
+  scrollbar-gutter: stable;
+}
+
+.compliance-table-body-scroll--sticky-chrome {
+  max-height: min(640px, calc(100vh - 380px));
+}
+
 .compliance-table__header,
 .compliance-table__row {
   display: flex;
@@ -168,7 +235,7 @@ watch(totalPages, (pages) => {
 
 .compliance-table--fluid .compliance-table__cell--header {
   white-space: nowrap;
-  overflow: hidden;
+  overflow: visible;
   text-overflow: ellipsis;
   height: 42px;
   line-height: 42px;
@@ -286,5 +353,4 @@ watch(totalPages, (pages) => {
   font-size: 14px;
   padding: 0;
 }
-
 </style>

+ 31 - 36
src/components/stats/TopicFullDataListSection.vue

@@ -1,35 +1,36 @@
 <template>
   <div ref="rootRef" class="topic-full-list">
-    <div class="topic-full-list__head">
-      <SectionHeader :title="title" />
-      <button
-        v-if="showExport"
-        type="button"
-        class="export-btn"
-        @click="$emit('export')"
-      >
-        {{ exportLabel }}
-      </button>
-    </div>
-
-    <slot name="filters" />
-
-    <slot name="hint">
-      <p v-if="filterHint" class="filter-hint">
-        当前筛选:<strong>{{ filterHint }}</strong>
-        <template v-if="total != null"> · 共 {{ total }} 条</template>
-      </p>
-    </slot>
-
-    <div class="table-scroll">
-      <ComplianceAssetTable
-        fluid
-        :columns="columns"
-        :rows="rows"
-        :page-size="pageSize"
-        @row-action="$emit('row-action', $event)"
-      />
-    </div>
+    <ComplianceAssetTable
+      fluid
+      sticky-chrome
+      :columns="columns"
+      :rows="rows"
+      :page-size="pageSize"
+      @row-action="$emit('row-action', $event)"
+    >
+      <template #chrome>
+        <div class="topic-full-list__head">
+          <SectionHeader :title="title" />
+          <button
+            v-if="showExport"
+            type="button"
+            class="export-btn"
+            @click="$emit('export')"
+          >
+            {{ exportLabel }}
+          </button>
+        </div>
+
+        <slot name="filters" />
+
+        <slot name="hint">
+          <p v-if="filterHint" class="filter-hint">
+            当前筛选:<strong>{{ filterHint }}</strong>
+            <template v-if="total != null"> · 共 {{ total }} 条</template>
+          </p>
+        </slot>
+      </template>
+    </ComplianceAssetTable>
   </div>
 </template>
 
@@ -91,7 +92,6 @@ defineExpose({
   padding: var(--card-padding);
   display: flex;
   flex-direction: column;
-  gap: var(--section-gap);
   background: var(--color-surface);
   border: 1px solid var(--color-accent-border);
 }
@@ -104,11 +104,6 @@ defineExpose({
   flex-wrap: wrap;
 }
 
-.table-scroll {
-  width: 100%;
-  overflow-x: auto;
-}
-
 .export-btn {
   height: 38px;
   padding: 0 20px;

+ 1 - 1
src/pages/HomePage.vue

@@ -120,7 +120,7 @@ const metricCardList = computed(() => [
   },
   {
     id: 2,
-    title: '已出租/在用建筑面积',
+    title: '已使用建筑面积',
     text: '占总建筑面积',
     showTrendIcon: false,
     percent: rentPercent.value,

+ 26 - 19
src/pages/Topic1Page.vue

@@ -74,8 +74,8 @@
 
 <script setup>
 import { showWarning } from '@/utils/message';
-import { ref, computed, watch } from 'vue';
-import { useRouter } from 'vue-router';
+import { ref, computed, watch, nextTick } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
 import PageHeader from '@/components/layout/PageHeader.vue';
 import TopicPageHeader from '@/components/stats/TopicPageHeader.vue';
 import QueryFilter from '@/components/stats/QueryFilter.vue';
@@ -93,21 +93,25 @@ import {
   mapTopic1Row,
 } from '@/utils/topic1AssetList';
 import { GROUP_DISPLAY_NAMES } from '@/utils/aggregateReportRecords';
+import {
+  AGE_SLICE_TO_KEY,
+  createDefaultTopic1ListFilters,
+  hasTopic1ChartQuery,
+  parseTopic1ListFiltersFromQuery,
+} from '@/utils/topic1ChartNavigation';
 
 const enterpriseStore = useEnterpriseStore();
 const filterStore = useFilterStore();
 const router = useRouter();
+const route = useRoute();
 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 createDefaultFilters = (group = filterStore.selectGroup) =>
+  createDefaultTopic1ListFilters(group);
 
 const USAGE_PIE_CONFIG = [
   { name: '出租', field: 'c_czmj', color: 'rgba(1, 118, 255, 1)' },
@@ -116,17 +120,6 @@ const USAGE_PIE_CONFIG = [
   { name: '闲置', field: 'c_xzmj', color: 'rgba(255, 232, 119, 1)' },
 ];
 
-const createDefaultFilters = (group = filterStore.selectGroup) => ({
-  keyword: '',
-  purpose: '全部',
-  group,
-  age: '全部',
-  util: '全部',
-  fwUsage: '',
-  landType: '',
-  landUsage: '',
-});
-
 const listFilters = ref(createDefaultFilters());
 
 const filteredList = computed(() =>
@@ -272,9 +265,23 @@ const resetListFilters = () => {
 };
 
 watch(() => filterStore.selectGroup, (group) => {
+  if (hasTopic1ChartQuery(route.query)) {
+    listFilters.value = parseTopic1ListFiltersFromQuery(route.query, group);
+    return;
+  }
   listFilters.value = createDefaultFilters(group);
 });
 
+watch(
+  () => route.fullPath,
+  () => {
+    if (!hasTopic1ChartQuery(route.query)) return;
+    listFilters.value = parseTopic1ListFiltersFromQuery(route.query, filterStore.selectGroup);
+    nextTick(() => scrollToFullList());
+  },
+  { immediate: true },
+);
+
 watch(
   () => listFilters.value.group,
   (group) => {
@@ -389,7 +396,7 @@ const topPieChartsList = computed(() => [
   width: 100%;
   max-width: var(--page-max-width);
   padding: 24px;
-  padding-bottom: 32px;
+  padding-bottom: 0;
   display: flex;
   flex-direction: column;
   gap: 12px;

+ 1 - 1
src/pages/Topic2Page.vue

@@ -56,7 +56,7 @@
         title="权属穿透全量数据列表"
         :columns="TOPIC2_FULL_COLUMNS"
         :rows="fullRows"
-        :page-size="7"
+        :page-size="10"
         :filter-hint="filterHint"
         @export="handleExport"
         @row-action="openAssetDetail"

+ 1 - 1
src/pages/Topic3Page.vue

@@ -120,7 +120,7 @@
         title="闲置资产全量数据列表"
         :columns="TOPIC3_FULL_COLUMNS"
         :rows="fullRows"
-        :page-size="7"
+        :page-size="10"
         :filter-hint="filterHint"
         :total="fullRows.length"
         @export="handleExport"

+ 54 - 0
src/utils/topic1ChartNavigation.js

@@ -0,0 +1,54 @@
+/** 房龄环形图扇区名称 → 列表筛选项 key */
+export const AGE_SLICE_TO_KEY = {
+  '0-10年': '0-10',
+  '10-20年': '10-20',
+  '20年以上': '20+',
+};
+
+export const TOPIC1_HOME_LAND_OTHER = '其他';
+
+export const createDefaultTopic1ListFilters = (group = '全部') => ({
+  keyword: '',
+  purpose: '全部',
+  group,
+  age: '全部',
+  util: '全部',
+  fwUsage: '',
+  landType: '',
+  landUsage: '',
+});
+
+export const hasTopic1ChartQuery = (query = {}) =>
+  Boolean(query.util || query.landUsage || query.age || query.fwUsage);
+
+/** 首页 / 外链跳转:环形图扇区 → 专题1路由 query */
+export const buildTopic1ChartQuery = (chartKey, sliceName) => {
+  const query = {};
+  if (chartKey === 'util' && sliceName) {
+    query.util = sliceName;
+    return query;
+  }
+  if (chartKey === 'landUsage' && sliceName && sliceName !== TOPIC1_HOME_LAND_OTHER) {
+    query.landUsage = sliceName;
+    return query;
+  }
+  if (chartKey === 'age') {
+    const ageKey = AGE_SLICE_TO_KEY[sliceName];
+    if (ageKey) query.age = ageKey;
+    return query;
+  }
+  if (chartKey === 'fwUsage' && sliceName) {
+    query.fwUsage = sliceName;
+    return query;
+  }
+  return query;
+};
+
+export const parseTopic1ListFiltersFromQuery = (query = {}, group = '全部') => {
+  const filters = createDefaultTopic1ListFilters(group);
+  if (typeof query.util === 'string' && query.util) filters.util = query.util;
+  if (typeof query.landUsage === 'string' && query.landUsage) filters.landUsage = query.landUsage;
+  if (typeof query.age === 'string' && query.age) filters.age = query.age;
+  if (typeof query.fwUsage === 'string' && query.fwUsage) filters.fwUsage = query.fwUsage;
+  return filters;
+};