提交 7b3de8a1 authored 作者: coderBryanFu's avatar coderBryanFu

修改工作日程表模块

...@@ -1084,7 +1084,8 @@ const processRankingData = rawData => { ...@@ -1084,7 +1084,8 @@ const processRankingData = rawData => {
// 新增字段 // 新增字段
domains: item.domainList ? item.domainList.map(d => d.name) : [], domains: item.domainList ? item.domainList.map(d => d.name) : [],
date: formattedDate, date: formattedDate,
member: item.keyMember || "-" member: item.keyMember || "-",
province: item.province || ""
}; };
}); });
}; };
......
...@@ -148,7 +148,7 @@ ...@@ -148,7 +148,7 @@
<div class="warning"> <div class="warning">
<div class="warning-title"> <div class="warning-title">
<img src="./assets/warning.png" alt /> <img src="./assets/warning.png" alt />
<span>新增排华联盟预警1</span> <span>新增排华联盟预警</span>
</div> </div>
<div class="warning-content"> <div class="warning-content">
<div class="content-item" v-for="(item, index) in warningList" :key="index"> <div class="content-item" v-for="(item, index) in warningList" :key="index">
......
...@@ -469,43 +469,45 @@ const handleIndicatorChange = indicator => { ...@@ -469,43 +469,45 @@ const handleIndicatorChange = indicator => {
{ {
name: "中国", name: "中国",
type: "line", type: "line",
symbolSize: 10, symbolSize: 8,
smooth: true,
itemStyle: { itemStyle: {
color: "#CE4F51" color: "#CE4F51"
}, },
areaStyle: { // areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ // color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ // {
offset: 0, // offset: 0,
color: "rgb(206, 79, 81, .8)" // color: "rgb(206, 79, 81, .8)"
}, // },
{ // {
offset: 1, // offset: 1,
color: "rgb(206, 79, 81, .3)" // color: "rgb(206, 79, 81, .3)"
} // }
]) // ])
}, // },
data: chinaValues data: chinaValues
}, },
{ {
name: "美国", name: "美国",
type: "line", type: "line",
symbolSize: 10, smooth: true,
symbolSize: 8,
itemStyle: { itemStyle: {
color: "#055FC2" color: "#055FC2"
}, },
areaStyle: { // areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ // color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ // {
offset: 0, // offset: 0,
color: "rgb(5, 95, 194,.8)" // color: "rgb(5, 95, 194,.8)"
}, // },
{ // {
offset: 1, // offset: 1,
color: "rgb(5, 95, 194, .3)" // color: "rgb(5, 95, 194, .3)"
} // }
]) // ])
}, // },
data: usaValues data: usaValues
} }
]; ];
...@@ -540,22 +542,25 @@ const lineOption = ref({ ...@@ -540,22 +542,25 @@ const lineOption = ref({
xAxis: { xAxis: {
type: "category", type: "category",
boundaryGap: false, boundaryGap: false,
minInterval: 1,
scale: true,
axisLine: { axisLine: {
lineStyle: { lineStyle: {
color: "#ccc", color: "#e0e0e0",
width: 1 width: 1
} }
}, },
axisTick: { axisTick: {
show: true, show: false,
lineStyle: { lineStyle: {
color: "#ccc" color: "#ccc"
} }
}, },
axisLabel: { axisLabel: {
color: "#ccc", color: "#666",
fontSize: 16, fontSize: 13,
fontWeight: 400 fontWeight: 500,
margin: 12
}, },
data: ["2021", "2022", "2023", "2024", "2025"] data: ["2021", "2022", "2023", "2024", "2025"]
}, },
...@@ -571,9 +576,9 @@ const lineOption = ref({ ...@@ -571,9 +576,9 @@ const lineOption = ref({
padding: [0, 0, 10, 0] padding: [0, 0, 10, 0]
}, },
axisLine: { axisLine: {
show: false, show: true,
lineStyle: { lineStyle: {
color: "#ccc", color: "#e0e0e0",
width: 1 width: 1
} }
}, },
......
...@@ -34,10 +34,10 @@ defineProps({ ...@@ -34,10 +34,10 @@ defineProps({
.overview-card { .overview-card {
height: 450px; height: 450px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid rgba(234, 236, 238, 1); border: 1px solid var(--border-black-5);
border-radius: 10px; 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.1);
background: rgba(255, 255, 255, 1); background: var(--bg-white-100);
overflow: hidden; overflow: hidden;
.overview-card-header { .overview-card-header {
......
...@@ -25,7 +25,7 @@ const getWordCloudChart = (data = []) => { ...@@ -25,7 +25,7 @@ const getWordCloudChart = (data = []) => {
// shape: 'star' // 星形 // shape: 'star' // 星形
// shape: 'cardioid' // 心形 // shape: 'cardioid' // 心形
gridSize: 15, // 网格大小,影响词间距。 gridSize: 15, // 网格大小,影响词间距。
sizeRange: [10, 30], // 定义词云中文字大小的范围 sizeRange: [16, 36], // 定义词云中文字大小的范围
rotationRange: [0, 0], rotationRange: [0, 0],
rotationStep: 15, rotationStep: 15,
drawOutOfBound: false, // 是否超出画布 drawOutOfBound: false, // 是否超出画布
......
...@@ -246,7 +246,7 @@ ...@@ -246,7 +246,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, watch, computed } from "vue"; import { ref, onMounted, watch, computed, nextTick } from "vue";
import { getBillPoliContribution, getBillMainPoliContribution, getBillPersonPoliContribution } from "@/api/deepdig"; import { getBillPoliContribution, getBillMainPoliContribution, getBillPersonPoliContribution } from "@/api/deepdig";
import setChart from "@/utils/setChart"; import setChart from "@/utils/setChart";
...@@ -488,6 +488,27 @@ const chart2Data = ref([]); ...@@ -488,6 +488,27 @@ const chart2Data = ref([]);
const chart2ColorList = ref(["#4096FF", "#FFA39E", "#ADC6FF", "#FFC069", "#B5F5EC", "#B37FEB", "#D6E4FF"]); const chart2ColorList = ref(["#4096FF", "#FFA39E", "#ADC6FF", "#FFC069", "#B5F5EC", "#B37FEB", "#D6E4FF"]);
// 使用静态行业分布数据,用于“政治献金领域分布”模块展示
const useStaticIndustryData = true;
const staticChart2Data = [
{ name: "集成电路", value: 21 },
{ name: "能源", value: 18 },
{ name: "量子科技", value: 16 },
{ name: "航空航天", value: 14 },
{ name: "通信设备", value: 12 },
{ name: "生物科技", value: 11 },
{ name: "其他", value: 8 }
];
const staticAreaList = [
{ name: "集成电路", amount: 186000, insNum: 8 },
{ name: "能源", amount: 180000, insNum: 5 },
{ name: "量子科技", amount: 171000, insNum: 2 },
{ name: "航空航天", amount: 75000, insNum: 3 },
{ name: "通信设备", amount: 70000, insNum: 2 }
];
const sankeyColors = [ const sankeyColors = [
"#5470c6", "#5470c6",
"#91cc75", "#91cc75",
...@@ -617,7 +638,7 @@ const getMainPoliContribution = async () => { ...@@ -617,7 +638,7 @@ const getMainPoliContribution = async () => {
const fullSourceList = ref([]); const fullSourceList = ref([]);
const showAllSankeyData = ref(false); const showAllSankeyData = ref(false);
const renderSankeyChart = () => { const renderSankeyChart = async () => {
const sourceList = showAllSankeyData.value ? fullSourceList.value : fullSourceList.value.slice(0, 5); const sourceList = showAllSankeyData.value ? fullSourceList.value : fullSourceList.value.slice(0, 5);
if (sourceList.length > 0) { if (sourceList.length > 0) {
...@@ -626,33 +647,39 @@ const renderSankeyChart = () => { ...@@ -626,33 +647,39 @@ const renderSankeyChart = () => {
const totalAmount = sourceList.reduce((sum, item) => sum + (item.amount || 0), 0); const totalAmount = sourceList.reduce((sum, item) => sum + (item.amount || 0), 0);
const orgNodes = sourceList.map((item, index) => ({
name: item.orgNameZh || item.orgName || `机构${index + 1}`,
value: item.amount,
itemStyle: {
color: sankeyColors[index % sankeyColors.length]
}
}));
const nodes = [ const nodes = [
...orgNodes,
{ {
name: personName, name: personName,
value: totalAmount, value: totalAmount,
label: { position: "left" },
itemStyle: { itemStyle: {
color: "#FF1493" color: "#34C38F"
} }
},
...sourceList.map((item, index) => ({
name: item.orgNameZh,
value: item.amount,
itemStyle: {
color: sankeyColors[index % sankeyColors.length]
} }
}))
]; ];
const links = sourceList.map(item => ({ const links = sourceList
source: item.orgNameZhZh, .map((item, index) => ({
source: item.orgNameZh || item.orgName || `机构${index + 1}`,
target: personName, target: personName,
value: item.amount value: item.amount
})); }))
.filter(item => !!item.source && !!item.target);
// `chart1` 容器由 v-if/v-else 动态渲染,必须等 DOM 挂载后再 init
await nextTick();
let chart1 = getSankeyChart(nodes, links); let chart1 = getSankeyChart(nodes, links);
setChart(chart1, "chart1"); setChart(chart1, "chart1");
} else { } else {
await nextTick();
let chart1 = getSankeyChart([], []); let chart1 = getSankeyChart([], []);
setChart(chart1, "chart1"); setChart(chart1, "chart1");
} }
...@@ -678,25 +705,37 @@ const getPersonPoliContribution = async personId => { ...@@ -678,25 +705,37 @@ const getPersonPoliContribution = async personId => {
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
personPoliContribution.value = res.data; personPoliContribution.value = res.data;
// 政治献金流向:始终使用真实接口数据渲染桑基图
fullSourceList.value = res.data.sourceList || []; fullSourceList.value = res.data.sourceList || [];
showAllSankeyData.value = false; // Reset to default (top 5) showAllSankeyData.value = false; // Reset to default (top 5)
renderSankeyChart(); renderSankeyChart();
// Update Industry List (Chart 2 and List) // 政治献金领域分布:根据配置选择静态数据或接口数据
if (useStaticIndustryData) {
// 饼图数据(左侧圆环)
chart2Data.value = staticChart2Data;
const chart2Static = getPieChart(chart2Data.value, chart2ColorList.value);
setChart(chart2Static, "chart2");
// 右侧列表(Top5 领域)
areaList.value = staticAreaList.map(item => ({
name: item.name,
num: item.amount,
numtext: `$${item.amount.toLocaleString()}`,
insNum: item.insNum
}));
} else {
// 使用接口返回的行业分布
const industries = res.data.industryList || []; const industries = res.data.industryList || [];
// Update Chart 2 Data
chart2Data.value = industries.map(item => ({ chart2Data.value = industries.map(item => ({
name: item.industryName, name: item.industryName,
value: item.amount value: item.amount
})); }));
// Re-render Chart 2 const chart2Dynamic = getPieChart(chart2Data.value, chart2ColorList.value);
let chart2 = getPieChart(chart2Data.value, chart2ColorList.value); setChart(chart2Dynamic, "chart2");
setChart(chart2, "chart2");
// Update List Data
// Sort by amount desc to ensure first item is max for progress bar
const sortedIndustries = [...industries].sort((a, b) => (b.amount || 0) - (a.amount || 0)); const sortedIndustries = [...industries].sort((a, b) => (b.amount || 0) - (a.amount || 0));
areaList.value = sortedIndustries.map(item => ({ areaList.value = sortedIndustries.map(item => ({
...@@ -705,6 +744,7 @@ const getPersonPoliContribution = async personId => { ...@@ -705,6 +744,7 @@ const getPersonPoliContribution = async personId => {
numtext: `$${(item.amount || 0).toLocaleString()}`, numtext: `$${(item.amount || 0).toLocaleString()}`,
insNum: item.orgNum insNum: item.orgNum
})); }));
}
} else { } else {
personPoliContribution.value = []; personPoliContribution.value = [];
fullSourceList.value = []; fullSourceList.value = [];
...@@ -712,8 +752,8 @@ const getPersonPoliContribution = async personId => { ...@@ -712,8 +752,8 @@ const getPersonPoliContribution = async personId => {
chart2Data.value = []; chart2Data.value = [];
areaList.value = []; areaList.value = [];
let chart2 = getPieChart([], chart2ColorList.value); const chart2Empty = getPieChart([], chart2ColorList.value);
setChart(chart2, "chart2"); setChart(chart2Empty, "chart2");
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
...@@ -723,8 +763,8 @@ const getPersonPoliContribution = async personId => { ...@@ -723,8 +763,8 @@ const getPersonPoliContribution = async personId => {
chart2Data.value = []; chart2Data.value = [];
areaList.value = []; areaList.value = [];
let chart2 = getPieChart([], chart2ColorList.value); const chart2Error = getPieChart([], chart2ColorList.value);
setChart(chart2, "chart2"); setChart(chart2Error, "chart2");
} }
}; };
......
...@@ -8,30 +8,87 @@ const getSankeyChart = (data = [], links = []) => { ...@@ -8,30 +8,87 @@ const getSankeyChart = (data = [], links = []) => {
}, },
series: { series: {
type: 'sankey', type: 'sankey',
layout: 'none', // 禁止鼠标拖拽/缩放平移,避免图表被交互改变
left: '1%', draggable: false,
right: '1%', roam: false,
top: '5%', left: 16,
bottom: '5%', right: 16,
top: 14,
bottom: 14,
nodeAlign: 'right', // 目标效果:人名贴右侧
emphasis: { emphasis: {
focus: 'adjacency' focus: 'adjacency'
}, },
nodeWidth: 50, // 所有色块宽度增加三倍
nodeGap: 2, nodeWidth: 54,
nodeGap: 10,
layoutIterations: 32, layoutIterations: 32,
lineStyle: { lineStyle: {
color: 'source', color: 'rgba(0, 0, 0, 0.08)', // 浅灰“行背景带”的基色
curveness: 0.5 opacity: 1,
curveness: 0.2
}, },
label: { label: {
show: true, show: true,
formatter: function (params) { formatter: function (params) {
return `${params.name} $${params.value.toLocaleString()}`; return `${params.name}`;
}, },
position: 'right', position: 'right',
fontSize: 16, fontSize: 16,
color: '#555' color: '#303133',
overflow: 'truncate',
width: 140
}, },
edgeLabel: {
show: true,
formatter: function (params) {
const val = params.value || 0;
return `$${Number(val).toLocaleString()}`;
},
color: '#303133',
fontSize: 16,
overflow: 'truncate',
width: 140,
align: 'center'
},
itemStyle: {
borderWidth: 0
},
nodeSort: 'descending',
levels: [
// depth = 0(机构节点)
{
depth: 0,
itemStyle: {
borderWidth: 0
},
label: {
position: 'right',
color: '#303133',
fontSize: 16,
overflow: 'truncate',
width: 140
}
},
// depth = 1(人物节点)
{
depth: 1,
itemStyle: {
color: '#34C38F'
},
label: {
// 人物名称移至色块左侧
position: 'left',
// 让文字最右侧紧贴绿色色块左侧
distance: 5,
color: '#303133',
fontSize: 16,
overflow: 'truncate',
width: 120,
align: 'right'
}
}
],
data: data, data: data,
links: links links: links
} }
......
...@@ -2049,6 +2049,7 @@ onMounted(async () => { ...@@ -2049,6 +2049,7 @@ onMounted(async () => {
margin-top: 21px; margin-top: 21px;
height: 450px; height: 450px;
display: flex; display: flex;
gap:16px;
.box3 { .box3 {
width: 792px; width: 792px;
......
...@@ -274,7 +274,7 @@ ...@@ -274,7 +274,7 @@
<el-row :gutter="20" style="width: 1600px; margin: 0 auto; height: 528px; margin-top: 64px"> <el-row :gutter="20" style="width: 1600px; margin: 0 auto; height: 528px; margin-top: 64px">
<CustomTitle id="position3" title="数据总览" /> <CustomTitle id="position3" title="数据总览" />
<el-col :span="24"> <el-col :span="24">
<custom-container title="发布频度" :titleIcon="box3Icon" height="450px"> <custom-container title="发布频度" :titleIcon="box3Icon" height="400px">
<template #default> <template #default>
<div class="box3"> <div class="box3">
<div class="box3-content"> <div class="box3-content">
......
<template> <template>
<div class="deep-mining" :class="{ deepMiningChartmode: isInChart }"> <div class="deep-mining" :class="{ deepMiningChartmode: isInChart }">
<div class="side-nav"> <div class="side-nav">
<div v-for="(item, index) in activeTab" :key="index" class="tab-item" :class="{ active: index === activeIndex }" <div
@click="activeIndex = index"> v-for="(item, index) in activeTab"
:key="index"
class="tab-item"
:class="{ active: index === activeIndex }"
@click="activeIndex = index"
>
{{ item }} {{ item }}
<span v-if="index === activeIndex" class="arrow"></span> <span v-if="index === activeIndex" class="arrow"></span>
</div> </div>
...@@ -47,8 +52,13 @@ ...@@ -47,8 +52,13 @@
<span class="group-count">{{ group.count }}家</span> <span class="group-count">{{ group.count }}家</span>
</div> </div>
<div class="group-children custom-scrollbar" v-show="group.expanded"> <div class="group-children custom-scrollbar" v-show="group.expanded">
<div class="entity-item" v-for="item in group.children" :key="item.id" <div
:class="{ active: activeEntityId === item.id }" @click="selectEntity(item)"> class="entity-item"
v-for="item in group.children"
:key="item.id"
:class="{ active: activeEntityId === item.id }"
@click="selectEntity(item)"
>
<div class="item-icon"> <div class="item-icon">
<img :src="defaultTitle" alt="" class="item-img" /> <img :src="defaultTitle" alt="" class="item-img" />
</div> </div>
...@@ -63,12 +73,19 @@ ...@@ -63,12 +73,19 @@
<div class="right" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave"> <div class="right" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
<div class="title-com custom-right-header"> <div class="title-com custom-right-header">
<div class="toggle-group"> <div class="toggle-group">
<div class="toggle-btn" :class="{ active: rightActiveTab === 'supplyChain' }" <div
@click="rightActiveTab = 'supplyChain'"> class="toggle-btn"
:class="{ active: rightActiveTab === 'supplyChain' }"
@click="rightActiveTab = 'supplyChain'"
>
<img :src="rightActiveTab === 'supplyChain' ? icon01Active : icon01" alt="" /> <img :src="rightActiveTab === 'supplyChain' ? icon01Active : icon01" alt="" />
<span>供应链</span> <span>供应链</span>
</div> </div>
<div class="toggle-btn" :class="{ active: rightActiveTab === 'equity' }" @click="rightActiveTab = 'equity'"> <div
class="toggle-btn"
:class="{ active: rightActiveTab === 'equity' }"
@click="rightActiveTab = 'equity'"
>
<img :src="rightActiveTab === 'equity' ? icon02Active : icon02" alt="" /> <img :src="rightActiveTab === 'equity' ? icon02Active : icon02" alt="" />
<span>股权</span> <span>股权</span>
</div> </div>
...@@ -115,8 +132,8 @@ import { ...@@ -115,8 +132,8 @@ import {
getSingleSanctionEntitySupplyChain, getSingleSanctionEntitySupplyChain,
getSingleSanctionEntityEquity getSingleSanctionEntityEquity
} from "@/api/exportControlV2.0"; } from "@/api/exportControlV2.0";
import RelationGraph from './components/RelationGraph.vue'; import RelationGraph from "./components/RelationGraph.vue";
import AnalysisBox from '@/components/base/boxBackground/analysisBox.vue'; import AnalysisBox from "@/components/base/boxBackground/analysisBox.vue";
const sanRecordId = ref(""); const sanRecordId = ref("");
const activeTab = ref(["实体穿透分析"]); const activeTab = ref(["实体穿透分析"]);
...@@ -170,11 +187,11 @@ const handleMouseLeave = () => { ...@@ -170,11 +187,11 @@ const handleMouseLeave = () => {
isInChart.value = false; isInChart.value = false;
}; };
const handleNodeClick = (node) => { const handleNodeClick = node => {
selectedNode.value = node; selectedNode.value = node;
}; };
const handleLayoutChange = (type) => { const handleLayoutChange = type => {
controlActive.value = type; controlActive.value = type;
if (type !== 2) { if (type !== 2) {
isInChart.value = true; isInChart.value = true;
...@@ -184,9 +201,8 @@ const handleLayoutChange = (type) => { ...@@ -184,9 +201,8 @@ const handleLayoutChange = (type) => {
}; };
const updateGraphData = () => { const updateGraphData = () => {
const data = rightActiveTab.value === 'supplyChain' const data =
? singleSanctionEntitySupplyChainData.value rightActiveTab.value === "supplyChain" ? singleSanctionEntitySupplyChainData.value : singleSanctionEntityEquityData.value;
: singleSanctionEntityEquityData.value;
if (!data) return; if (!data) return;
...@@ -214,7 +230,7 @@ const updateGraphData = () => { ...@@ -214,7 +230,7 @@ const updateGraphData = () => {
links.push({ links.push({
source: `p-${item.id || index}`, source: `p-${item.id || index}`,
target: "0", target: "0",
name: rightActiveTab.value === 'supplyChain' ? "供应商" : (item.type || "持股") name: rightActiveTab.value === "supplyChain" ? "供应商" : item.type || "持股"
}); });
}); });
...@@ -231,14 +247,14 @@ const updateGraphData = () => { ...@@ -231,14 +247,14 @@ const updateGraphData = () => {
links.push({ links.push({
source: "0", source: "0",
target: `c-${item.id || index}`, target: `c-${item.id || index}`,
name: rightActiveTab.value === 'supplyChain' ? "客户" : (item.type || "投资") name: rightActiveTab.value === "supplyChain" ? "客户" : item.description || "投资"
}); });
}); });
graphData.value = { nodes, links }; graphData.value = { nodes, links };
}; };
const updateTreeData = (data) => { const updateTreeData = data => {
if (!data) return; if (!data) return;
treeData.value = { treeData.value = {
...@@ -318,17 +334,17 @@ const getSingleSanctionEntityListRequest = async () => { ...@@ -318,17 +334,17 @@ const getSingleSanctionEntityListRequest = async () => {
} }
}; };
watch(rightActiveTab, async (newTab) => { watch(rightActiveTab, async newTab => {
if (newTab === 'supplyChain') { if (newTab === "supplyChain") {
await getSingleSanctionEntitySupplyChainRequest(); await getSingleSanctionEntitySupplyChainRequest();
} else { } else {
await getSingleSanctionEntityEquityRequest(); await getSingleSanctionEntityEquityRequest();
} }
}); });
watch(activeEntityId, async (newId) => { watch(activeEntityId, async newId => {
if (newId) { if (newId) {
if (rightActiveTab.value === 'supplyChain') { if (rightActiveTab.value === "supplyChain") {
await getSingleSanctionEntitySupplyChainRequest(); await getSingleSanctionEntitySupplyChainRequest();
} else { } else {
await getSingleSanctionEntityEquityRequest(); await getSingleSanctionEntityEquityRequest();
...@@ -337,7 +353,7 @@ watch(activeEntityId, async (newId) => { ...@@ -337,7 +353,7 @@ watch(activeEntityId, async (newId) => {
}); });
watch(is50PercentRule, async () => { watch(is50PercentRule, async () => {
if (rightActiveTab.value === 'equity') { if (rightActiveTab.value === "equity") {
await getSingleSanctionEntityEquityRequest(); await getSingleSanctionEntityEquityRequest();
} }
}); });
...@@ -857,7 +873,6 @@ onMounted(async () => { ...@@ -857,7 +873,6 @@ onMounted(async () => {
} }
.title-com.custom-right-header { .title-com.custom-right-header {
.box, .box,
.text { .text {
display: none; display: none;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论