提交 e3ee96f6 authored 作者: hsx's avatar hsx

feat:企业详情供应链关系

上级 b773299a
<svg viewBox="0 0 52 52" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="52.000000" height="52.000000" fill="none" customFrame="#000000">
<defs>
<g id="pixso_custom_effect_0">
<effect x="0.000000" y="0.000000" visibility="visible" fill="rgb(0,0,0)" fill-opacity="0.100000001" effectType="dropShadow" stdDeviation="8" radius="0" />
</g>
<filter id="filter_0" width="52.000000" height="52.000000" x="0.000000" y="0.000000" filterUnits="userSpaceOnUse" customEffect="url(#pixso_custom_effect_0)" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feOffset dx="0.000000" dy="0.000000" in="SourceAlpha" />
<feGaussianBlur stdDeviation="2.66666675" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0 " />
<feBlend result="effect_dropShadow_1" in2="BackgroundImageFix" mode="normal" />
<feBlend result="shape" in="SourceGraphic" in2="effect_dropShadow_1" mode="normal" />
</filter>
<clipPath id="clipPath_0">
<rect width="16.000000" height="16.000000" x="18.000000" y="18.000000" fill="rgb(255,255,255)" />
</clipPath>
</defs>
<rect id="普通企业节点" width="36.000000" height="36.000000" x="8.000000" y="8.000000" />
<g filter="url(#filter_0)">
<circle id="椭圆 25" cx="26" cy="26" r="18" fill="rgb(246,250,255)" />
<circle id="椭圆 25" cx="26" cy="26" r="17.5" stroke="rgb(231,243,255)" stroke-width="1.000000" />
</g>
<g id="企业 " clip-path="url(#clipPath_0)" customFrame="url(#clipPath_0)">
<rect id="企业 " width="16.000000" height="16.000000" x="18.000000" y="18.000000" />
<path id="矢量 370" d="M33.4667 31.9231L32.9333 31.9231L32.9333 24.3846C32.9333 23.2 31.9733 22.2308 30.8 22.2308L27.6 22.2308L27.6 21.1538C27.6 19.9692 26.64 19 25.4667 19L21.2 19C20.0267 19 19.0667 19.9692 19.0667 21.1538L19.0667 31.9231L18.5333 31.9231C18.2347 31.9231 18 32.16 18 32.4615C18 32.7631 18.2347 33 18.5333 33L33.4667 33C33.7653 33 34 32.7631 34 32.4615C34 32.16 33.7653 31.9231 33.4667 31.9231ZM26.5333 22.2308L26.5333 23.3077L26.5333 31.9231L20.1333 31.9231L20.1333 21.1538C20.1333 20.5615 20.6133 20.0769 21.2 20.0769L25.4667 20.0769C26.0533 20.0769 26.5333 20.5615 26.5333 21.1538L26.5333 22.2308ZM31.8667 31.9231L27.6 31.9231L27.6 23.3077L30.8 23.3077C31.3867 23.3077 31.8667 23.7923 31.8667 24.3846L31.8667 31.9231ZM24.9333 22.2308L21.7333 22.2308C21.4347 22.2308 21.2 22.4677 21.2 22.7692C21.2 23.0708 21.4347 23.3077 21.7333 23.3077L24.9333 23.3077C25.232 23.3077 25.4667 23.0708 25.4667 22.7692C25.4667 22.4677 25.232 22.2308 24.9333 22.2308ZM24.9333 25.4615L21.7333 25.4615C21.4347 25.4615 21.2 25.6985 21.2 26C21.2 26.3015 21.4347 26.5385 21.7333 26.5385L24.9333 26.5385C25.232 26.5385 25.4667 26.3015 25.4667 26C25.4667 25.6985 25.232 25.4615 24.9333 25.4615ZM24.9333 28.6923L21.7333 28.6923C21.4347 28.6923 21.2 28.9292 21.2 29.2308C21.2 29.5323 21.4347 29.7692 21.7333 29.7692L24.9333 29.7692C25.232 29.7692 25.4667 29.5323 25.4667 29.2308C25.4667 28.9292 25.232 28.6923 24.9333 28.6923ZM30.2667 25.4615L29.2 25.4615C28.9013 25.4615 28.6667 25.6985 28.6667 26C28.6667 26.3015 28.9013 26.5385 29.2 26.5385L30.2667 26.5385C30.5653 26.5385 30.8 26.3015 30.8 26C30.8 25.6985 30.5653 25.4615 30.2667 25.4615ZM30.2667 28.6923L29.2 28.6923C28.9013 28.6923 28.6667 28.9292 28.6667 29.2308C28.6667 29.5323 28.9013 29.7692 29.2 29.7692L30.2667 29.7692C30.5653 29.7692 30.8 29.5323 30.8 29.2308C30.8 28.9292 30.5653 28.6923 30.2667 28.6923Z" fill="rgb(5,95,194)" fill-rule="nonzero" />
</g>
</svg>
......@@ -165,3 +165,10 @@ export function getRelevantMeasures(params) {
// data,
// })
// }
export function getRuleOrg(params) {
return request({
method: 'POST',
url: `/api/organization/relate/ruleOrg`, data: params
})
}
\ No newline at end of file
......@@ -23,6 +23,7 @@
border-radius: 4px;
border: 1px solid var(--color-primary-10);
color: var(--color-primary-100);
margin: 10px;
}
.img {
......
<template>
<el-space :size="16" class="text-tip-1-bold box">
<div class="color-prefix"></div>
<slot></slot>
</el-space>
</template>
<script setup lang="ts">
import '@/styles/common.scss';
import { ElSpace } from 'element-plus';
const props = defineProps({
color: {
type: String,
default: 'var(--color-primary-100)'
}
})
</script>
<style lang="scss" scoped>
.color-prefix {
width: 8px;
height: 16px;
background-color: v-bind(color);
}
.box {
color: v-bind(color);
}
</style>
\ No newline at end of file
......@@ -4,7 +4,7 @@
border-radius: 10px;
border: 1px solid var(--bg-white-100);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.15);
}
.flex-display {
......@@ -39,13 +39,13 @@
/***文本样式***/
.text-base {
color: var(--color-primary-80);
color: var(--text-primary-80);
}
//0级标题
.text-title-0 {
@extend .text-base;
color: var(--color-primary-90);
color: var(--text-primary-90);
font-size: 32px;
}
......@@ -63,7 +63,7 @@
//1级标题
.text-title-1 {
@extend .text-base;
color: var(--color-primary-90);
color: var(--text-primary-90);
font-size: 24px;
}
......@@ -81,7 +81,7 @@
//2级标题
.text-title-2 {
@extend .text-base;
color: var(--color-primary-90);
color: var(--text-primary-90);
font-size: 20px;
line-height: 26px;
}
......@@ -101,7 +101,7 @@
//3级标题
.text-title-3 {
@extend .text-base;
color: var(--color-primary-90);
color: var(--text-primary-90);
font-size: 18px;
line-height: 24px;
}
......@@ -148,7 +148,7 @@
//1级提示文字
.text-tip-1 {
@extend .text-base;
color: var(--color-primary-90);
color: var(--text-primary-90);
font-size: 16px;
line-height: 24px;
}
......@@ -161,7 +161,7 @@
//2级提示文字
.text-tip-2 {
@extend .text-base;
color: var(--color-primary-90);
color: var(--text-primary-90);
font-size: 14px;
line-height: 22px;
}
......@@ -174,60 +174,60 @@
//3级提示文字
.text-tip-3 {
@extend .text-base;
color: var(--color-primary-90);
color: var(--text-primary-90);
font-size: 12px;
}
.main-color{
.main-color {
color: rgb(5, 95, 194);
}
// 业务主色 高亮背景
.color-bg-active{
.color-bg-active {
background: rgb(246, 250, 255);
}
// 黑色
.text-primary-clor{
.text-primary-clor {
color: #0a121e;
}
// 黑色90% / 主标题文字颜色
.text-primary-90-clor{
.text-primary-90-clor {
color: #222934;
}
// 黑色80% / 小标题文字颜色
.text-primary-80-clor{
.text-primary-80-clor {
color: #3b414b;
}
// 黑色65% / 正文颜色
.text-primary-65-clor{
.text-primary-65-clor {
color: #5f656c;
}
// 黑色50%
.text-primary-50-clor{
.text-primary-50-clor {
color: #84888e;
}
// 黑色10%
.bg-black-10{
.bg-black-10 {
background: #E6E7E8;
}
// 黑色5%
.bg-black-5{
.bg-black-5 {
background: #EAECEE;
}
// 黑色2%
.bg-black-2{
.bg-black-2 {
background: #F7F8F9;
}
// 白色主色
.bg-white-100{
.bg-white-100 {
background: #FFFFFF;
}
\ No newline at end of file
......@@ -25,7 +25,7 @@ const span = 12
<style lang="scss" scoped>
.wrapper {
background-color: rgba(0, 0, 0, 0.068);
// background-color: rgba(0, 0, 0, 0.068);
padding: 10px;
}
</style>
\ No newline at end of file
<script setup lang="ts">
import { ElSpace, ElRow, ElCol } from 'element-plus';
import '@/styles/tabs.scss'
import ColorPrefixTitle from '@/components/base/texts/ColorPrefixTitle.vue';
const span = 12
</script>
<template>
<el-row>
<el-col :span="span">
<pre>
{{ `import ColorPrefixTitle from '@/components/base/texts/ColorPrefixTitle.vue';
<template>
<color-prefix-title>科技领域</color-prefix-title>
<color-prefix-title color="var(--color-yellow-100)">科技领域</color-prefix-title>
<color-prefix-title color="red">科技领域</color-prefix-title>
</template>
`}}
</pre>
<el-space direction="vertical">
<color-prefix-title>科技领域</color-prefix-title>
<color-prefix-title color="var(--color-yellow-100)">科技领域</color-prefix-title>
<color-prefix-title color="red">科技领域</color-prefix-title>
</el-space>
</el-col>
</el-row>
</template>
<style lang="scss" scoped></style>
\ No newline at end of file
......@@ -12,6 +12,9 @@
<el-tab-pane label="通用" lazy>
<common-page />
</el-tab-pane>
<el-tab-pane label="文本" lazy>
<text-page />
</el-tab-pane>
<el-tab-pane label="单选框" lazy>
<radio-page />
</el-tab-pane>
......@@ -32,6 +35,7 @@ import { ElTabs, ElTabPane, ElSpace } from "element-plus";
import RadioPage from './RadioPage/index.vue';
import TabsPage from './TabsPage/index.vue';
import CommonPage from './CommonPage/index.vue';
import TextPage from './TextPage/index.vue';
</script>
<style lang="scss" scoped></style>
\ No newline at end of file
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue';
import * as echarts from 'echarts';
// 定义组件属性
const props = defineProps({
nodes: {
type: Array,
default: () => []
}
, links: {
type: Array,
default: () => []
}
});
const chartDom = ref()
let myChart = null
onMounted(async () => {
})
watch(() => props.nodes, () => {
nodeTopBottomLayout(props.nodes)
updateCharts()
})
watch(() => props.links, updateCharts)
onUnmounted(() => myChart?.dispose())
function nodeTopBottomLayout(nodes: Array<any>) {
if (nodes.length < 1) return
// 中心节点放在中间
const centerNode = nodes[0]
centerNode.x = 0
centerNode.y = 0
// 其余节点分成上下两列
const otherNodes = nodes.slice(1)
const tops = []
const bottoms = []
otherNodes.forEach((item) => {
if (item.type === 'to') {
tops.push(item)
} else {
bottoms.push(item)
}
})
// 计算每列的节点数
const topCount = tops.length
const bottomCount = bottoms.length
// 列间距
const columnSpacing = 250
// 上下列距离中心的距离
const verticalDistance = 260
let ci = (topCount - 1) / 2
// 处理上列节点
for (let i = 0; i < topCount; i++) {
const node = tops[i]
// 计算x坐标,居中排列
const offsetX = (i - ci) * columnSpacing
node.x = offsetX
node.y = -verticalDistance
}
ci = (bottomCount - 1) / 2
// 处理下列节点
for (let i = 0; i < bottomCount; i++) {
const node = bottoms[i]
// 计算x坐标,居中排列
const offsetX = (i - ci) * columnSpacing
node.x = offsetX
node.y = verticalDistance
}
}
// 辅助函数:更新图表数据
function updateCharts() {
// 销毁现有图表实例
if (myChart) {
myChart.dispose()
}
myChart = echarts.init(chartDom.value)
myChart.setOption({
grid: {
top: 0,
bottom: 0,
left: 0,
right: 0,
},
// 关闭不必要的交互以提高性能
tooltip: {
show: false
},
// 优化渲染性能
progressive: 500,
progressiveThreshold: 300,
animation: false,
animationDuration: 1000,
animationEasing: 'cubicOut',
series: [{
type: 'graph',
// zoom: Math.max(1, props.nodes.length / 10.0), // 放大显示
itemStyle: {
color: '#73C0DE'
},
height: '80%',
layout: 'none',
data: props.nodes,
links: props.links,
center: [0, 10],
nodeScaleRatio: 0,
// categories: categories,
roam: true,
label: {
show: true,
position: 'bottom',
offset: [0, -10],
formatter: '{b}',
fontSize: 14,
fontWeight: 'bold',
padding: [4, 6],
borderRadius: 4,
color: 'rgb(5 95 194)',
// 限制宽度并自动换行
width: 120, // 限制标签宽度
overflow: 'break', // 超出宽度自动换行
lineHeight: 16 // 设置行高,确保多行显示正常
},
lineStyle: {
// color: 'source',
curveness: 0,
width: 1,
color: '#AED6FF'
},
edgeSymbol: ['arrow', 'none'],
}]
})
}
</script>
<template>
<div class="flex-display content-box">
<div ref="chartDom" class="chart-container"></div>
</div>
</template>
<style lang="scss" scoped>
.content-box {
padding: 10px;
gap: 10px;
flex-direction: column;
}
.chart-container {
height: 500px;
width: 100%;
}
</style>
\ No newline at end of file
<script setup lang="ts">
import '@/styles/common.scss';
import { getSupplyAreaList, getSupplyCountryList, getSupplyList } from "@/api/companyPages/index.js";
import { onMounted, ref } from 'vue';
import { ElCheckbox, ElCheckboxGroup, ElRow, ElCol, ElMessage } from 'element-plus';
import ColorPrefixTitle from '@/components/base/texts/ColorPrefixTitle.vue';
import GraphChart from './GraphChart.vue'
import { forEach } from 'lodash';
import AiTipPane from '@/components/base/panes/AiTipPane.vue';
import { getRuleOrg } from '@/api/ruleRestriction';
const CompanyImg = "/icon/company/普通企业节点.svg"
// 定义组件属性
const props = defineProps({
enterpriseInfo: {
type: Object,
default: {}
}
});
const supplyAreaList = ref([]);
const supplyCountryList = ref([]);
const supplyTypeList = [{
name: '上游', id: 'supply'
}, {
name: '下游', id: 'customer'
}];
const selectedSupplyAreas = ref([]);
const selectedSupplyCountries = ref([]);
const selectedSupplyTypes = ref([]);
const nodes = ref([])
const links = ref([])
onMounted(async () => {
await initData()
await handleOptionsChnage()
await getRuleOrg({ orgId: props.enterpriseInfo.id })
})
async function initData() {
const { data } = await getSupplyAreaList(props.enterpriseInfo.id);
supplyAreaList.value = data ?? [];
const { data: countryData } = await getSupplyCountryList(props.enterpriseInfo.id);
supplyCountryList.value = countryData ?? [];
}
async function handleOptionsChnage() {
const params = {
arealist: selectedSupplyAreas.value ? selectedSupplyAreas.value.join(",") : null,
countrylist: selectedSupplyCountries.value ? selectedSupplyCountries.value.join(",") : null,
type: selectedSupplyTypes.value.length === 2 ? 'all' : selectedSupplyTypes.value ? selectedSupplyTypes.value : null,
id: props.enterpriseInfo.id
}
const { data } = await getSupplyList(params)
if (!data) {
ElMessage.error('获取供应链数据失败')
return
}
// 提取所有公司名称
const companyMap: Map<string, any> = new Map();
data.forEach(item => {
companyMap.set(item.orgId, { name: item.orgName, type: item.type })
companyMap.set(item.otherOrgId, { name: item.otherOrgName, type: item.type })
})
// 创建nodes数组
nodes.value = Array.from(companyMap).map(([orgId, item]) => ({
id: orgId,
name: item.name,
type: item.type,
symbolSize: 50,
symbol: `image://${CompanyImg}` // 假设CompanyImg是一个图片路径变量
}));
const linksData = []
forEach(data, item => {
const link = {
source: item.orgId,
target: item.otherOrgId,
label: {
show: true,
formatter: '供应商',
color: 'rgba(137, 193, 255, 1)',
backgroundColor: 'rgba(137, 193, 255, 0.2)',
padding: [4, 4],
borderRadius: 10
},
}
if (item.type === 'from') {
link.source = item.otherOrgId
link.target = item.orgId
link.label.formatter = '客户'
}
linksData.push(link)
})
// 创建links数组
links.value = linksData
}
</script>
<template>
<div class="flex-display box-div">
<div class="background-as-card options-pane">
<color-prefix-title>科技领域</color-prefix-title>
<el-checkbox-group class="checkbox-group" v-model="selectedSupplyAreas" @change="handleOptionsChnage">
<el-row>
<el-col v-for="item in supplyAreaList" :span="12">
<el-checkbox :label="item.name + '(' + item.num + ')'" :value="item.id" />
</el-col>
</el-row>
</el-checkbox-group>
<color-prefix-title>国家/地区</color-prefix-title>
<el-checkbox-group class="checkbox-group" v-model="selectedSupplyCountries" @change="handleOptionsChnage">
<el-row>
<el-col v-for="item in supplyCountryList" :span="12">
<el-checkbox :label="item.name + '(' + item.num + ')'" :value="item.id" />
</el-col>
</el-row>
</el-checkbox-group>
<color-prefix-title>上下游</color-prefix-title>
<el-checkbox-group class="checkbox-group" v-model="selectedSupplyTypes" @change="handleOptionsChnage">
<el-row>
<el-col v-for="item in supplyTypeList" :span="12">
<el-checkbox :label="item.name" :value="item.id" />
</el-col>
</el-row>
</el-checkbox-group>
</div>
<div class="background-as-card chart-box flex-fill">
<graph-chart :nodes="nodes" :links="links" />
<ai-tip-pane v-if="enterpriseInfo.id === '914403001922038216'">
华为构建了覆盖全球的复杂供应链网络,以自身为核心,辐射芯片、显示、摄像头等关键领域。其供应链深度扎根中国,同时整合美国高通、韩国三星、日本索尼等国际顶尖供应商,形成多元化供应格局。面对外部技术封锁,华为通过扶持海思等子公司强化垂直整合,并积极拓展本土替代资源,在保持技术领先的同时增强供应链韧性,展现出卓越的全球资源调配与风险管控能力。
</ai-tip-pane>
</div>
</div>
</template>
<style lang="css" scoped>
.box-div {
margin: 2px;
}
.options-pane {
width: 360px;
padding-top: 16px;
margin-right: 16px;
}
.chart-box {
padding: 10px 0px;
background-color: var(--bg-white-100);
}
.checkbox-group {
padding: 0px 25px;
margin-bottom: 20px;
}
</style>
\ No newline at end of file
......@@ -17,8 +17,7 @@
<operating-pages :enterprise-info="enterpriseInfo" />
</el-tab-pane>
<el-tab-pane lazy label="供应链 / 股权">
<div class="flex-display">
</div>
<supply-chain :enterprise-info="enterpriseInfo" />
</el-tab-pane>
</el-tabs>
</el-space>
......@@ -33,6 +32,7 @@ import TitlePane from './component/TitlePane.vue';
import NewsPane from './component/DetailsPages/NewsPane.vue';
import BaseInfo from './component/DetailsPages/BaseInfo.vue';
import OperatingPages from './component/OperatingPages/index.vue';
import SupplyChain from './component/SupplyChain/index.vue';
import '@/styles/tabs.scss'
import '@/styles/container.scss'
import { ElScrollbar, ElSpace, ElTabs, ElTabPane } from 'element-plus';
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论