| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779 |
- <script lang="ts" setup>
- import * as echarts from 'echarts'
- import {
- Checked,
- CollectionTag,
- DataAnalysis,
- DocumentChecked,
- Files,
- Histogram,
- Postcard
- } from '@element-plus/icons-vue'
- import {
- ANIMATION,
- CHART_RENDERER,
- DESIGN_HEIGHT,
- DESIGN_WIDTH,
- FONT_FAMILY,
- THEME,
- createTooltip
- } from '@/utils/kb'
- import '@vuepic/vue-datepicker/dist/main.css'
- defineOptions({ name: 'HomeIndex' })
- type SummaryCard = {
- title: string
- value: string | number
- note: string
- accent: string
- glow: string
- icon: any
- }
- type ProgressItem = {
- label: string
- value: number
- color: string
- }
- const wrapperRef = ref<HTMLDivElement>()
- const scale = ref(1)
- const supportsZoom = ref(false)
- let resizeObserver: ResizeObserver | null = null
- let resizeRaf = 0
- const summaryCards: SummaryCard[] = [
- {
- title: '专家数量',
- value: 150,
- note: '较上月 +1.0%',
- accent: '#4f8dff',
- glow: 'rgba(79,141,255,.22)',
- icon: Postcard
- },
- {
- title: '科研项目数量',
- value: 10,
- note: '较上月 +0.0%',
- accent: '#43c7ca',
- glow: 'rgba(67,199,202,.22)',
- icon: DocumentChecked
- },
- {
- title: '核心技术',
- value: 8,
- note: '较上月 +0.0%',
- accent: '#ffb14a',
- glow: 'rgba(255,177,74,.22)',
- icon: DataAnalysis
- },
- {
- title: '专利',
- value: 18,
- note: '较上月 +150.0%',
- accent: '#4f8dff',
- glow: 'rgba(79,141,255,.22)',
- icon: Histogram
- },
- {
- title: '论文',
- value: 28,
- note: '较上月 +17.0%',
- accent: '#8d8cff',
- glow: 'rgba(141,140,255,.22)',
- icon: CollectionTag
- },
- {
- title: '荣誉',
- value: 38,
- note: '较上月 +100.0%',
- accent: '#ff93dd',
- glow: 'rgba(255,147,221,.22)',
- icon: Checked
- },
- {
- title: '资料库',
- value: 3800,
- note: '较上月 +100.0%',
- accent: '#4bc7ff',
- glow: 'rgba(75,199,255,.22)',
- icon: Files
- }
- ]
- const progressItems: ProgressItem[] = [
- { label: '审批中', value: 7, color: '#2e9f8c' },
- { label: '生效中', value: 18, color: '#208d78' },
- { label: '已验收', value: 12, color: '#2b6aa8' },
- { label: '已终止', value: 2, color: '#9da8a1' },
- { label: '延期', value: 5, color: '#cf4a4a' }
- ]
- const donutData = [
- { name: '进行中', value: 54, color: '#f35e5c' },
- { name: '结题', value: 28, color: '#8dd46c' },
- { name: '终止', value: 5, color: '#5578d4' },
- { name: '延期', value: 7, color: '#8f7cff' }
- ]
- const expertSeries = [
- { name: '技术', value: 195 },
- { name: '经营', value: 116 },
- { name: '商务', value: 86 },
- { name: '安全', value: 61 },
- { name: '法务', value: 50 },
- { name: '研发', value: 34 },
- { name: '质量', value: 23 },
- { name: '综合', value: 8 },
- { name: '其他', value: 6 }
- ]
- const budgetSeries = {
- month: [
- { date: '07-31', value: 61 },
- { date: '08-01', value: 64 },
- { date: '08-02', value: 63 },
- { date: '08-03', value: 62 },
- { date: '08-04', value: 60 },
- { date: '08-05', value: 62 },
- { date: '08-06', value: 62 }
- ],
- quarter: [
- { date: '06-30', value: 58 },
- { date: '07-15', value: 60 },
- { date: '07-31', value: 61 },
- { date: '08-10', value: 62 },
- { date: '08-20', value: 63 },
- { date: '08-30', value: 64 },
- { date: '09-10', value: 63 }
- ],
- year: [
- { date: '01', value: 57 },
- { date: '03', value: 58 },
- { date: '05', value: 59 },
- { date: '07', value: 61 },
- { date: '09', value: 62 },
- { date: '11', value: 63 },
- { date: '12', value: 64 }
- ]
- } as const
- const period = ref<keyof typeof budgetSeries>('month')
- const dateText = '2026-08-06'
- const donutRef = ref<HTMLDivElement | null>(null)
- const barRef = ref<HTMLDivElement | null>(null)
- const lineRef = ref<HTMLDivElement | null>(null)
- let donutChart: echarts.ECharts | null = null
- let barChart: echarts.ECharts | null = null
- let lineChart: echarts.ECharts | null = null
- const targetWrapperStyle = computed(() => ({
- width: `${DESIGN_WIDTH * scale.value}px`,
- height: `${DESIGN_HEIGHT * scale.value}px`
- }))
- const targetAreaStyle = computed(() => {
- const style = {
- width: `${DESIGN_WIDTH}px`,
- height: `${DESIGN_HEIGHT}px`,
- transformOrigin: '0 0'
- } as Record<string, string | number>
- if (supportsZoom.value) {
- style.zoom = scale.value
- } else {
- style.transform = `scale(${scale.value})`
- }
- return style
- })
- function updateScale() {
- cancelAnimationFrame(resizeRaf)
- resizeRaf = requestAnimationFrame(() => {
- const wrapper = wrapperRef.value
- if (!wrapper) return
- const { clientWidth, clientHeight } = wrapper
- if (!clientWidth || !clientHeight) return
- scale.value = Math.min(clientWidth / DESIGN_WIDTH, clientHeight / DESIGN_HEIGHT)
- nextTick(() => resizeCharts())
- })
- }
- function createDonutOption(): echarts.EChartsOption {
- return {
- ...ANIMATION,
- tooltip: createTooltip({ trigger: 'item' }),
- legend: {
- bottom: 0,
- icon: 'rect',
- itemWidth: 8,
- itemHeight: 8,
- textStyle: { color: '#5f6f88', fontSize: 11, fontFamily: FONT_FAMILY }
- },
- series: [
- {
- type: 'pie',
- radius: ['46%', '70%'],
- center: ['50%', '46%'],
- avoidLabelOverlap: false,
- label: {
- formatter: '{b|{b}} {c|{d}%}',
- rich: {
- b: { color: '#3e5575', fontWeight: 700, fontSize: 11 },
- c: { color: '#3e5575', fontWeight: 700, fontSize: 11 }
- }
- },
- labelLine: { length: 12, length2: 16 },
- itemStyle: { borderRadius: 8, borderColor: 'transparent', borderWidth: 4 },
- data: donutData.map((item) => ({
- value: item.value,
- name: item.name,
- itemStyle: { color: item.color }
- }))
- }
- ]
- }
- }
- function createBarOption(): echarts.EChartsOption {
- return {
- ...ANIMATION,
- grid: { left: 34, right: 18, top: 18, bottom: 0, containLabel: true },
- tooltip: createTooltip({ trigger: 'axis', axisPointer: { type: 'shadow' } }),
- xAxis: {
- type: 'value',
- max: 200,
- splitLine: { lineStyle: { color: THEME.split } },
- axisLine: { show: false },
- axisTick: { show: false },
- axisLabel: { color: '#51637f', fontSize: 11, fontFamily: FONT_FAMILY }
- },
- yAxis: {
- type: 'category',
- data: expertSeries.map((item) => item.name),
- axisLine: { show: false },
- axisTick: { show: false },
- axisLabel: { fontSize: 11, fontWeight: 700, fontFamily: FONT_FAMILY }
- },
- series: [
- {
- type: 'bar',
- data: expertSeries.map((item) => item.value).reverse(),
- barWidth: 14,
- itemStyle: {
- borderRadius: [0, 10, 10, 0],
- color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
- { offset: 0, color: '#2f74e8' },
- { offset: 1, color: '#7bb2ff' }
- ])
- },
- label: {
- show: true,
- position: 'right',
- color: '#2f74e8',
- fontWeight: 700,
- fontSize: 11
- }
- }
- ]
- }
- }
- function createLineOption(): echarts.EChartsOption {
- const series = budgetSeries[period.value]
- return {
- ...ANIMATION,
- grid: { left: 22, right: 18, top: 22, bottom: 0, containLabel: true },
- tooltip: createTooltip({ trigger: 'axis' }),
- xAxis: {
- type: 'category',
- boundaryGap: false,
- data: series.map((item) => item.date),
- axisLine: { lineStyle: { color: 'rgba(106,144,221,0.45)' } },
- axisTick: { show: false },
- axisLabel: { color: '#5b6f8f', fontSize: 11, fontFamily: FONT_FAMILY }
- },
- yAxis: {
- type: 'value',
- min: 0,
- max: 100,
- splitLine: { lineStyle: { color: 'rgba(104,139,205,0.22)', type: 'dashed' } },
- axisLabel: { color: '#8a9bb5', fontSize: 11, fontFamily: FONT_FAMILY }
- },
- series: [
- {
- type: 'line',
- smooth: true,
- symbol: 'circle',
- symbolSize: 7,
- lineStyle: { color: '#2e9e5f', width: 3 },
- itemStyle: { color: '#2e9e5f' },
- areaStyle: { color: 'rgba(46,158,95,0.14)' },
- data: series.map((item) => item.value),
- label: {
- show: true,
- position: 'top',
- color: '#2e9e5f',
- fontSize: 10,
- formatter: '{c}.00%'
- }
- }
- ]
- }
- }
- function initCharts() {
- if (donutRef.value) {
- donutChart?.dispose()
- donutChart = echarts.init(donutRef.value, undefined, { renderer: CHART_RENDERER })
- donutChart.setOption(createDonutOption(), true)
- }
- if (barRef.value) {
- barChart?.dispose()
- barChart = echarts.init(barRef.value, undefined, { renderer: CHART_RENDERER })
- barChart.setOption(createBarOption(), true)
- }
- if (lineRef.value) {
- lineChart?.dispose()
- lineChart = echarts.init(lineRef.value, undefined, { renderer: CHART_RENDERER })
- lineChart.setOption(createLineOption(), true)
- }
- }
- function resizeCharts() {
- donutChart?.resize({ animation: { duration: 180 } })
- barChart?.resize({ animation: { duration: 180 } })
- lineChart?.resize({ animation: { duration: 180 } })
- }
- watch(period, () => {
- nextTick(() => {
- lineChart?.setOption(createLineOption(), true)
- })
- })
- onMounted(() => {
- initCharts()
- updateScale()
- resizeObserver = new ResizeObserver(updateScale)
- if (wrapperRef.value) resizeObserver.observe(wrapperRef.value)
- window.addEventListener('resize', updateScale)
- })
- onBeforeUnmount(() => {
- resizeObserver?.disconnect()
- window.removeEventListener('resize', updateScale)
- donutChart?.dispose()
- barChart?.dispose()
- lineChart?.dispose()
- })
- </script>
- <template>
- <div ref="wrapperRef" class="bg absolute top-0 left-0 size-full z-10">
- <div class="mx-a overflow-hidden" :style="targetWrapperStyle">
- <div id="qhse-kanban" class="bg qhse-board" :style="targetAreaStyle">
- <header class="header" style="font-family: YouSheBiaoTiHei, sans-serif !important"
- >技术研发</header
- >
- <div class="summary-toolbar">
- <div class="summary-toolbar__tabs"> </div>
- <div class="summary-toolbar__date">
- <span class="summary-toolbar__date-label">日期:</span>
- <strong class="summary-toolbar__date-value">{{ dateText }}</strong>
- <button
- type="button"
- class="summary-toolbar__tab summary-toolbar__tab--mini is-active"
- @click="period = 'month'"
- >月度</button
- >
- <button
- type="button"
- class="summary-toolbar__tab summary-toolbar__tab--mini"
- @click="period = 'quarter'"
- >季度</button
- >
- <button
- type="button"
- class="summary-toolbar__tab summary-toolbar__tab--mini"
- @click="period = 'year'"
- >年度</button
- >
- <button
- type="button"
- class="summary-toolbar__tab summary-toolbar__tab--mini"
- @click="period = 'month'"
- >初始化</button
- >
- </div>
- </div>
- <div class="board-body">
- <section class="panel summary-panel kb-stage-card kb-stage-card--1">
- <div class="panel-title">
- <span class="icon-decorator"><span></span><span></span></span>
- 技术研发概况
- </div>
- <div class="summary-grid summary-grid--tight">
- <article
- v-for="card in summaryCards"
- :key="card.title"
- class="summary-card summary-tile"
- :style="
- {
- '--card-accent': card.accent,
- '--card-glow': card.glow
- } as any
- "
- >
- <span class="summary-card__shine"></span>
- <span class="summary-card__corner"></span>
- <div class="summary-card__header">
- <div class="summary-card__icon">
- <el-icon class="summary-card__icon-glyph">
- <component :is="card.icon" />
- </el-icon>
- </div>
- <div class="summary-card__label">{{ card.title }}</div>
- </div>
- <div class="summary-card__content">
- <div class="summary-tile__meta">
- <span class="summary-tile__value">月度累计 {{ card.value }}</span>
- <span class="summary-tile__note" :style="{ color: card.accent }">{{
- card.note
- }}</span>
- </div>
- </div>
- </article>
- </div>
- </section>
- <div class="board-main board-main--static">
- <section class="panel board-panel kb-stage-card kb-stage-card--3">
- <div class="panel-title panel-title--center">
- <span class="icon-decorator"><span></span><span></span></span>
- 项目进度统计
- </div>
- <div
- style="height: 308px"
- ref="donutRef"
- class="chart-panel chart-panel--echart chart-panel--compact"
- ></div>
- </section>
- <section class="panel board-panel kb-stage-card kb-stage-card--4">
- <div class="panel-title panel-title--center">
- <span class="icon-decorator"><span></span><span></span></span>
- 专家序列统计
- </div>
- <div
- ref="barRef"
- style="height: 308px"
- class="chart-panel chart-panel--echart chart-panel--compact"
- ></div>
- </section>
- <section class="panel board-panel kb-stage-card kb-stage-card--4">
- <div class="panel-title panel-title--center panel-title--with-actions">
- <span class="icon-decorator"><span></span><span></span></span>
- <span class="panel-title__text">预算费用</span>
- <div class="qhse-metric-tabs qhse-metric-tabs--compact">
- <button
- type="button"
- class="qhse-metric-tab"
- :class="{ 'is-active': period === 'month' }"
- @click="period = 'month'"
- >近一周</button
- >
- <button
- type="button"
- class="qhse-metric-tab"
- :class="{ 'is-active': period === 'quarter' }"
- @click="period = 'quarter'"
- >近六个月</button
- >
- </div>
- </div>
- <div
- ref="lineRef"
- style="height: 308px"
- class="chart-panel chart-panel--echart chart-panel--compact"
- ></div>
- </section>
- </div>
- <section class="panel progress-panel kb-stage-card kb-stage-card--5">
- <div class="panel-title">
- <span class="icon-decorator"><span></span><span></span></span>
- 项目组合进展
- </div>
- <div class="progress-list">
- <div v-for="item in progressItems" :key="item.label" class="progress-item">
- <span class="progress-item__label">{{ item.label }}</span>
- <div class="progress-item__track">
- <div
- class="progress-item__bar"
- :style="{ width: `${item.value * 5}%`, background: item.color }"
- ></div>
- </div>
- <span class="progress-item__value">{{ item.value }}</span>
- </div>
- </div>
- </section>
- </div>
- </div>
- </div>
- </div>
- </template>
- <style lang="scss" scoped>
- @import url('@/styles/qhse_kb.scss');
- .home-board {
- overflow: hidden;
- }
- .summary-toolbar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 8px 18px 4px;
- }
- .summary-toolbar__tabs,
- .summary-toolbar__date {
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .summary-toolbar__tab {
- min-width: 108px;
- height: 34px;
- padding: 0 16px;
- font-family: YouSheBiaoTiHei, sans-serif;
- font-size: 14px;
- color: #2b76e9;
- background: linear-gradient(180deg, rgb(255 255 255 / 92%) 0%, rgb(228 239 255 / 90%) 100%);
- border: 1px solid rgb(126 174 244 / 40%);
- border-radius: 10px;
- box-shadow:
- inset 0 1px 0 rgb(255 255 255 / 96%),
- 0 6px 12px rgb(63 103 171 / 8%);
- }
- .summary-toolbar__tab--mini {
- min-width: 68px;
- height: 30px;
- padding: 0 10px;
- font-size: 12px;
- }
- .summary-toolbar__tab.is-active {
- color: #fff;
- background: linear-gradient(180deg, #59a6ff 0%, #2d78ea 100%);
- }
- .summary-toolbar__date-label {
- font-size: 14px;
- font-weight: 700;
- color: #5c7393;
- }
- .summary-toolbar__date-value {
- font-size: 13px;
- font-weight: 700;
- color: #264564;
- }
- .board-body {
- padding: 4px 14px 12px;
- }
- .summary-panel {
- height: auto;
- }
- .summary-grid--tight {
- display: grid;
- grid-template-columns: repeat(7, minmax(0, 1fr));
- gap: 10px;
- padding: 4px 0 0;
- width: 100%;
- padding-bottom: 10px;
- }
- .summary-card {
- min-height: 130px;
- padding: 16px 14px 12px 18px;
- border-radius: 12px;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- gap: 10px;
- }
- .summary-card__header {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .summary-card__icon {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 34px;
- height: 34px;
- border-radius: 10px;
- background: linear-gradient(135deg, rgb(255 255 255 / 74%) 0%, rgb(233 242 255 / 92%) 100%);
- box-shadow: inset 0 1px 0 rgb(255 255 255 / 90%);
- }
- .summary-card__icon-glyph {
- font-size: 16px;
- color: #2f74de;
- }
- .summary-card__content {
- display: flex;
- flex: 1;
- flex-direction: column;
- justify-content: space-between;
- gap: 8px;
- }
- .summary-card__title,
- .summary-card__label {
- font-size: 14px;
- font-weight: 600;
- line-height: 1.4;
- color: #26425f;
- }
- .summary-tile__meta {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- gap: 4px;
- margin-top: auto;
- }
- .summary-tile__value {
- font-size: 18px;
- font-weight: 700;
- line-height: 1.2;
- letter-spacing: 0.02em;
- color: #0f2f54;
- }
- .summary-tile__note {
- font-size: 12px;
- font-weight: 600;
- line-height: 1.45;
- color: #6b7b92;
- }
- .board-main--static {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 12px;
- margin-top: 10px;
- height: 408px;
- }
- .board-panel {
- min-height: 272px;
- }
- .chart-panel--compact {
- height: 208px;
- }
- .panel-title--with-actions {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 10px;
- }
- .panel-title__text {
- flex: 1;
- text-align: left;
- }
- .qhse-metric-tabs--compact {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 4px;
- border: 1px solid rgb(121 169 242 / 40%);
- border-radius: 999px;
- background: linear-gradient(180deg, rgb(246 250 255 / 96%) 0%, rgb(232 241 255 / 92%) 100%);
- box-shadow: inset 0 1px 0 rgb(255 255 255 / 90%);
- }
- .qhse-metric-tabs--compact .qhse-metric-tab {
- min-height: 24px;
- padding: 0 10px;
- font-size: 12px;
- font-weight: 600;
- color: #5c7393;
- background: transparent;
- border: 0;
- border-radius: 999px;
- cursor: pointer;
- transition:
- background-color 0.2s ease,
- color 0.2s ease,
- box-shadow 0.2s ease;
- }
- .qhse-metric-tabs--compact .qhse-metric-tab.is-active {
- color: #fff;
- background: linear-gradient(135deg, #5aa8ff 0%, #2d78ea 100%);
- box-shadow: 0 4px 10px rgb(45 120 234 / 20%);
- }
- .progress-panel {
- margin-top: 10px;
- padding-bottom: 10px;
- height: 352px;
- }
- .progress-list {
- padding: 12px 12px 4px;
- }
- .progress-item {
- display: grid;
- grid-template-columns: 70px 1fr 28px;
- align-items: center;
- gap: 10px;
- margin-bottom: 10px;
- font-size: 12px;
- font-weight: 700;
- }
- .progress-item__track {
- height: 18px;
- border-radius: 999px;
- background: #e4ece7;
- overflow: hidden;
- }
- .progress-item__bar {
- height: 100%;
- border-radius: inherit;
- }
- .progress-item__value {
- text-align: right;
- }
- </style>
|