| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561 |
- <template>
- <div :class="prefixCls" class="case-clue-page">
- <div class="page-header">
- <div>
- <div class="page-title">线索推送</div>
- <div class="page-subtitle">按照分析对象查看线索分类和命中详情,保持与案件分析页面一致的卡片式布局。</div>
- </div>
- </div>
- <div class="filter-bar">
- <div class="filter-item">
- <span class="filter-label">分析对象:</span>
- <Input v-model:value="filters.keyword" class="search-input" placeholder="请输入对象姓名" allow-clear />
- </div>
- <div class="filter-actions">
- <Button type="primary" @click="handleSearch">查询</Button>
- <Button @click="handleReset">重置</Button>
- </div>
- </div>
- <div class="content-panel">
- <aside class="panel-left">
- <div class="panel-header">
- <div>
- <div class="panel-title">分析对象列表</div>
- <div class="panel-desc">点击左侧对象可联动查看右侧的线索分类和命中详情。</div>
- </div>
- </div>
- <div class="target-list">
- <div
- v-for="item in filteredTargets"
- :key="item.id"
- class="target-item"
- :class="{ active: item.id === activeTarget }"
- @click="activeTarget = item.id"
- >
- <div class="target-name">{{ item.name }}</div>
- <div class="target-meta">共发现 {{ item.count }} 类线索</div>
- </div>
- <EmptyState variant="compact" description="暂无匹配对象" :min-height="180" />
- </div>
- <div class="note-card">此处展示的线索类别数量为当前对象已命中的结果总数。</div>
- </aside>
- <section class="panel-right">
- <div class="panel-header detail-header">
- <div>
- <div class="panel-title">{{ activeTargetInfo.name || '未选择对象' }} 的线索详情</div>
- <div class="panel-desc">按通信、交易、轨迹等维度切换查看具体命中情况。</div>
- </div>
- <div class="target-badge">命中 {{ activeTargetInfo.count || 0 }} 类</div>
- </div>
- <div class="category-tabs">
- <button
- v-for="item in categoryTabs"
- :key="item.key"
- type="button"
- class="category-tab"
- :class="{ active: activeCategory === item.key }"
- @click="activeCategory = item.key"
- >
- {{ item.label }}({{ item.count }})
- </button>
- </div>
- <div class="detail-layout">
- <div class="detail-nav">
- <button
- v-for="item in activeCategoryConfig.tabs"
- :key="item.key"
- type="button"
- class="detail-nav-item"
- :class="{ active: activeSubTab === item.key }"
- @click="activeSubTab = item.key"
- >
- {{ item.label }}
- </button>
- </div>
- <div class="detail-main">
- <div class="detail-main__header">
- <div class="detail-title">{{ activeTabConfig?.label || '-' }}</div>
- <div class="detail-desc">{{ activeTabConfig?.desc || '暂无线索描述' }}</div>
- </div>
- <div v-if="activeDetailRows.length" class="detail-list">
- <div v-for="row in activeDetailRows" :key="row.id" class="detail-item">
- <div class="detail-item__title">{{ row.name }}</div>
- <div class="detail-item__desc">{{ row.desc }}</div>
- </div>
- </div>
- <EmptyState variant="compact" description="当前分类暂无线索" :min-height="220" />
- </div>
- </div>
- </section>
- </div>
- </div>
- </template>
- <script lang="ts" setup name="ViewsCaseCluePush">
- import { computed, ref, watch } from 'vue';
- import { Button, Input } from 'ant-design-vue';
- import { EmptyState } from '@jeesite/core/components/EmptyState';
- import { useDesign } from '@jeesite/core/hooks/web/useDesign';
- import { useMessage } from '@jeesite/core/hooks/web/useMessage';
- const { prefixCls } = useDesign('case-clue');
- const { createMessage } = useMessage();
- const filters = ref({
- keyword: '',
- });
- const clueTargets = ref([
- { id: 1, name: '高某某', count: 3 },
- { id: 2, name: '王某某', count: 3 },
- { id: 3, name: '刘某某', count: 12 },
- { id: 4, name: '秦某某', count: 3 },
- ]);
- const activeTarget = ref(3);
- const activeCategory = ref<'call' | 'trade' | 'track'>('call');
- const activeSubTab = ref('continuous');
- const categoryTabs = [
- { key: 'call', label: '通信情况', count: 4 },
- { key: 'trade', label: '交易情况', count: 4 },
- { key: 'track', label: '活动轨迹', count: 4 },
- ] as const;
- const detailMap = {
- call: {
- tabs: [
- {
- key: 'continuous',
- label: '持续联系',
- desc: '展示对象与同一号码持续发生联系的命中记录。',
- rows: [
- { id: 1, name: '刘某某与 15890987654', desc: '近 30 天持续联系 12 次,通联密度较高。' },
- { id: 2, name: '刘某某与 13978324567', desc: '工作日夜间存在稳定联系记录。' },
- { id: 3, name: '刘某某与 17762099876', desc: '节假日期间仍保持频繁通话。' },
- ],
- },
- {
- key: 'night',
- label: '夜间联系',
- desc: '重点查看深夜时段的频繁联系情况。',
- rows: [
- { id: 4, name: '刘某某与 15890987654', desc: '23:00 至 02:00 期间联系 6 次。' },
- { id: 5, name: '刘某某与 18620234567', desc: '凌晨时段多次短通话,建议重点关注。' },
- ],
- },
- ],
- },
- trade: {
- tabs: [
- {
- key: 'capital',
- label: '资金往来',
- desc: '展示对象与可疑账户之间的交易命中概况。',
- rows: [
- { id: 6, name: '刘某某与 张某某', desc: '近 7 日发生 5 笔资金往来,累计 18.6 万元。' },
- { id: 7, name: '刘某某与 王某某', desc: '交易频次较高,呈现固定时间段转账特征。' },
- ],
- },
- {
- key: 'cash',
- label: '现金交易',
- desc: '展示对象现金存取等高频操作情况。',
- rows: [{ id: 8, name: '刘某某关联账户', desc: '工作日连续 3 天存在大额现金存入。' }],
- },
- ],
- },
- track: {
- tabs: [
- {
- key: 'co-stay',
- label: '共同驻留',
- desc: '展示对象与同伴在关键地点的共同驻留情况。',
- rows: [
- { id: 9, name: '刘某某与 高某某', desc: '在重点商圈连续 2 天共同出现。' },
- { id: 10, name: '刘某某与 王某某', desc: '凌晨时段在同一基站片区长时间驻留。' },
- ],
- },
- {
- key: 'route',
- label: '轨迹同行',
- desc: '展示对象在路线上的同步活动情况。',
- rows: [{ id: 11, name: '刘某某与 秦某某', desc: '近 3 次跨区出行轨迹高度重叠。' }],
- },
- ],
- },
- } as const;
- const filteredTargets = computed(() => {
- const keyword = filters.value.keyword.trim();
- if (!keyword) return clueTargets.value;
- return clueTargets.value.filter((item) => item.name.includes(keyword));
- });
- const activeCategoryConfig = computed(() => detailMap[activeCategory.value]);
- const activeTabConfig = computed(
- () =>
- activeCategoryConfig.value.tabs.find((item) => item.key === activeSubTab.value) ||
- activeCategoryConfig.value.tabs[0],
- );
- const activeTargetInfo = computed(
- () => filteredTargets.value.find((item) => item.id === activeTarget.value) || { name: '未选择对象', count: 0 },
- );
- const activeDetailRows = computed(() => {
- if (!filteredTargets.value.length) return [];
- return activeTabConfig.value?.rows || [];
- });
- const handleSearch = () => {
- if (!filteredTargets.value.length) {
- createMessage.info('未找到符合条件的分析对象');
- }
- };
- const handleReset = () => {
- filters.value.keyword = '';
- activeCategory.value = 'call';
- activeSubTab.value = 'continuous';
- };
- watch(
- filteredTargets,
- (list) => {
- if (!list.length) {
- activeTarget.value = 0;
- return;
- }
- if (!list.some((item) => item.id === activeTarget.value)) {
- activeTarget.value = list[0].id;
- }
- },
- { immediate: true },
- );
- watch(
- activeCategory,
- () => {
- activeSubTab.value = activeCategoryConfig.value.tabs[0]?.key || '';
- },
- { immediate: true },
- );
- </script>
- <style lang="less">
- @prefix-cls: ~'jeesite-case-clue';
- .@{prefix-cls}.case-clue-page {
- min-height: 100%;
- height: 100%;
- padding: 12px 16px 24px;
- background: #fff;
- display: flex;
- flex-direction: column;
- gap: 12px;
- box-sizing: border-box;
- overflow: auto;
- .page-title {
- font-size: 18px;
- font-weight: 600;
- line-height: 1.4;
- color: #111827;
- }
- .page-subtitle {
- margin-top: 4px;
- font-size: 13px;
- line-height: 1.6;
- color: #6b7280;
- }
- .filter-bar {
- display: flex;
- align-items: center;
- gap: 12px;
- flex-wrap: wrap;
- padding: 12px 16px;
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- background: #fafafa;
- box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
- }
- .filter-item {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .filter-label {
- font-size: 13px;
- color: #4b5563;
- white-space: nowrap;
- }
- .search-input {
- width: 220px;
- }
- .filter-actions {
- display: flex;
- gap: 8px;
- margin-left: auto;
- }
- .content-panel {
- display: grid;
- grid-template-columns: minmax(240px, 320px) minmax(0, 1fr);
- gap: 12px;
- flex: 1 1 auto;
- min-height: 0;
- }
- .panel-left,
- .panel-right {
- display: flex;
- flex-direction: column;
- min-height: 0;
- border: 1px solid #e5e7eb;
- border-radius: 14px;
- background: #fff;
- box-shadow: 0 10px 28px rgba(15, 23, 42, 0.04);
- overflow: hidden;
- }
- .panel-header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 12px;
- padding: 16px 18px 12px;
- border-bottom: 1px solid #eef2f7;
- }
- .panel-title {
- font-size: 15px;
- font-weight: 600;
- color: #111827;
- }
- .panel-desc {
- margin-top: 4px;
- font-size: 12px;
- line-height: 1.6;
- color: #6b7280;
- }
- .target-list {
- flex: 1 1 auto;
- min-height: 0;
- overflow: auto;
- padding: 14px;
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- .target-item {
- padding: 12px 14px;
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- background: #fff;
- cursor: pointer;
- transition:
- border-color 0.2s ease,
- box-shadow 0.2s ease,
- background-color 0.2s ease;
- &:hover {
- border-color: #cbd5e1;
- background: #fafafa;
- }
- &.active {
- border-color: #bfdbfe;
- background: #eff6ff;
- box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.08);
- }
- }
- .target-name {
- font-size: 14px;
- font-weight: 600;
- color: #111827;
- }
- .target-meta {
- margin-top: 6px;
- font-size: 12px;
- color: #6b7280;
- }
- .note-card {
- margin: 0 14px 14px;
- padding: 12px 14px;
- border-radius: 12px;
- border: 1px solid #f3e8a3;
- background: #fffbea;
- color: #6b7280;
- font-size: 12px;
- line-height: 1.7;
- }
- .detail-header {
- border-bottom: none;
- padding-bottom: 8px;
- }
- .target-badge {
- padding: 6px 12px;
- border-radius: 999px;
- background: #eff6ff;
- color: #2563eb;
- font-size: 12px;
- font-weight: 600;
- white-space: nowrap;
- }
- .category-tabs {
- display: flex;
- gap: 10px;
- padding: 0 18px 12px;
- border-bottom: 1px solid #eef2f7;
- flex-wrap: wrap;
- }
- .category-tab,
- .detail-nav-item {
- border: 1px solid #e5e7eb;
- background: #fff;
- color: #4b5563;
- cursor: pointer;
- transition: all 0.2s ease;
- }
- .category-tab {
- padding: 8px 14px;
- border-radius: 999px;
- font-size: 13px;
- &.active {
- border-color: #bfdbfe;
- background: #eff6ff;
- color: #2563eb;
- font-weight: 600;
- }
- }
- .detail-layout {
- display: grid;
- grid-template-columns: 150px minmax(0, 1fr);
- gap: 12px;
- min-height: 0;
- flex: 1 1 auto;
- padding: 16px 18px 18px;
- }
- .detail-nav {
- padding: 12px;
- border: 1px solid #eef2f7;
- border-radius: 12px;
- background: #fafafa;
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .detail-nav-item {
- padding: 10px 12px;
- border-radius: 10px;
- font-size: 13px;
- text-align: left;
- &.active {
- border-color: #bfdbfe;
- background: #eff6ff;
- color: #2563eb;
- font-weight: 600;
- }
- }
- .detail-main {
- min-height: 0;
- display: flex;
- flex-direction: column;
- border: 1px solid #eef2f7;
- border-radius: 12px;
- background: #fff;
- overflow: hidden;
- }
- .detail-main__header {
- padding: 16px 18px 12px;
- border-bottom: 1px solid #eef2f7;
- }
- .detail-title {
- font-size: 15px;
- font-weight: 600;
- color: #111827;
- }
- .detail-desc {
- margin-top: 4px;
- font-size: 12px;
- line-height: 1.6;
- color: #6b7280;
- }
- .detail-list {
- flex: 1 1 auto;
- min-height: 0;
- overflow: auto;
- padding: 14px 18px 18px;
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
- .detail-item {
- padding: 14px 16px;
- border-radius: 12px;
- border: 1px solid #e5e7eb;
- background: #fafafa;
- }
- .detail-item__title {
- font-size: 14px;
- font-weight: 600;
- color: #111827;
- }
- .detail-item__desc {
- margin-top: 6px;
- font-size: 12px;
- line-height: 1.7;
- color: #6b7280;
- }
- @media (max-width: 960px) {
- .content-panel,
- .detail-layout {
- grid-template-columns: 1fr;
- }
- .filter-actions {
- margin-left: 0;
- }
- }
- }
- </style>
|