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

feat:企业经营情况详情页

上级 000d30fd
...@@ -129,3 +129,42 @@ export function getSupplyList(params) { ...@@ -129,3 +129,42 @@ export function getSupplyList(params) {
params, params,
}) })
} }
// 企业市值:市值变化
export function getMarketCapList(params) {
return request({
method: 'GET',
url: `/api/enterprisePage/marketCap/${params}`,
})
}
// 企业发展:营收折线图
export function getRevenueList(params) {
return request({
method: 'GET',
url: `/api/enterprisePage/revenue/${params}`,
})
}
// 企业发展:净利润折线图
export function getNetProfitList(params) {
return request({
method: 'GET',
url: `/api/enterprisePage/netProfit/${params}`,
})
}
// 企业发展:人员情况折线图
export function getPersonnelList(params) {
return request({
method: 'GET',
url: `/api/enterprisePage/personnel/${params}`,
})
}
// 企业发展: 市场占比折线图
export function getMarketShareList(params) {
return request({
method: 'GET',
url: `/api/enterprisePage/marketShare/${params}`,
})
}
\ No newline at end of file
@use '@/styles/common.scss';
.radio-group-as-gap-btn {
@extend .text-tip-1;
.el-radio-button {
--el-radio-button-checked-bg-color: var(--bg-white-100);
--el-radio-button-checked-border-color: var(--bg-black-10);
border-radius: 4px;
}
.el-radio-button.is-active {
--el-radio-button-checked-text-color: var(--color-primary-100);
--el-radio-button-checked-bg-color: var(--color-primary-10);
--el-radio-button-checked-border-color: var(--color-primary-5);
border-radius: 4px;
}
}
\ No newline at end of file
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue'; import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { getEnterpriseBranch, getEnterpriseKeyPerson } from '@/api/companyPages'; import { getEnterpriseBranch, getEnterpriseKeyPerson } from '@/api/companyPages';
import PersonAvatar from '@/components/base/people/personAvatar.vue'; import PersonAvatar from '@/components/base/people/PersonAvatar.vue';
import { ElDescriptions, ElDescriptionsItem, ElDivider, ElImage, ElSpace } from 'element-plus'; import { ElDescriptions, ElDescriptionsItem, ElDivider, ElImage, ElSpace } from 'element-plus';
import '@/styles/descriptions.scss' import '@/styles/descriptions.scss'
......
<template>
<sanctions-situation :enterprise-info="enterpriseInfo" :line-data="lineData"></sanctions-situation>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { getNetProfitList, getPersonnelList, getRevenueList } from '@/api/companyPages';
import SanctionsSituation, { LineDataItem } from './SanctionsSituation.vue';
import { l } from 'vite/dist/node/types.d-aGj9QkWt';
// 定义组件属性
const props = defineProps({
enterpriseInfo: {
type: Object,
default: {}
}
});
const lineData = ref<LineDataItem[]>([])
onMounted(async () => {
await intData()
})
async function intData() {
let { data: revenue } = await getRevenueList(props.enterpriseInfo.id)
revenue = revenue?.map(item => ({
time: new Date(item.year, 1, 1),
value: item.value,
type: '营收',
unit: item.unit ?? '亿元'
})) ?? []
let { data: netProfit } = await getNetProfitList(props.enterpriseInfo.id)
netProfit = netProfit?.map(item => ({
time: new Date(item.year, 1, 1),
value: item.value,
type: '净利润',
unit: item.unit ?? '亿元'
})) ?? []
let { data: personnel } = await getPersonnelList(props.enterpriseInfo.id)
personnel = personnel?.map(item => ({
time: new Date(item.year, 1, 1),
value: item.value,
type: '人员',
unit: item.unit ?? '亿元'
})) ?? []
lineData.value = [...revenue, ...netProfit, ...personnel]
}
</script>
\ No newline at end of file
<template>
<sanctions-situation :enterprise-info="enterpriseInfo" :line-data="lineData"></sanctions-situation>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { getMarketShareList } from '@/api/companyPages';
import SanctionsSituation, { LineDataItem } from './SanctionsSituation.vue';
// 定义组件属性
const props = defineProps({
enterpriseInfo: {
type: Object,
default: {}
}
});
const lineData = ref<LineDataItem[]>([])
onMounted(async () => {
await intData()
})
async function intData() {
const { data } = await getMarketShareList(props.enterpriseInfo.id)
lineData.value = data?.map(item => ({
time: new Date(item.year, 1, 1),
value: item.value,
unit: item.unit ?? '亿元'
})) ?? []
}
</script>
\ No newline at end of file
<template>
<sanctions-situation :enterprise-info="enterpriseInfo" :line-data="lineData"></sanctions-situation>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { getMarketCapList } from '@/api/companyPages';
import SanctionsSituation, { LineDataItem } from './SanctionsSituation.vue';
// 定义组件属性
const props = defineProps({
enterpriseInfo: {
type: Object,
default: {}
}
});
const lineData = ref<LineDataItem[]>([])
onMounted(async () => {
await intData()
})
async function intData() {
const { data } = await getMarketCapList(props.enterpriseInfo.id)
lineData.value = data?.map(item => ({
time: new Date(item.year, 1, 1),
value: item.value,
type: '市值变化',
unit: item.unit ?? '亿元'
})) ?? []
}
</script>
\ No newline at end of file
<template>
<sanctions-situation :enterprise-info="enterpriseInfo" :line-data="lineData"></sanctions-situation>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { getMarketCapList, getStudyList } from '@/api/companyPages';
import SanctionsSituation, { LineDataItem } from './SanctionsSituation.vue';
// 定义组件属性
const props = defineProps({
enterpriseInfo: {
type: Object,
default: {}
}
});
const lineData = ref<LineDataItem[]>([])
onMounted(async () => {
await intData()
})
async function intData() {
const { data } = await getStudyList(props.enterpriseInfo.id)
lineData.value = data?.map(item => ({
time: new Date(item.year, 1, 1),
value: item.currentValue,
type: item.type,
unit: item.unit ?? '亿元'
})) ?? []
}
</script>
\ No newline at end of file
<script setup> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'; import { ref, onMounted, onUnmounted, watch } from 'vue';
import { ElSpace, ElRadioGroup, ElRadio, ElRadioButton } from 'element-plus'; import { ElRadioGroup, ElRadioButton } from 'element-plus';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { getStudyList, getSanctionList } from '@/api/companyPages'; import { getSanctionList } from '@/api/companyPages';
import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue'; import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue';
import AiTipPane from '@/components/base/panes/AiTipPane.vue' import AiTipPane from '@/components/base/panes/AiTipPane.vue'
export interface LineDataItem {
time: Date;
value: number;
type: string;
unit: string;
}
// 定义组件属性 // 定义组件属性
const props = defineProps({ const props = defineProps({
enterpriseInfo: { enterpriseInfo: {
type: Object, type: Object,
default: {} default: {}
},
lineData: {
type: Array<LineDataItem>,
default: () => []
} }
}); });
const studyList = ref([])
const sanctionList = ref([]) const sanctionList = ref([])
const studyTypes = ref([]) const lineTypes = ref([])
const studyType = ref() const lineType = ref()
const chartDom = ref() const chartDom = ref()
let myChart = null let myChart = null
...@@ -28,24 +38,26 @@ onMounted(async () => { ...@@ -28,24 +38,26 @@ onMounted(async () => {
}) })
onUnmounted(() => myChart?.dispose()) onUnmounted(() => myChart?.dispose())
watch(() => [props.lineData, sanctionList.value], async () => {
lineTypes.value = [...new Set(props.lineData.map(t => t.type))]
if (lineTypes.value.length > 0) {
lineType.value = lineTypes.value[0]
}
updateCharts(lineType.value, props.lineData, sanctionList.value)
})
async function intData() { async function intData() {
const { data } = await getStudyList(props.enterpriseInfo.id) const { data } = await getSanctionList(props.enterpriseInfo.id)
studyList.value = data ?? [] sanctionList.value = data ?? []
data.forEach(t => t.time = new Date(t.year, 1, 1))
studyTypes.value = [...new Set(data.map(t => t.type))]
const { data: sanctionData } = await getSanctionList(props.enterpriseInfo.id)
sanctionList.value = sanctionData ?? []
sanctionList.value.forEach(t => t.time = new Date(t.sanctionDate)) sanctionList.value.forEach(t => t.time = new Date(t.sanctionDate))
if (studyTypes.value.length > 0) {
studyType.value = studyTypes.value[0]
}
updateCharts(studyType.value, studyList.value, sanctionList.value)
} }
// 辅助函数:获取制裁年份对应的Y值 // 辅助函数:获取制裁年份对应的Y值
function getSanctionYValue(sanctionDate, filteredList, defaultYValue) { function getSanctionYValue(sanctionDate: Date, filteredList: LineDataItem[], defaultYValue: number) {
const year = sanctionDate.getFullYear() const year = sanctionDate.getFullYear()
const yearData = filteredList.find(d => d.year === year) const yearData = filteredList.find(d => d.time.getFullYear() === year)
return yearData ? yearData.currentValue : defaultYValue return yearData ? yearData.value : defaultYValue
} }
// 辅助函数:格式化文本内容,实现智能换行(考虑中英文混合) // 辅助函数:格式化文本内容,实现智能换行(考虑中英文混合)
...@@ -77,74 +89,10 @@ function formatContent(content, maxWidth = 26) { ...@@ -77,74 +89,10 @@ function formatContent(content, maxWidth = 26) {
return lines.join('\n') return lines.join('\n')
} }
function updateCharts(type, dataStudy, dataSanction) {
const filteredList = dataStudy.filter(t => t.type === type)
if (!filteredList.length) return // 辅助函数:获取折线图数据
function getLineSeries(filteredList: LineDataItem[]) {
// 销毁现有图表实例 return {
if (myChart) {
myChart.dispose()
}
myChart = echarts.init(chartDom.value)
// y轴单位
const unit = filteredList[0].unit
const yValue = Math.max(...filteredList.map(d => d.currentValue))
// 计算x轴范围,扩大活动空间
const allDates = [
...filteredList.map(t => t.time),
...dataSanction.map(t => t.time)
]
const minDate = new Date(Math.min(...allDates))
const maxDate = new Date(Math.max(...allDates))
// 向前扩展1年,向后扩展1年
minDate.setFullYear(minDate.getFullYear() - 1)
maxDate.setFullYear(maxDate.getFullYear() + 1)
myChart.setOption({
textStyle: {
fontSize: 14
},
grid: {
left: 60,
right: 110,
},
tooltip: {
trigger: 'axis',
position: function (pt) {
return [pt[0], '10%'];
},
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100
},
{
start: 0,
end: 100
}
],
xAxis: {
type: 'time',
boundaryGap: false,
min: minDate,
max: maxDate
},
yAxis: {
type: 'value',
boundaryGap: [0, '100%'],
// 显示y轴单位在顶部
name: unit,
nameLocation: 'end'
},
series: [
{
type: 'line', type: 'line',
//从上到下填充颜色 //从上到下填充颜色
areaStyle: { areaStyle: {
...@@ -159,34 +107,42 @@ function updateCharts(type, dataStudy, dataSanction) { ...@@ -159,34 +107,42 @@ function updateCharts(type, dataStudy, dataSanction) {
} }
]) ])
}, },
data: filteredList.map(t => [t.time, t.currentValue]) color: '#80B5FF',
}, data: filteredList.map(t => [t.time, t.value])
{ }
type: 'scatter', }
tooltip: { show: false },
data: dataSanction.map(t => { // 辅助函数:获取散点图数据
function getScatterSeries(filteredList: LineDataItem[], dataSanction: LineDataItem[], yValue: number) {
// 计算散点图数据
const xyData = dataSanction.map(t => {
const currentYValue = getSanctionYValue(t.time, filteredList, yValue) + yValue / 3 const currentYValue = getSanctionYValue(t.time, filteredList, yValue) + yValue / 3
return [t.time, currentYValue, t] return [t.time, currentYValue, t]
}), })
return {
type: 'scatter',
tooltip: { show: false },
data: xyData,
// 绘制垂直虚线
markLine: { markLine: {
lineStyle: { lineStyle: {
color: '#ff4d4f', color: '#ff4d4f',
type: 'dashed', type: 'dashed',
width: 1 width: 1
}, },
data: dataSanction.map(t => { data: xyData.map(t => {
const currentYValue = getSanctionYValue(t.time, filteredList, yValue) + yValue / 3
return [{ return [{
coord: [t.time, 0], coord: [t[0], 0],
symbol: 'none' symbol: 'none'
}, { }, {
coord: [t.time, currentYValue], coord: [t[0], t[1]],
symbol: 'none', symbol: 'none',
}] }]
}) })
}, },
coordinateSystem: 'cartesian2d', coordinateSystem: 'cartesian2d',
symbolSize: 1, symbolSize: 1,
// 散点图标签
label: { label: {
show: true, show: true,
position: 'insideBottomLeft', position: 'insideBottomLeft',
...@@ -218,30 +174,110 @@ function updateCharts(type, dataStudy, dataSanction) { ...@@ -218,30 +174,110 @@ function updateCharts(type, dataStudy, dataSanction) {
distance: -1 distance: -1
} }
} }
}
// 辅助函数:更新图表数据
function updateCharts(type: string, dataStudy: LineDataItem[], dataSanction: LineDataItem[]) {
const filteredList = dataStudy.filter(t => t.type === type)
if (!filteredList.length) return
// 销毁现有图表实例
if (myChart) {
myChart.dispose()
}
myChart = echarts.init(chartDom.value)
// y轴单位
const unit = filteredList[0].unit
const yValue = Math.max(...filteredList.map(d => d.value))
// 计算x轴范围,扩大活动空间
const allDates = [
...filteredList.map(t => t.time.getTime()),
...dataSanction.map(t => t.time.getTime())
]
// 计算x轴范围,扩大活动空间
const minDate = new Date(Math.min(...allDates))
const maxDate = new Date(Math.max(...allDates))
// 向前扩展1年,向后扩展1年
minDate.setFullYear(minDate.getFullYear() - 1)
maxDate.setFullYear(maxDate.getFullYear() + 1)
myChart.setOption({
textStyle: {
fontSize: 14
},
grid: {
left: 60,
right: 110,
},
tooltip: {
trigger: 'axis',
position: function (pt) {
return [pt[0], '10%'];
},
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100
},
{
start: 0,
end: 100
}
], ],
xAxis: {
type: 'time',
boundaryGap: false,
min: minDate,
max: maxDate
},
yAxis: {
type: 'value',
boundaryGap: [0, '100%'],
// 显示y轴单位在顶部
name: unit,
nameLocation: 'end'
},
series: [
getScatterSeries(filteredList, dataSanction, yValue),
getLineSeries(filteredList),
]
}) })
} }
// 辅助函数:处理研究类型改变事件
function handleStudyTypesChange() { function handleStudyTypesChange() {
updateCharts(studyType.value, studyList.value, sanctionList.value) updateCharts(lineType.value, props.lineData, sanctionList.value)
} }
</script> </script>
<template> <template>
<analysis-box title="被制裁时间轴"> <analysis-box title="被制裁时间轴">
<template v-slot:header-btn> <template v-if="lineTypes.length > 1" v-slot:header-btn>
<el-radio-group v-model="studyType" @change="handleStudyTypesChange"> <el-radio-group class="radio-group-as-gap-btn" v-model="lineType" @change="handleStudyTypesChange">
<el-radio-button v-for="item in studyTypes" :key="item" :label="item">{{ item }}</el-radio-button> <el-radio-button v-for="item in lineTypes" :key="item" :label="item">{{ item }}</el-radio-button>
</el-radio-group> </el-radio-group>
</template> </template>
<div class="flex-display content-box"> <div class="flex-display content-box">
<div ref="chartDom" class="chart-container"></div> <div ref="chartDom" class="chart-container"></div>
<ai-tip-pane>123</ai-tip-pane> <ai-tip-pane
v-if="enterpriseInfo.id === '914403001922038216'">近年来,华为遭遇了严峻的外部技术封锁,其核心源于某些国家的持续制裁。这主要包括被列入“实体清单”,禁止其未经许可从美国公司获取芯片等关键技术;以及遭受更严格的“外国直接产品规则”打击,旨在切断其利用美国工具、软件和技术设计或制造先进芯片的渠道。这些措施不仅限制了华为自身产品的设计与生产,也影响了其全球供应链,使其在获取先进半导体、移动操作系统生态(如GMS)及基础软件工具等方面面临巨大挑战,意图从根本上遏制其技术发展。</ai-tip-pane>
</div> </div>
</analysis-box> </analysis-box>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@use '@/styles/radio.scss';
.content-box { .content-box {
padding: 10px; padding: 10px;
gap: 10px; gap: 10px;
......
<script setup lang="ts"> <script setup lang="ts">
import '@/styles/tabs.scss' import '@/styles/tabs.scss'
import { ElTabPane, ElTabs } from 'element-plus'; import { ElTabPane, ElTabs } from 'element-plus';
import SanctionsSituation from './SanctionsSituation.vue'; import EnterpriseScale from './EnterpriseScale.vue';
import MarketShare from './MarketShare.vue';
import MarketValue from './MarketValue.vue';
import ResearchAndDevelopment from './ResearchAndDevelopment.vue';
// 定义组件属性 // 定义组件属性
const props = defineProps({ const props = defineProps({
...@@ -15,12 +18,18 @@ const props = defineProps({ ...@@ -15,12 +18,18 @@ const props = defineProps({
<template> <template>
<div style="overflow: visible;"> <div style="overflow: visible;">
<el-tabs tabPosition="left" class="disinheritance tabs-nav-no-wrap left-float-nav-tabs"> <el-tabs tabPosition="left" class="disinheritance tabs-nav-no-wrap left-float-nav-tabs">
<el-tab-pane label="企业规模"> <el-tab-pane label="企业规模" lazy>
<sanctions-situation :enterprise-info="enterpriseInfo"></sanctions-situation> <enterprise-scale :enterprise-info="enterpriseInfo"></enterprise-scale>
</el-tab-pane>
<el-tab-pane label="市值变化" lazy>
<market-value :enterprise-info="enterpriseInfo"></market-value>
</el-tab-pane>
<el-tab-pane label="研发投入" lazy>
<research-and-development :enterprise-info="enterpriseInfo"></research-and-development>
</el-tab-pane>
<el-tab-pane label="市场占比" lazy>
<market-share :enterprise-info="enterpriseInfo"></market-share>
</el-tab-pane> </el-tab-pane>
<!-- <el-tab-pane label="市值变化"></el-tab-pane>
<el-tab-pane label="研发投入"></el-tab-pane>
<el-tab-pane label="市场占比"></el-tab-pane> -->
</el-tabs> </el-tabs>
</div> </div>
</template> </template>
\ No newline at end of file
...@@ -30,7 +30,7 @@ import { getSupplyAreaList } from '@/api/companyPages'; ...@@ -30,7 +30,7 @@ import { getSupplyAreaList } from '@/api/companyPages';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { ElSpace } from 'element-plus'; import { ElSpace } from 'element-plus';
// 导入自定义组件 // 导入自定义组件
import CapitalScale from './capitalScale.vue' import CapitalScale from './CapitalScale.vue'
// 响应式数据 // 响应式数据
const areas = ref([]) // 供应区域列表 const areas = ref([]) // 供应区域列表
......
...@@ -29,10 +29,10 @@ ...@@ -29,10 +29,10 @@
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { getEnterprisePageInfo } from '@/api/companyPages'; import { getEnterprisePageInfo } from '@/api/companyPages';
import TitlePane from './component/titlePane.vue'; import TitlePane from './component/TitlePane.vue';
import NewsPane from './component/detailsPages/newsPane.vue'; import NewsPane from './component/DetailsPages/NewsPane.vue';
import BaseInfo from './component/detailsPages/baseInfo.vue'; import BaseInfo from './component/DetailsPages/BaseInfo.vue';
import OperatingPages from './component/operatingPages/index.vue'; import OperatingPages from './component/OperatingPages/index.vue';
import '@/styles/tabs.scss' import '@/styles/tabs.scss'
import '@/styles/container.scss' import '@/styles/container.scss'
import { ElScrollbar, ElSpace, ElTabs, ElTabPane } from 'element-plus'; import { ElScrollbar, ElSpace, ElTabs, ElTabPane } from 'element-plus';
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论