| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712 |
- <template>
- <div class="intimacy-panel">
- <div class="section-header">
- <div class="section-title">亲密关系人</div>
- <div class="section-tools">
- <Input v-model:value="keyword" class="relation-search" placeholder="搜索被关联对象" allow-clear />
- <Button type="link" class="section-action" @click="handleOpenConfigModal">编辑</Button>
- </div>
- </div>
- <Spin :spinning="loading">
- <div v-if="filteredRelations.length" class="intimacy-content">
- <div class="intimacy-rail">
- <div class="intimacy-left-wrap">
- <Button v-if="showPrevButton" class="scroll-btn scroll-btn--prev" @click="scrollCards('left')"><</Button>
- <div ref="cardListRef" class="intimacy-card-list" @scroll="updateScrollButtons">
- <button
- v-for="(relation, index) in filteredRelations"
- :key="relation.id"
- type="button"
- class="intimacy-card"
- :class="{ active: index === activeIndex }"
- @click="activeIndex = index"
- >
- <div class="intimacy-card__top">
- <div class="intimacy-card__head">
- <PersonAvatar class="relation-avatar" :size="42" :alt="relation.otherName" />
- <div class="intimacy-card__name" :title="relation.otherName">{{ relation.otherName }}</div>
- </div>
- <div class="intimacy-card__score-wrap">
- <div class="intimacy-card__score-badge">{{ relation.intimacyText }}</div>
- <div class="intimacy-card__score">亲密度</div>
- </div>
- </div>
- <div class="intimacy-card__tags">
- <span
- v-for="behavior in relation.behaviors"
- :key="behavior.key"
- class="behavior-tag"
- :style="getTagStyle(behavior)"
- >
- {{ getIntimacyBehaviorDisplayName(behavior.name) }}
- </span>
- </div>
- </button>
- </div>
- <Button v-if="showNextButton" class="scroll-btn scroll-btn--next" @click="scrollCards('right')">
- >
- </Button>
- </div>
- </div>
- <div class="intimacy-detail">
- <div class="intimacy-detail__title-row">
- <div class="intimacy-detail__title">亲密度详情</div>
- <div class="intimacy-detail__summary-inline">
- <div class="summary-item">
- <span class="label">关系人:</span>
- <span class="value">{{ activeRelation?.otherName || '-' }}</span>
- </div>
- <div class="summary-item">
- <span class="label">亲密度:</span>
- <span class="value value--accent">{{ activeRelation?.intimacyText || '-' }}</span>
- </div>
- <div class="intimacy-detail__meta">
- {{ activeRelation ? `命中 ${activeRelation.details.length} 项行为` : '未选择关系人' }}
- </div>
- </div>
- </div>
- <div v-if="activeRelation?.details.length" class="intimacy-detail__list">
- <div v-for="detail in activeRelation.details" :key="detail.key" class="detail-item">
- <span class="detail-dot" :style="{ backgroundColor: detail.color }"></span>
- <div class="detail-main">
- <div class="detail-title">
- {{ detail.title }}
- <span v-if="detail.weightText" class="detail-weight">(权重{{ detail.weightText }})</span>
- </div>
- <div class="detail-desc">
- <span
- v-for="(seg, segIndex) in parseDescriptionSegments(detail.description)"
- :key="segIndex"
- :class="{ 'detail-value--clickable': seg.clickable }"
- @click="seg.clickable && handleJumpToAnalysis(detail)"
- >{{ seg.text }}</span
- >
- </div>
- </div>
- </div>
- </div>
- <div v-else class="detail-empty">
- <EmptyState title="暂无数据" description="当前关系暂无行为详情" variant="compact" :min-height="160" />
- </div>
- </div>
- </div>
- <div v-else class="intimacy-empty">
- <EmptyState
- :title="selectedPerson ? '暂无亲密关系人' : '请先选择对象'"
- :description="selectedPerson ? '当前对象暂无亲密度分析结果' : '选择左侧对象后可查看亲密关系人与行为详情'"
- />
- </div>
- </Spin>
- <BasicModal
- :open="configModalOpen"
- title="亲密度配置"
- :confirmLoading="configSaving"
- :width="920"
- :centered="true"
- :destroyOnClose="false"
- okText="应用配置"
- wrapClassName="intimacy-config-modal"
- @ok="handleSaveConfig"
- @update:open="handleConfigModalChange"
- @cancel="closeConfigModal"
- >
- <div class="config-modal">
- <div class="config-modal__summary" :class="{ 'is-error': configTotalExceeded }">
- <div class="config-modal__summary-main">
- <div class="config-modal__summary-label">权重总和</div>
- <div class="config-modal__summary-value"> {{ configTotalWeight }}<span class="unit">%</span> </div>
- <div class="config-modal__summary-tip">支持同时调整权重和分值,权重总和不能超过 100。</div>
- </div>
- <div class="config-modal__summary-extra">
- <div class="config-modal__remaining">
- {{ configTotalExceeded ? `已超出 ${configExceededWeight}%` : `剩余 ${configRemainingWeight}%` }}
- </div>
- <Button size="small" @click="handleReloadConfigs">刷新配置</Button>
- </div>
- </div>
- <div v-if="editableIntimacyConfigs.length" class="config-modal__list">
- <div
- v-for="item in editableIntimacyConfigs"
- :key="item.rowKey"
- class="config-item"
- :class="{ 'is-invalid': isConfigItemInvalid(item) }"
- >
- <div class="config-item__main">
- <div class="config-item__tags">
- <span v-for="tag in item.tagList" :key="tag" class="config-item__tag">{{ tag }}</span>
- <span v-if="!item.tagList.length" class="config-item__tag config-item__tag--empty">未配置标签</span>
- </div>
- <div v-if="item.scoreItems.length" class="config-item__score-editor">
- <div v-for="score in item.scoreItems" :key="score.key" class="score-editor-item">
- <span class="score-editor-item__label">{{ score.label }}:</span>
- <InputNumber
- v-model:value="score.value"
- class="score-editor-item__input"
- :min="0"
- :precision="0"
- placeholder="请输入分值"
- />
- </div>
- </div>
- <div v-else class="config-item__score-empty">暂无分数配置</div>
- </div>
- <div class="config-item__weight">
- <span class="config-item__weight-label">权重</span>
- <InputNumber
- v-model:value="item.weightValue"
- class="config-item__weight-input"
- :min="0"
- :max="100"
- :precision="0"
- placeholder="0-100"
- />
- <span class="config-item__weight-unit">%</span>
- </div>
- </div>
- </div>
- <div v-else class="config-modal__empty">
- <EmptyState
- title="暂无配置"
- description="亲密度配置接入后可在此调整权重"
- variant="compact"
- :min-height="220"
- />
- </div>
- </div>
- </BasicModal>
- </div>
- </template>
- <script lang="ts" setup>
- import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
- import { Button, Input, InputNumber, Spin } from 'ant-design-vue';
- import { EmptyState } from '@jeesite/core/components/EmptyState';
- import { BasicModal } from '@jeesite/core/components/Modal';
- import { PersonAvatar } from '@jeesite/core/components/PersonAvatar';
- import { useMessage } from '@jeesite/core/hooks/web/useMessage';
- import { useUserStoreWithOut } from '@jeesite/core/store/modules/case';
- import { getIntimacySummary } from '../../../../person/api/dpApi';
- import { listIntimacy, modifyIntimacy } from '../../../../person/api/personApi';
- import type { IntimacyConfigDTO, IntimacySummaryDTO, PersonBasicInfo } from '../../../../person/api/types/dto';
- interface BehaviorPalette {
- color: string;
- bgColor: string;
- borderColor: string;
- }
- interface BehaviorViewItem extends BehaviorPalette {
- key: string;
- name: string;
- weight?: number;
- weightText?: string;
- }
- interface DetailViewItem {
- key: string;
- behaviorName: string;
- title: string;
- description: string;
- weightText: string;
- color: string;
- detailRecord: Record<string, unknown>;
- }
- interface RelationViewItem {
- id: number | string;
- otherName: string;
- avatar: string;
- intimacyText: string;
- behaviors: BehaviorViewItem[];
- details: DetailViewItem[];
- }
- interface EditableIntimacyConfigItem extends IntimacyConfigDTO {
- rowKey: string;
- tagList: string[];
- weightValue?: number;
- scoreItems: EditableScoreItem[];
- }
- interface EditableScoreItem {
- key: string;
- label: string;
- value?: number;
- }
- const props = defineProps<{
- selectedPerson?: PersonBasicInfo | null;
- }>();
- const emit = defineEmits<{
- (e: 'jump-to-analysis', payload: JumpToAnalysisPayload): void;
- }>();
- interface JumpToAnalysisPayload extends Record<string, unknown> {
- personName: string;
- otherName: string;
- behaviorType: string;
- detailRecord: Record<string, unknown>;
- path: string;
- query: Record<string, string>;
- }
- const userStore = useUserStoreWithOut();
- const { createMessage } = useMessage();
- const keyword = ref('');
- const loading = ref(false);
- const MAX_INTIMACY_RELATIONS = 100;
- const activeIndex = ref(0);
- const cardListRef = ref<HTMLElement | null>(null);
- const showPrevButton = ref(false);
- const showNextButton = ref(false);
- const intimacyConfigs = ref<IntimacyConfigDTO[]>([]);
- const intimacySummaryRows = ref<IntimacySummaryDTO[]>([]);
- const configModalOpen = ref(false);
- const configSaving = ref(false);
- const editableIntimacyConfigs = ref<EditableIntimacyConfigItem[]>([]);
- const defaultPalette: BehaviorPalette = {
- color: '#1677ff',
- bgColor: '#e6f4ff',
- borderColor: '#bfdbfe',
- };
- const behaviorPaletteMap: Record<string, BehaviorPalette> = {
- 大额转账: { color: '#1677ff', bgColor: '#e6f4ff', borderColor: '#bfdbfe' },
- 基站碰面: { color: '#ef4444', bgColor: '#fee2e2', borderColor: '#fecaca' },
- 特殊日期特殊转账: { color: '#6366f1', bgColor: '#e0e7ff', borderColor: '#c7d2fe' },
- 频繁交易: { color: '#475569', bgColor: '#e2e8f0', borderColor: '#cbd5e1' },
- 酒店同住: { color: '#16a34a', bgColor: '#dcfce7', borderColor: '#bbf7d0' },
- 特殊日期通话: { color: '#d97706', bgColor: '#fef3c7', borderColor: '#fde68a' },
- 异卡同号: { color: '#a16207', bgColor: '#fef9c3', borderColor: '#fde047' },
- 联系次数: { color: '#b91c1c', bgColor: '#fee2e2', borderColor: '#fecaca' },
- 同交通工具出行: { color: '#0f766e', bgColor: '#ccfbf1', borderColor: '#99f6e4' },
- 关联企业: { color: '#ea580c', bgColor: '#ffedd5', borderColor: '#fed7aa' },
- 夜间通话次数: { color: '#7c3aed', bgColor: '#ede9fe', borderColor: '#ddd6fe' },
- };
- const intimacyBehaviorDisplayNameMap: Record<string, string> = {
- 特殊日期特殊转账: '敏感日期特殊转账',
- 特殊日期通话: '敏感日期通话',
- };
- const getIntimacyBehaviorDisplayName = (name: string) => intimacyBehaviorDisplayNameMap[name] || name;
- const scoreLabelTemplateMap: Record<string, string[]> = {
- 大额转账: ['大于等于5万', '1万到5万', '小于1万'],
- 频繁交易: ['交易次数top1~5', '交易次数top6~10', '交易次数top11~top20', '交易次数大于top20'],
- };
- // 行为类型到分析页面的跳转映射
- const BEHAVIOR_JUMP_MAP: Record<string, { path: string; query: Record<string, string> }> = {
- 大额转账: {
- path: '/largeAmount/index',
- query: {
- countOp: 'GE',
- countValue: '1',
- amountOp: 'GE',
- amountValue: '10000',
- },
- },
- 频繁交易: {
- path: '/tradeFrequency/index',
- query: {},
- },
- 基站碰面: {
- path: '/coResidence/index',
- query: {},
- },
- 特殊日期特殊转账: {
- path: '/transSensitive/index',
- query: {},
- },
- 敏感日期特殊转账: {
- path: '/transSensitive/index',
- query: {},
- },
- 敏感日期交易: {
- path: '/transSensitive/index',
- query: {},
- },
- 特殊日期交易: {
- path: '/transSensitive/index',
- query: {},
- },
- 酒店同住: {
- path: '/coResidence/index',
- query: {},
- },
- 特殊日期通话: {
- path: '/sensitive/index',
- query: {},
- },
- 敏感日期通话: {
- path: '/sensitive/index',
- query: {},
- },
- 联系次数: {
- path: '/continuous/index',
- query: {
- monthValue: '1',
- },
- },
- 同交通工具出行: {
- path: '/peerAnalysis/index',
- query: {},
- },
- 夜间通话次数: {
- path: '/night/index',
- query: {},
- },
- 异卡同号: {
- path: '/cardHolding/index',
- query: {},
- },
- 关联企业: {
- path: '/objectRelation/index',
- query: {},
- },
- };
- /** 将详情描述中的数字提取为可点击的片段 */
- const parseDescriptionSegments = (
- description: string,
- ): Array<{ text: string; clickable: boolean; value: string }> => {
- const segments: Array<{ text: string; clickable: boolean; value: string }> = [];
- const regex = /(\d+(?:\.\d+)?)/g;
- let lastIndex = 0;
- let match: RegExpExecArray | null;
- while ((match = regex.exec(description)) !== null) {
- if (match.index > lastIndex) {
- segments.push({ text: description.slice(lastIndex, match.index), clickable: false, value: '' });
- }
- segments.push({ text: match[0], clickable: true, value: match[0] });
- lastIndex = match.index + match[0].length;
- }
- if (lastIndex < description.length) {
- segments.push({ text: description.slice(lastIndex), clickable: false, value: '' });
- }
- return segments;
- };
- const handleJumpToAnalysis = (detail: DetailViewItem) => {
- const personName = selectedPersonName.value;
- const otherName = activeRelation.value?.otherName || '';
- const behaviorType = detail.title;
- const behaviorName = detail.behaviorName || detail.title;
- const jumpConfig = BEHAVIOR_JUMP_MAP[behaviorName] || BEHAVIOR_JUMP_MAP[behaviorType];
- if (!personName && !otherName) {
- createMessage.warning('无可跳转的对象信息');
- return;
- }
- if (!jumpConfig?.path) {
- createMessage.warning('当前行为暂未配置分析页跳转');
- return;
- }
- const detailRecord = detail.detailRecord || {};
- const query = { ...jumpConfig.query };
- if (behaviorType === '同交通工具出行') {
- const personNames = [personName, otherName].map((item) => item.trim()).filter(Boolean);
- if (personNames.length) {
- query.personNames = personNames.join(',');
- }
- if (otherName) {
- query.companionName = otherName;
- }
- }
- emit('jump-to-analysis', {
- personName,
- otherName,
- behaviorType,
- detailRecord,
- path: jumpConfig.path,
- query,
- });
- };
- let resizeObserver: ResizeObserver | null = null;
- let summaryRequestId = 0;
- const caseId = computed(() => userStore.getCaseInfo?.id as number | string | undefined);
- const selectedPerson = computed(() => props.selectedPerson || null);
- const selectedObjId = computed(() => selectedPerson.value?.objId ?? selectedPerson.value?.id);
- const selectedPersonName = computed(() => String(selectedPerson.value?.personName || '').trim());
- const normalizeArrayResponse = <T,>(value: any): T[] => {
- if (Array.isArray(value)) return value as T[];
- const candidates = [
- value?.records,
- value?.list,
- value?.data,
- value?.data?.records,
- value?.data?.list,
- value?.data?.data,
- value?.result,
- ];
- for (const item of candidates) {
- if (Array.isArray(item)) return item as T[];
- }
- return [];
- };
- const toDisplayText = (value: unknown) => {
- if (value === 0) return '0';
- if (value === null || value === undefined || value === '') return '-';
- return String(value);
- };
- const toNumericValue = (value: unknown) => {
- if (value === null || value === undefined || value === '') return null;
- const numeric = Number(
- String(value)
- .replace(/[,%\s]/g, '')
- .trim(),
- );
- return Number.isFinite(numeric) ? numeric : null;
- };
- const formatMetric = (value: unknown, unit = '') => {
- const text = toDisplayText(value);
- return text === '-' ? '-' : `${text}${unit}`;
- };
- const parseBehaviorItems = (value: unknown) => {
- const source = Array.isArray(value) ? value.join(',') : String(value || '');
- const itemMap = new Map<string, { name: string; weight?: number; weightText?: string }>();
- source
- .split(/[,,]/)
- .map((item) => String(item || '').trim())
- .filter(Boolean)
- .forEach((item) => {
- const name = item.split('-')[0]?.trim();
- if (!name) return;
- const weightMatch = item.match(/权重\s*(\d+(?:\.\d+)?)\s*%?/);
- const weightValue = weightMatch ? toNumericValue(weightMatch[1]) : null;
- itemMap.set(name, {
- name,
- weight: weightValue === null ? undefined : weightValue,
- weightText: weightValue === null ? '' : `${weightValue}%`,
- });
- });
- return Array.from(itemMap.values());
- };
- const parseConfigTags = (value: unknown) => {
- return String(value || '')
- .split(/[,,、]/)
- .map((item) => item.trim())
- .filter(Boolean);
- };
- const buildDefaultScoreLabels = (tagList: string[], scoreCount: number) => {
- const primaryTag = tagList[0] || '';
- const template = scoreLabelTemplateMap[primaryTag];
- if (template?.length) {
- return template;
- }
- const count = Math.max(scoreCount, 1);
- return Array.from({ length: count }, (_, index) => `分值${index + 1}`);
- };
- const cleanScoreLabel = (value: string) => {
- return value
- .replace(/^[\s{[\("'“”、,,、::]+/, '')
- .replace(/[\s}\]"'“”、,,、::]+$/, '')
- .trim();
- };
- const parseScoreItems = (value: unknown, tagList: string[], rowKey: string): EditableScoreItem[] => {
- const source = String(value || '').trim();
- if (!source) {
- return buildDefaultScoreLabels(tagList, 0).map((label, index) => ({
- key: `${rowKey}-score-${index}`,
- label,
- value: undefined,
- }));
- }
- const pairEntries = Array.from(source.matchAll(/["“]?([^"“”:=:{}]+?)["”]?\s*[::=]\s*(-?\d+(?:\.\d+)?)/g))
- .map((match) => ({
- label: cleanScoreLabel(match[1] || ''),
- value: toNumericValue(match[2]),
- }))
- .filter((item) => item.label);
- if (pairEntries.length) {
- return pairEntries.map((item, index) => ({
- key: `${rowKey}-score-${index}`,
- label: item.label,
- value: item.value === null ? undefined : item.value,
- }));
- }
- const numericValues = (source.match(/-?\d+(?:\.\d+)?/g) || [])
- .map((item) => toNumericValue(item))
- .filter((item): item is number => item !== null);
- const labels = buildDefaultScoreLabels(tagList, numericValues.length);
- return labels.map((label, index) => ({
- key: `${rowKey}-score-${index}`,
- label,
- value: numericValues[index],
- }));
- };
- const stringifyScoreItems = (scoreItems: EditableScoreItem[]) => {
- if (!scoreItems.length) return '';
- const scoreMap = scoreItems.reduce<Record<string, number>>((result, item) => {
- const label = cleanScoreLabel(item.label);
- if (!label) return result;
- result[label] = Number(item.value ?? 0);
- return result;
- }, {});
- return Object.keys(scoreMap).length ? JSON.stringify(scoreMap) : '';
- };
- const parseDetailRecord = (value: unknown): Record<string, unknown> => {
- if (!value) return {};
- if (typeof value === 'string') {
- try {
- const parsed = JSON.parse(value);
- return parsed && typeof parsed === 'object' ? parsed : {};
- } catch {
- return {};
- }
- }
- return typeof value === 'object' ? (value as Record<string, unknown>) : {};
- };
- const buildDetailDescription = (name: string, detail: Record<string, unknown>) => {
- switch (name) {
- case '大额转账':
- return `大额交易次数 ${formatMetric(detail['大额交易次数'], '次')}`;
- case '基站碰面':
- return `碰面 ${formatMetric(detail['碰面次数'], '次')},碰面地点 ${formatMetric(detail['碰面地点次数'], '个')}`;
- case '特殊日期特殊转账':
- return `敏感日期特殊转账 ${formatMetric(detail['特殊日期特殊转账'], '次')}`;
- case '频繁交易':
- return `相互交易 ${formatMetric(detail['交易次数'], '次')}`;
- case '酒店同住':
- return `同住 ${formatMetric(detail['同住次数'], '次')}`;
- case '特殊日期通话':
- return `敏感日期联系 ${formatMetric(detail['特殊日期通话次数'], '次')}`;
- case '异卡同号': {
- const count = toDisplayText(detail['异卡同号']);
- return count === '-' ? '存在异卡同号关联' : `有 ${count} 张卡绑定分析对象号码`;
- }
- case '联系次数':
- return `相互联系 ${formatMetric(detail['联系次数'], '次')}`;
- case '同交通工具出行':
- return `同行 ${formatMetric(detail['同行次数'], '次')}`;
- case '关联企业':
- return `共同关联企业 ${formatMetric(detail['共同关联企业'], '家')}`;
- case '夜间通话次数':
- return `夜间通话 ${formatMetric(detail['夜间通话次数'], '次')}`;
- default:
- return '关联指标已命中,详细数据以接口返回为准';
- }
- };
- const intimacyWeightMap = computed(() => {
- const map = new Map<string, number>();
- intimacyConfigs.value.forEach((item) => {
- const weight = toNumericValue(item.weight);
- if (weight === null) return;
- parseConfigTags(item.tags).forEach((tag) => {
- if (!map.has(tag)) {
- map.set(tag, weight);
- }
- });
- });
- return map;
- });
- const configTotalWeight = computed(() =>
- editableIntimacyConfigs.value.reduce((sum, item) => sum + (Number(item.weightValue) || 0), 0),
- );
- const configTotalExceeded = computed(() => configTotalWeight.value > 100);
- const configRemainingWeight = computed(() => Math.max(0, 100 - configTotalWeight.value));
- const configExceededWeight = computed(() => Math.max(0, configTotalWeight.value - 100));
- const relationList = computed<RelationViewItem[]>(() => {
- return intimacySummaryRows.value
- .filter((item) => (toNumericValue(item.intimacy) ?? 0) > 0)
- .map((item, rowIndex) => {
- const detailRecord = parseDetailRecord(item.detail);
- const behaviorItems = parseBehaviorItems(item.intimacyBehavior);
- const relationId =
- (item as IntimacySummaryDTO & { id?: number | string }).id ??
- `${selectedObjId.value || 'relation'}-${rowIndex}`;
- const behaviors = behaviorItems.map((behaviorItem, behaviorIndex) => {
- const name = behaviorItem.name;
- const palette = behaviorPaletteMap[name] || defaultPalette;
- const configWeight = intimacyWeightMap.value.get(name);
- const weight = behaviorItem.weight ?? configWeight;
- return {
- key: `${relationId}-${behaviorIndex}`,
- name,
- weight,
- weightText: behaviorItem.weightText || (weight === undefined ? '' : `${weight}%`),
- ...palette,
- };
- });
- const details = behaviors.map((behavior) => ({
- key: `${relationId}-${behavior.name}`,
- behaviorName: behavior.name,
- title: getIntimacyBehaviorDisplayName(behavior.name),
- description: buildDetailDescription(behavior.name, detailRecord),
- weightText: behavior.weightText || (behavior.weight === undefined ? '' : `${behavior.weight}%`),
- color: behavior.color,
- detailRecord,
- }));
- const otherName = String(item.otherName || `关系人${rowIndex + 1}`);
- return {
- id: relationId,
- otherName,
- avatar: otherName.slice(0, 1) || '关',
- intimacyText: toDisplayText(item.intimacy),
- behaviors,
- details,
- };
- });
- });
- const filteredRelations = computed(() => {
- const searchValue = keyword.value.trim();
- if (!searchValue) return relationList.value;
- return relationList.value.filter((item) => item.otherName.includes(searchValue));
- });
- const activeRelation = computed(() => filteredRelations.value[activeIndex.value] || null);
- const getTagStyle = (behavior: BehaviorViewItem) => ({
- color: behavior.color,
- background: behavior.bgColor,
- borderColor: behavior.borderColor,
- });
- const buildEditableConfigRows = (list: IntimacyConfigDTO[]): EditableIntimacyConfigItem[] => {
- return list.map((item, index) => {
- const weight = toNumericValue(item.weight);
- const rowKey = String(item.id ?? index);
- const tagList = parseConfigTags(item.tags);
- return {
- ...item,
- rowKey,
- tagList,
- weightValue: weight === null ? undefined : weight,
- scoreItems: parseScoreItems(item.scoreJson ?? item.scores, tagList, rowKey),
- };
- });
- };
- const syncEditableConfigs = () => {
- editableIntimacyConfigs.value = buildEditableConfigRows(intimacyConfigs.value);
- };
- const isConfigItemInvalid = (item: EditableIntimacyConfigItem) => {
- if (item.weightValue === undefined || item.weightValue === null) return true;
- return item.weightValue < 0 || item.weightValue > 100;
- };
- const getInvalidScoreItem = (item: EditableIntimacyConfigItem) => {
- return (
- item.scoreItems.find((score) => score.value === undefined || score.value === null || score.value < 0) || null
- );
- };
- const handleOpenConfigModal = async () => {
- if (!intimacyConfigs.value.length) {
- await loadIntimacyConfigs();
- }
- syncEditableConfigs();
- configModalOpen.value = true;
- };
- const closeConfigModal = () => {
- configModalOpen.value = false;
- };
- const handleConfigModalChange = (open: boolean) => {
- configModalOpen.value = open;
- if (!open) {
- syncEditableConfigs();
- }
- };
- const handleReloadConfigs = async () => {
- await loadIntimacyConfigs(true);
- syncEditableConfigs();
- };
- const validateEditableConfigs = () => {
- const invalidItem = editableIntimacyConfigs.value.find((item) => isConfigItemInvalid(item));
- if (invalidItem) {
- createMessage.warning(`请填写有效权重:${invalidItem.tagList.join(' / ') || '未配置标签'}`);
- return false;
- }
- const invalidScoreConfig = editableIntimacyConfigs.value.find((item) => !!getInvalidScoreItem(item));
- if (invalidScoreConfig) {
- const invalidScoreItem = getInvalidScoreItem(invalidScoreConfig);
- createMessage.warning(
- `请填写有效分值:${invalidScoreConfig.tagList.join(' / ') || '未配置标签'} - ${invalidScoreItem?.label || '分值'}`,
- );
- return false;
- }
- if (configTotalExceeded.value) {
- createMessage.warning('亲密度权重总和不能超过 100');
- return false;
- }
- return true;
- };
- const handleSaveConfig = async () => {
- if (configSaving.value) return;
- if (!validateEditableConfigs()) return;
- configSaving.value = true;
- try {
- const payload = editableIntimacyConfigs.value.map((item) => {
- return {
- id: item.id,
- tags: item.tags,
- scores: stringifyScoreItems(item.scoreItems),
- weight: Number(item.weightValue ?? 0),
- };
- });
- await modifyIntimacy(payload);
- intimacyConfigs.value = payload;
- await loadIntimacyConfigs();
- syncEditableConfigs();
- createMessage.success('亲密度配置保存成功');
- configModalOpen.value = false;
- } catch (error) {
- console.error('保存亲密度配置失败', error);
- createMessage.warning('亲密度配置保存失败,请稍后重试');
- } finally {
- configSaving.value = false;
- }
- };
- const updateScrollButtons = () => {
- const element = cardListRef.value;
- if (!element) {
- showPrevButton.value = false;
- showNextButton.value = false;
- return;
- }
- const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth);
- showPrevButton.value = element.scrollLeft > 4;
- showNextButton.value = maxScrollLeft - element.scrollLeft > 4;
- };
- const scheduleScrollStateUpdate = () => {
- nextTick(() => {
- requestAnimationFrame(() => {
- updateScrollButtons();
- });
- });
- };
- const scrollCards = (direction: 'left' | 'right') => {
- const element = cardListRef.value;
- if (!element) return;
- const offset = direction === 'left' ? -320 : 320;
- element.scrollBy({ left: offset, behavior: 'smooth' });
- window.setTimeout(updateScrollButtons, 220);
- };
- const bindResizeObserver = () => {
- if (typeof ResizeObserver === 'undefined') return;
- resizeObserver?.disconnect();
- resizeObserver = new ResizeObserver(() => {
- scheduleScrollStateUpdate();
- });
- if (cardListRef.value) {
- resizeObserver.observe(cardListRef.value);
- }
- };
- const loadIntimacyConfigs = async (showSuccessMessage = false) => {
- try {
- intimacyConfigs.value = normalizeArrayResponse<IntimacyConfigDTO>(await listIntimacy());
- if (showSuccessMessage) {
- createMessage.success('亲密度配置已刷新');
- }
- } catch (error) {
- console.error('获取亲密度配置失败', error);
- intimacyConfigs.value = [];
- if (showSuccessMessage) {
- createMessage.warning('亲密度配置刷新失败');
- }
- }
- };
- const loadIntimacySummary = async () => {
- const currentRequestId = ++summaryRequestId;
- const objId = selectedObjId.value;
- const personName = selectedPersonName.value;
- if (!objId && !personName) {
- intimacySummaryRows.value = [];
- loading.value = false;
- return;
- }
- loading.value = true;
- try {
- const response = await getIntimacySummary({
- personName: personName || undefined,
- });
- if (currentRequestId !== summaryRequestId) return;
- intimacySummaryRows.value = normalizeArrayResponse<IntimacySummaryDTO>(response).slice(0, MAX_INTIMACY_RELATIONS);
- } catch (error) {
- if (currentRequestId !== summaryRequestId) return;
- console.error('获取亲密关系人失败', error);
- intimacySummaryRows.value = [];
- } finally {
- if (currentRequestId !== summaryRequestId) return;
- loading.value = false;
- scheduleScrollStateUpdate();
- }
- };
- watch(
- () => [selectedObjId.value, selectedPersonName.value, caseId.value],
- async () => {
- keyword.value = '';
- activeIndex.value = 0;
- await loadIntimacySummary();
- },
- { immediate: true },
- );
- watch(
- filteredRelations,
- (list) => {
- if (!list.length) {
- activeIndex.value = 0;
- } else if (activeIndex.value >= list.length) {
- activeIndex.value = 0;
- }
- scheduleScrollStateUpdate();
- },
- { deep: true },
- );
- watch(
- () => cardListRef.value,
- () => {
- bindResizeObserver();
- scheduleScrollStateUpdate();
- },
- );
- onMounted(() => {
- void loadIntimacyConfigs();
- bindResizeObserver();
- scheduleScrollStateUpdate();
- });
- onBeforeUnmount(() => {
- resizeObserver?.disconnect();
- resizeObserver = null;
- });
- </script>
- <style lang="less" scoped>
- .intimacy-panel {
- --intimacy-content-height: 248px;
- display: flex;
- flex-direction: column;
- gap: 12px;
- min-height: 0;
- min-width: 0;
- overflow: visible;
- box-sizing: border-box;
- :deep(.ant-spin-nested-loading),
- :deep(.ant-spin-container) {
- display: flex;
- flex: 1 1 auto;
- flex-direction: column;
- min-height: 0;
- }
- .section-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px 16px;
- margin-bottom: 0;
- padding-bottom: 10px;
- border-bottom: 1px solid #eef2f7;
- }
- .section-title {
- position: relative;
- display: inline-flex;
- align-items: center;
- gap: 8px;
- font-size: 15px;
- font-weight: 600;
- color: #111827;
- &::before {
- content: '';
- width: 3px;
- height: 14px;
- border-radius: 999px;
- background: #1677ff;
- }
- }
- .section-tools {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-left: auto;
- }
- .relation-search {
- width: 200px;
- max-width: 100%;
- }
- .intimacy-content {
- width: 100%;
- max-width: 100%;
- height: var(--intimacy-content-height);
- display: grid;
- grid-template-columns: minmax(0, 1fr) minmax(320px, 36%);
- grid-template-rows: minmax(0, 1fr);
- gap: 2px;
- grid-auto-rows: minmax(0, 1fr);
- min-height: 0;
- align-items: stretch;
- overflow: hidden;
- padding: 2px;
- background: #f3f4f6;
- box-sizing: border-box;
- }
- .intimacy-rail {
- min-width: 0;
- width: 100%;
- max-width: 100%;
- height: 100%;
- min-height: 0;
- display: flex;
- flex-direction: column;
- gap: 0;
- }
- .intimacy-left-wrap {
- position: relative;
- width: 100%;
- max-width: 100%;
- min-width: 0;
- flex: 1 1 auto;
- height: 100%;
- min-height: 0;
- display: flex;
- align-self: stretch;
- align-items: stretch;
- border-radius: 12px;
- background: #fff;
- border: 1px solid #e2e8f0;
- overflow: hidden;
- box-sizing: border-box;
- box-shadow: none;
- }
- .intimacy-card-list {
- flex: 1 1 auto;
- width: 100%;
- max-width: 100%;
- height: 100%;
- min-width: 0;
- min-height: 0;
- display: flex;
- align-items: stretch;
- gap: 8px;
- padding: 10px 36px 18px;
- overflow: auto hidden;
- scroll-behavior: smooth;
- box-sizing: border-box;
- &::-webkit-scrollbar {
- height: 6px;
- }
- &::-webkit-scrollbar-thumb {
- border-radius: 999px;
- background: #cbd5e1;
- }
- &::-webkit-scrollbar-track {
- background: transparent;
- }
- }
- .intimacy-card {
- flex: 0 0 244px;
- align-self: stretch;
- height: auto;
- min-height: 0;
- max-height: none;
- padding: 10px 12px;
- border: 1px solid #e5e7eb;
- border-radius: 10px;
- background: #fff;
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- gap: 12px;
- text-align: left;
- cursor: pointer;
- box-sizing: border-box;
- transition:
- border-color 0.2s ease,
- box-shadow 0.2s ease,
- background 0.2s ease;
- &:hover {
- border-color: #91caff;
- box-shadow: none;
- transform: none;
- }
- &.active {
- border-color: #1677ff;
- background: rgba(247, 251, 255, 0.72);
- box-shadow: none;
- }
- }
- .intimacy-card__top {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 12px;
- }
- .intimacy-card__head {
- display: flex;
- align-items: center;
- gap: 12px;
- min-width: 0;
- flex: 1 1 auto;
- }
- .relation-avatar {
- flex: 0 0 42px;
- display: inline-flex;
- }
- .intimacy-card__name {
- min-width: 0;
- font-size: 15px;
- font-weight: 600;
- line-height: 22px;
- color: #111827;
- word-break: break-all;
- }
- .intimacy-card__score-wrap {
- flex: 0 0 auto;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 8px;
- }
- .intimacy-card__score-badge {
- min-width: 42px;
- height: 42px;
- padding: 0 10px;
- border-radius: 12px;
- background: linear-gradient(180deg, #1d4ed8 0%, #2563eb 100%);
- color: #fff;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- font-size: 18px;
- line-height: 1;
- font-weight: 700;
- box-shadow: none;
- }
- .intimacy-card__score {
- font-size: 12px;
- line-height: 18px;
- color: #64748b;
- text-align: center;
- white-space: nowrap;
- }
- .intimacy-card__tags {
- display: grid;
- grid-auto-flow: column;
- grid-template-rows: repeat(2, max-content);
- grid-auto-columns: max-content;
- align-content: end;
- gap: 6px;
- margin-top: auto;
- max-height: 58px;
- overflow: visible;
- }
- .behavior-tag {
- display: inline-flex;
- align-items: center;
- min-height: 26px;
- padding: 0 10px;
- border-radius: 999px;
- border: 1px solid transparent;
- font-size: 11px;
- line-height: 1.1;
- font-weight: 500;
- white-space: nowrap;
- }
- .scroll-btn {
- position: absolute;
- top: 50%;
- z-index: 2;
- width: 30px;
- height: 30px;
- padding: 0;
- border-radius: 999px;
- border-color: #dbe2ea;
- background: rgb(255 255 255 / 96%);
- transform: translateY(-50%);
- box-shadow: none;
- &.scroll-btn--prev {
- left: 8px;
- }
- &.scroll-btn--next {
- right: 8px;
- }
- }
- .intimacy-detail {
- width: 100%;
- max-width: 100%;
- flex: 1 1 auto;
- min-width: 0;
- height: 100%;
- min-height: 0;
- max-height: 100%;
- display: flex;
- flex-direction: column;
- align-self: stretch;
- padding: 10px 12px;
- border: 1px solid #e2e8f0;
- border-radius: 12px;
- background: #fff;
- overflow: hidden;
- box-sizing: border-box;
- box-shadow: none;
- }
- .intimacy-detail__title-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- min-width: 0;
- padding-bottom: 8px;
- border-bottom: 1px solid #eef2f7;
- }
- .intimacy-detail__title {
- flex: 0 0 auto;
- font-size: 15px;
- font-weight: 600;
- line-height: 22px;
- color: #111827;
- }
- .intimacy-detail__summary-inline {
- display: flex;
- flex: 1 1 auto;
- justify-content: flex-end;
- align-items: center;
- gap: 16px;
- min-width: 0;
- flex-wrap: wrap;
- }
- .intimacy-detail__meta {
- flex-shrink: 0;
- font-size: 12px;
- line-height: 20px;
- color: #94a3b8;
- }
- .summary-item {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- font-size: 13px;
- min-width: 0;
- }
- .label {
- flex: 0 0 auto;
- color: #6b7280;
- white-space: nowrap;
- }
- .value {
- color: #111827;
- font-weight: 500;
- word-break: break-all;
- }
- .value--accent {
- color: #1677ff;
- font-weight: 600;
- }
- .intimacy-detail__list {
- flex: 1 1 auto;
- min-height: 0;
- display: flex;
- flex-wrap: wrap;
- align-content: flex-start;
- gap: 8px;
- margin-top: 6px;
- padding-right: 4px;
- padding-bottom: 4px;
- overflow-y: auto;
- overflow-x: hidden;
- overscroll-behavior: contain;
- }
- .detail-item {
- display: flex;
- align-items: flex-start;
- gap: 10px;
- flex: 1 1 calc(50% - 4px);
- min-width: 220px;
- padding: 8px 12px;
- border-radius: 10px;
- background: #fff;
- border: 1px solid #eef2f7;
- box-sizing: border-box;
- }
- .detail-dot {
- width: 8px;
- height: 8px;
- margin-top: 6px;
- border-radius: 999px;
- flex: 0 0 8px;
- }
- .detail-main {
- flex: 1 1 auto;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 5px;
- }
- .detail-title {
- font-size: 14px;
- font-weight: 600;
- line-height: 20px;
- color: #111827;
- word-break: break-all;
- }
- .detail-weight {
- font-size: 12px;
- font-weight: 500;
- color: #6b7280;
- }
- .detail-desc {
- font-size: 13px;
- line-height: 20px;
- color: #4b5563;
- }
- .detail-value--clickable {
- display: inline;
- padding: 0 2px;
- color: #1677ff;
- font-weight: 600;
- cursor: pointer;
- border-radius: 4px;
- transition: background-color 0.2s ease;
- &:hover {
- background: #e6f4ff;
- text-decoration: underline;
- }
- }
- .detail-empty,
- .intimacy-empty {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 0;
- min-height: 220px;
- border: 0;
- border-radius: 0;
- background: transparent;
- color: inherit;
- }
- .detail-empty {
- margin-top: 16px;
- min-height: 160px;
- }
- }
- .config-modal {
- display: flex;
- flex-direction: column;
- gap: 12px;
- min-height: 0;
- &__summary {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- padding: 10px 14px;
- border: 1px solid #dbeafe;
- border-radius: 12px;
- background: linear-gradient(180deg, #f8fbff 0%, #eff6ff 100%);
- &.is-error {
- border-color: #fecaca;
- background: linear-gradient(180deg, #fff7f7 0%, #fef2f2 100%);
- }
- }
- &__summary-main {
- min-width: 0;
- display: flex;
- flex: 1 1 auto;
- flex-wrap: wrap;
- align-items: baseline;
- gap: 6px 12px;
- }
- &__summary-label {
- font-size: 12px;
- line-height: 18px;
- color: #64748b;
- white-space: nowrap;
- }
- &__summary-value {
- font-size: 22px;
- line-height: 1;
- font-weight: 700;
- color: #1677ff;
- white-space: nowrap;
- .unit {
- margin-left: 2px;
- font-size: 14px;
- }
- }
- &__summary-tip {
- font-size: 12px;
- line-height: 18px;
- color: #6b7280;
- flex: 1 1 240px;
- }
- &__summary-extra {
- display: flex;
- flex-direction: row;
- align-items: flex-end;
- gap: 8px;
- flex-shrink: 0;
- }
- &__remaining {
- font-size: 12px;
- line-height: 18px;
- font-weight: 600;
- color: #111827;
- white-space: nowrap;
- }
- &__list {
- display: flex;
- flex-direction: column;
- gap: 12px;
- max-height: 460px;
- padding-right: 4px;
- overflow-y: auto;
- }
- &__empty {
- padding: 24px 0 8px;
- }
- }
- .config-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- padding: 16px 18px;
- border: 1px solid #e5e7eb;
- border-radius: 14px;
- background: #fff;
- transition:
- border-color 0.2s ease,
- box-shadow 0.2s ease;
- &.is-invalid {
- border-color: #fca5a5;
- box-shadow: inset 0 0 0 1px rgb(248 113 113 / 12%);
- }
- &__main {
- min-width: 0;
- flex: 1 1 auto;
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- &__tags {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- }
- &__score-editor {
- display: flex;
- flex-wrap: wrap;
- gap: 12px 20px;
- padding-top: 2px;
- }
- &__score-empty {
- font-size: 12px;
- line-height: 20px;
- color: #94a3b8;
- }
- &__tag {
- display: inline-flex;
- align-items: center;
- min-height: 28px;
- padding: 0 12px;
- border-radius: 999px;
- background: #eff6ff;
- border: 1px solid #bfdbfe;
- color: #1d4ed8;
- font-size: 12px;
- line-height: 1;
- font-weight: 500;
- &--empty {
- color: #94a3b8;
- background: #f8fafc;
- border-color: #e2e8f0;
- }
- }
- &__weight {
- flex: 0 0 176px;
- display: flex;
- align-items: center;
- justify-content: flex-end;
- gap: 8px;
- }
- &__weight-label {
- font-size: 13px;
- line-height: 20px;
- color: #6b7280;
- white-space: nowrap;
- }
- &__weight-input {
- width: 96px;
- }
- &__weight-unit {
- font-size: 13px;
- line-height: 20px;
- color: #111827;
- font-weight: 600;
- }
- }
- .score-editor-item {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- min-width: 0;
- &__label {
- font-size: 13px;
- line-height: 20px;
- color: #4b5563;
- white-space: nowrap;
- }
- &__input {
- width: 132px;
- }
- }
- :deep(.intimacy-config-modal) {
- .ant-modal-content {
- max-height: 720px;
- display: flex;
- flex-direction: column;
- }
- .ant-modal-header,
- .ant-modal-footer {
- flex: 0 0 auto;
- }
- .ant-modal-body {
- flex: 1 1 auto;
- min-height: 0;
- overflow: auto;
- padding: 16px;
- }
- }
- @media (max-width: 992px) {
- .intimacy-panel {
- --intimacy-content-height: auto;
- .section-header {
- flex-direction: column;
- align-items: stretch;
- }
- .section-tools,
- .relation-search {
- width: 100%;
- }
- .intimacy-content {
- height: auto;
- grid-template-columns: 1fr;
- grid-template-rows: auto auto;
- gap: 12px;
- grid-auto-rows: auto;
- }
- .intimacy-rail {
- min-height: 0;
- }
- .intimacy-left-wrap {
- flex: 0 0 auto;
- height: 248px;
- min-height: 248px;
- }
- .intimacy-detail {
- width: 100%;
- flex: 0 0 auto;
- height: 320px;
- min-height: 320px;
- }
- .intimacy-detail__title-row {
- align-items: flex-start;
- flex-direction: column;
- }
- .intimacy-detail__summary-inline {
- width: 100%;
- justify-content: flex-start;
- gap: 8px 16px;
- }
- .detail-item {
- flex-basis: 100%;
- min-width: 0;
- }
- .intimacy-card-list {
- padding-inline: 32px;
- }
- .intimacy-card {
- flex-basis: 220px;
- }
- }
- .config-modal {
- &__summary {
- flex-direction: column;
- align-items: stretch;
- }
- &__summary-extra {
- align-items: flex-start;
- }
- }
- .config-item {
- flex-direction: column;
- align-items: stretch;
- &__score-editor {
- flex-direction: column;
- }
- &__weight {
- flex: 0 0 auto;
- justify-content: flex-start;
- }
- }
- }
- </style>
|