|
|
@@ -26,7 +26,7 @@
|
|
|
<div class="policy-item-action" @click.stop="openDetailByItem(item)">查看详情</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="view-all-btn" @click.stop="openPolicyList">更多 >></div>
|
|
|
+ <div class="view-all-btn" @click.stop="openCardList">更多 >></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -36,11 +36,11 @@
|
|
|
<transition name="policy-panel">
|
|
|
<div v-if="panelVisible" class="policy-viewer-content" :class="[
|
|
|
isWide ? 'right-fixed' : '',
|
|
|
- panelMode === 'list' ? 'list-mode-bg' : 'detail-mode-bg'
|
|
|
+ panelMode === 'detail' ? 'detail-mode-bg' : 'list-mode-bg'
|
|
|
]">
|
|
|
<div class="policy-viewer-header" :class="{ 'policy-viewer-header-record': recordMode }">
|
|
|
<div class="header-title" v-if="!recordMode && !(isMobileViewer && panelMode === 'detail')">
|
|
|
- {{ panelMode === "list" ? "智能政策匹配" : "智能政策匹配" }}
|
|
|
+ {{ panelMode === 'cardList' ? '政策列表' : '智能政策匹配' }}
|
|
|
</div>
|
|
|
<div class="header-actions">
|
|
|
<button v-if="panelMode === 'detail' && !recordMode" class="header-back" @click.stop="openPolicyList">
|
|
|
@@ -58,15 +58,19 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <div v-if="panelMode === 'list'" class="panel-scroll list-body" ref="listBodyRef"
|
|
|
+ <!-- list(旧「政策事项列表」)与 cardList(「更多」→「政策列表」)共用这个 body,
|
|
|
+ 靠下面的 v-if 分叉内容;样式类完全复用,未新增 CSS -->
|
|
|
+ <div v-if="panelMode !== 'detail'" class="panel-scroll list-body" ref="listBodyRef"
|
|
|
:class="{ 'list-body-record': recordMode }">
|
|
|
<div class="list-content">
|
|
|
<div class="record-list-header" v-if="recordMode">
|
|
|
- <div class="record-list-title">政策事项列表</div>
|
|
|
- <div class="record-list-subtitle">浏览、搜索及筛选最新的惠企政策</div>
|
|
|
+ <div class="record-list-title">{{ panelMode === 'cardList' ? '政策列表' : '政策事项列表' }}</div>
|
|
|
+ <div class="record-list-subtitle">
|
|
|
+ {{ panelMode === 'cardList' ? '汇总本会话内出现的全部政策事项' : '浏览、搜索及筛选最新的惠企政策' }}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
- <div class="search-filter-bar" v-if="recordMode && isZhaoshangLLM">
|
|
|
+ <div class="search-filter-bar" v-if="isZhaoshangLLM && (recordMode || panelMode === 'cardList')">
|
|
|
<div class="search-filter-top">
|
|
|
<div class="search-input-wrapper">
|
|
|
<svg class="search-icon" width="18" height="18" viewBox="0 0 18 18" fill="none"
|
|
|
@@ -77,7 +81,8 @@
|
|
|
</svg>
|
|
|
<input type="text" placeholder="输入政策关键词" class="search-input" v-model="searchKeyword" />
|
|
|
</div>
|
|
|
- <div class="filter-selects" @click.stop>
|
|
|
+ <!-- 部门 / 分类筛选:只在旧列表面板出现;「政策列表」侧边栏按需求不带筛选 -->
|
|
|
+ <div class="filter-selects" v-if="panelMode === 'list'" @click.stop>
|
|
|
<div class="filter-select-wrapper">
|
|
|
<div class="filter-option" @click="toggleFilterMenu" :class="{ 'is-open': showFilterMenu }">
|
|
|
{{ currentFilterOption }}
|
|
|
@@ -470,6 +475,10 @@ import {
|
|
|
extractPolicyListResponse,
|
|
|
getLocalPolicyListResponse,
|
|
|
resolvePolicyDetailIndex,
|
|
|
+ buildPolicyCardDedupKey,
|
|
|
+ dedupePolicyItems,
|
|
|
+ matchesPolicyCardKeyword,
|
|
|
+ sortPolicyCardsByMatchScore,
|
|
|
type PolicyListFilterOption,
|
|
|
} from "./policy-match-utils";
|
|
|
import { MODAL_MUTEX_KEY } from "../business-assistant/modal-mutex";
|
|
|
@@ -527,7 +536,10 @@ const panelVisible = ref(false);
|
|
|
const policyModalId = Symbol("policy-viewer");
|
|
|
const modalMutex = inject(MODAL_MUTEX_KEY, null);
|
|
|
let unregisterPolicyModal: (() => void) | undefined;
|
|
|
-const panelMode = ref<"list" | "detail">("list");
|
|
|
+// list = 旧列表(惠企政策库 262 条 + 部门/分类筛选),详情面板「返回政策事项列表」用
|
|
|
+// cardList = 卡片区「更多」打开的新侧边栏(本会话全部卡片聚合,无筛选,只有搜索+热门搜索)
|
|
|
+// detail = 单条政策详情
|
|
|
+const panelMode = ref<"list" | "cardList" | "detail">("list");
|
|
|
const selectedPolicy = ref<number>(0);
|
|
|
const selectedPolicyOverride = ref<PolicyItem | null>(null);
|
|
|
const isWide = ref(false);
|
|
|
@@ -616,6 +628,37 @@ onBeforeUnmount(() => {
|
|
|
document.removeEventListener("click", closeMenus);
|
|
|
});
|
|
|
|
|
|
+// ─── 「更多」→ 政策列表 侧边栏(本会话卡片聚合)─────────────────────────
|
|
|
+//
|
|
|
+// 数据来自 useBusinessAssistantChat provide 的会话级采集函数
|
|
|
+// (每个 PolicyMatch 实例只持有自己那条消息的卡片,聚合必须往上取)。
|
|
|
+// 采不到 provider 时(组件被独立使用)回退为本组件自己的卡片,行为与 syncPolicies 同源。
|
|
|
+const collectSessionPolicyCards = inject("collectSessionPolicyCards", null) as
|
|
|
+ | (() => Record<string, any>[])
|
|
|
+ | null;
|
|
|
+
|
|
|
+const cardListItems = ref<PolicyItem[]>([]);
|
|
|
+
|
|
|
+/** 重建聚合列表:采集 → 与政策库合并补全 → 规范化 → 去重(首见优先)→ 按匹配分排序 */
|
|
|
+const rebuildCardList = () => {
|
|
|
+ const raw =
|
|
|
+ collectSessionPolicyCards?.() ??
|
|
|
+ (props.policyData?.length ? props.policyData : parsePolicyTableContent(props.policyContent));
|
|
|
+ const normalized = (raw || []).map(mergeWithPublicPolicies).map(normalizePolicy);
|
|
|
+ cardListItems.value = sortPolicyCardsByMatchScore(
|
|
|
+ dedupePolicyItems(normalized, buildPolicyCardDedupKey)
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const cardListFiltered = computed(() =>
|
|
|
+ cardListItems.value.filter((item) => matchesPolicyCardKeyword(item, searchKeyword.value))
|
|
|
+);
|
|
|
+const cardListTotal = computed(() => cardListFiltered.value.length);
|
|
|
+const cardListPaginated = computed(() => {
|
|
|
+ const start = (currentPage.value - 1) * itemsPerPage;
|
|
|
+ return cardListFiltered.value.slice(start, start + itemsPerPage);
|
|
|
+});
|
|
|
+
|
|
|
const filteredPolicies = computed(() => {
|
|
|
if (props.recordMode) {
|
|
|
return listPolicyItems.value;
|
|
|
@@ -663,11 +706,18 @@ watch(currentPage, () => {
|
|
|
});
|
|
|
|
|
|
const totalPages = computed(() => {
|
|
|
+ if (panelMode.value === "cardList") {
|
|
|
+ return Math.ceil(cardListTotal.value / itemsPerPage);
|
|
|
+ }
|
|
|
const total = props.recordMode ? policyListTotal.value : filteredPolicies.value.length;
|
|
|
return Math.ceil(total / itemsPerPage);
|
|
|
});
|
|
|
|
|
|
const paginatedPolicies = computed(() => {
|
|
|
+ // 两种列表模式互斥(由 panelMode 单一开关),共用 currentPage/itemsPerPage 安全
|
|
|
+ if (panelMode.value === "cardList") {
|
|
|
+ return cardListPaginated.value;
|
|
|
+ }
|
|
|
if (props.recordMode) {
|
|
|
return filteredPolicies.value;
|
|
|
}
|
|
|
@@ -981,6 +1031,10 @@ const syncPolicies = () => {
|
|
|
buildRecordPolicyList();
|
|
|
}
|
|
|
}
|
|
|
+ // 「政策列表」侧边栏开着时,流式新增的卡片也同步进去
|
|
|
+ if (panelVisible.value && panelMode.value === "cardList") {
|
|
|
+ rebuildCardList();
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
let policyListRequestId = 0;
|
|
|
@@ -1197,11 +1251,13 @@ const openDetailByItem = async (item: { originalData?: RawPolicyItem; support_ob
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
- * 打开「政策事项列表」。
|
|
|
+ * 打开「政策事项列表」(旧列表)。
|
|
|
*
|
|
|
- * 卡片区「更多 >>」与惠企政策详情的「返回政策事项列表」**走同一条路径、同一个列表**——
|
|
|
- * 不因入口不同而内容不同。列表数据统一来自惠企政策库(policies_public.v1.json),
|
|
|
- * 本轮匹配到的惠企政策置顶。
|
|
|
+ * ⚠️ 2026-09-17 起,**卡片区的「更多 >>」不再走这里** —— 它改为打开
|
|
|
+ * `openCardList()` 的「政策列表」侧边栏(本会话卡片聚合、无筛选)。
|
|
|
+ * 本条路径现在只由惠企政策详情的「返回政策事项列表」使用。
|
|
|
+ *
|
|
|
+ * 列表数据统一来自惠企政策库(policies_public.v1.json),本轮匹配到的惠企政策置顶。
|
|
|
*/
|
|
|
const openPolicyList = async () => {
|
|
|
selectedPolicyOverride.value = null;
|
|
|
@@ -1222,6 +1278,36 @@ const openPolicyList = async () => {
|
|
|
emitPanelWidth();
|
|
|
};
|
|
|
|
|
|
+/**
|
|
|
+ * 打开「政策列表」侧边栏 —— 卡片区「更多 >>」的入口。
|
|
|
+ *
|
|
|
+ * 与 openPolicyList 的区别:内容是本会话内**所有消息**出现过的卡片(跨消息聚合、去重),
|
|
|
+ * 只有搜索框与热门搜索,**没有部门/分类筛选**。
|
|
|
+ *
|
|
|
+ * ⚠️ 必须**先切 panelMode 再重置共享状态**:重置会触发 watch → fetchPolicyList,
|
|
|
+ * 而它开头有 `panelMode !== "list"` 的守卫,切了模式才能让旧列表逻辑短路,
|
|
|
+ * 不污染 policyListItems / policyListTotal。
|
|
|
+ */
|
|
|
+const openCardList = async () => {
|
|
|
+ selectedPolicyOverride.value = null;
|
|
|
+ panelMode.value = "cardList";
|
|
|
+
|
|
|
+ currentPage.value = 1;
|
|
|
+ searchKeyword.value = "";
|
|
|
+ currentDeptFilter.value = "全部部门";
|
|
|
+ currentFilterOption.value = "所有分类";
|
|
|
+ showFilterMenu.value = false;
|
|
|
+ showDeptMenu.value = false;
|
|
|
+
|
|
|
+ rebuildCardList();
|
|
|
+
|
|
|
+ const openTicket = modalMutex?.requestOpen(policyModalId);
|
|
|
+ await nextTick();
|
|
|
+ if (modalMutex && openTicket !== undefined && !modalMutex.isCurrentRequest(policyModalId, openTicket)) return;
|
|
|
+ panelVisible.value = true;
|
|
|
+ emitPanelWidth();
|
|
|
+};
|
|
|
+
|
|
|
const closePanel = () => {
|
|
|
panelVisible.value = false;
|
|
|
modalMutex?.release(policyModalId);
|