Compare commits
No commits in common. "main" and "xingyy" have entirely different histories.
@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/assets/image/icon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>FiEE</title>
|
||||
</head>
|
||||
|
@ -17,8 +17,6 @@
|
||||
"@vicons/utils": "^0.1.4",
|
||||
"axios": "^1.7.3",
|
||||
"cnjm-postcss-px-to-viewport": "^1.0.1",
|
||||
"countup.js": "^2.8.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.6.0",
|
||||
"gsap": "^3.12.5",
|
||||
"jsdom": "^24.0.0",
|
||||
@ -51,7 +49,6 @@
|
||||
"unplugin-auto-import": "^0.18.2",
|
||||
"unplugin-vue-components": "^0.27.3",
|
||||
"vite": "^5.3.4",
|
||||
"vite-plugin-imagemin": "^0.6.1",
|
||||
"vite-plugin-vue-devtools": "^7.7.6"
|
||||
"vite-plugin-imagemin": "^0.6.1"
|
||||
}
|
||||
}
|
||||
|
797
pnpm-lock.yaml
Before Width: | Height: | Size: 117 KiB |
BIN
public/icon.png
Normal file
After Width: | Height: | Size: 105 KiB |
BIN
src/assets/file/2025 Q1 Quarterly Results.pdf
Normal file
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 8.7 KiB |
BIN
src/assets/images/header/logo.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
@ -1,55 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { CountUp } from 'countup.js'
|
||||
|
||||
const props = defineProps({
|
||||
endVal: {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 1.2
|
||||
},
|
||||
decimals: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
suffix: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const el = ref(null)
|
||||
let countUpInstance = null
|
||||
|
||||
const start = (val) => {
|
||||
if (countUpInstance) countUpInstance.update(val)
|
||||
else {
|
||||
countUpInstance = new CountUp(el.value, val, {
|
||||
duration: props.duration,
|
||||
decimalPlaces: props.decimals,
|
||||
prefix: props.prefix,
|
||||
suffix: props.suffix
|
||||
})
|
||||
countUpInstance.start()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
start(props.endVal)
|
||||
})
|
||||
|
||||
watch(() => props.endVal, (val) => {
|
||||
start(val)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span ref="el"></span>
|
||||
</template>
|
@ -3,8 +3,6 @@ import { computed } from 'vue'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
|
||||
import size375 from '@/components/customDefaultPage/size375/index.vue'
|
||||
import size768 from '@/components/customDefaultPage/size1920/index.vue'
|
||||
import size1440 from '@/components/customDefaultPage/size1920/index.vue'
|
||||
import size1920 from '@/components/customDefaultPage/size1920/index.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@ -17,22 +15,20 @@ const viewComponent = computed(() => {
|
||||
const viewWidth = width.value
|
||||
if (viewWidth <= 450) {
|
||||
return size375
|
||||
} else if (viewWidth <= 1100) {
|
||||
return size768
|
||||
} else if (viewWidth <= 1500) {
|
||||
return size1440
|
||||
} else if (viewWidth <= 1920 || viewWidth > 1920) {
|
||||
}
|
||||
// else if (viewWidth <= 1100) {
|
||||
// return size768;
|
||||
// } else if (viewWidth <= 1500) {
|
||||
// return size1440;
|
||||
// }
|
||||
else if (viewWidth <= 1920 || viewWidth > 1920) {
|
||||
return size1920
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="viewComponent" v-bind="$attrs">
|
||||
<template v-for="(_, slot) in $slots" #[slot]="scope">
|
||||
<slot :name="slot" v-bind="scope" />
|
||||
</template>
|
||||
</component>
|
||||
<component :is="viewComponent" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
@ -1,27 +1,18 @@
|
||||
<template>
|
||||
<!-- 通用缺省页 -->
|
||||
<div class="custom-default-page">
|
||||
<slot name="content"></slot>
|
||||
<template v-if="$slots.empty">
|
||||
<slot name="empty"></slot>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="default-empty-icon">
|
||||
<img
|
||||
src="@/assets/image/icon/default-empty.png"
|
||||
alt="empty"
|
||||
style="width: 476px; height: 262px;"
|
||||
/>
|
||||
<!-- 通用缺省页 -->
|
||||
<div class="custom-default-page">
|
||||
<div class="search-area">
|
||||
<customSelectSearch></customSelectSearch>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup></script>
|
||||
<style scoped lang="scss">
|
||||
.default-empty-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 40px 0 24px 0;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import customSelectSearch from '@/components/customSelectSearch/index.vue'
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.search-area {
|
||||
width: 300px;
|
||||
margin: 100px 300px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,27 +1,18 @@
|
||||
<template>
|
||||
<!-- 通用缺省页 -->
|
||||
<div class="custom-default-page">
|
||||
<slot name="content"></slot>
|
||||
<template v-if="$slots.empty">
|
||||
<slot name="empty"></slot>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="default-empty-icon">
|
||||
<img
|
||||
src="@/assets/image/icon/default-empty.png"
|
||||
alt="empty"
|
||||
style="width: 350px; height: 198px; margin: 40px 0 0;"
|
||||
/>
|
||||
<!-- 通用缺省页 -->
|
||||
<div class="custom-default-page">
|
||||
<div class="search-area">
|
||||
<customSelectSearch></customSelectSearch>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup></script>
|
||||
<style scoped lang="scss">
|
||||
.default-empty-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 40px 0 24px 0;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import customSelectSearch from '@/components/customSelectSearch/index.vue'
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.search-area {
|
||||
width: 1500px;
|
||||
margin: 500px 200px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,34 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
|
||||
import size375 from '@/components/customEcharts/size375/index.vue'
|
||||
import size768 from '@/components/customEcharts/size375/index.vue'
|
||||
import size1440 from '@/components/customEcharts/size1920/index.vue'
|
||||
import size1920 from '@/components/customEcharts/size1920/index.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const router = useRouter()
|
||||
const { width } = useWindowSize()
|
||||
const { t } = useI18n()
|
||||
|
||||
const viewComponent = computed(() => {
|
||||
const viewWidth = width.value
|
||||
if (viewWidth <= 500) {
|
||||
return size375
|
||||
} else if (viewWidth <= 960) {
|
||||
return size768
|
||||
} else if (viewWidth <= 1500) {
|
||||
return size1440
|
||||
} else if (viewWidth <= 1920 || viewWidth > 1920) {
|
||||
return size1920
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="viewComponent" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@ -1,602 +0,0 @@
|
||||
<template>
|
||||
<div class="custom-echarts">
|
||||
<div>
|
||||
<div class="echarts-header">
|
||||
<div class="echarts-header-title">
|
||||
<span>FiEE, Inc. Stock Price History</span>
|
||||
</div>
|
||||
<div class="echarts-search-area">
|
||||
<div class="echarts-search-byRange">
|
||||
<text style="font-size: 0.9rem; font-weight: 400; color: #666666;">
|
||||
Range
|
||||
</text>
|
||||
<div class="search-range-list">
|
||||
<div
|
||||
class="search-range-list-each"
|
||||
v-for="(item, index) in state.searchRange"
|
||||
:key="index"
|
||||
@click="changeSearchRange(item)"
|
||||
>
|
||||
<span>{{ item }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="echarts-search-byDate">
|
||||
<n-date-picker
|
||||
v-model:value="state.selectHistoricStartDate"
|
||||
type="date"
|
||||
:is-date-disabled="disableAfterDate"
|
||||
@update:value="changeSearchRangeStartDate"
|
||||
input-readonly
|
||||
/>
|
||||
<!-- <n-icon size="16">
|
||||
<ArrowForwardOutline />
|
||||
</n-icon> -->
|
||||
<span>to</span>
|
||||
<n-date-picker
|
||||
v-model:value="state.selectHistoricEndDate"
|
||||
type="date"
|
||||
:is-date-disabled="disablePreviousDate"
|
||||
@update:value="changeSearchRangeEndDate"
|
||||
input-readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="myEcharts" class="myChart"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, watch, reactive } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import markPointerIcon from '@/assets/image/icon/echarts_markPointer.png'
|
||||
import axios from 'axios'
|
||||
import { NDatePicker, NIcon } from 'naive-ui'
|
||||
import { ArrowForwardOutline } from '@vicons/ionicons5'
|
||||
|
||||
const state = reactive({
|
||||
searchRange: ['1m', '3m', 'YTD', '1Y', '5Y', '10Y', 'Max'],
|
||||
selectHistoricStartDate: '2009-10-07',
|
||||
selectHistoricEndDate: new Date(),
|
||||
})
|
||||
|
||||
let myCharts = null
|
||||
let historicData = []
|
||||
let xAxisData = []
|
||||
|
||||
//初始化eCharts
|
||||
const initEcharts = (data) => {
|
||||
historicData = data
|
||||
xAxisData = data.map((item) => {
|
||||
return new Date(item.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
})
|
||||
const yAxisData = data.map((item) => item.price)
|
||||
// console.error(xAxisData, yAxisData)
|
||||
// 基于准备好的dom,初始化echarts实例
|
||||
myCharts = echarts.init(document.getElementById('myEcharts'), null, {
|
||||
renderer: 'canvas',
|
||||
useDirtyRect: true
|
||||
})
|
||||
// 绘制图表
|
||||
myCharts.setOption({
|
||||
animation: false,
|
||||
progressive: 500,
|
||||
progressiveThreshold: 3000,
|
||||
// title: {
|
||||
// text: 'FiEE, Inc. Stock Price History',
|
||||
// },
|
||||
grid: {
|
||||
left: '8%', // 或 '2%',根据实际情况调整
|
||||
right: '12%', // 给右侧y轴留空间,数值可根据y轴label宽度调整
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
label: {
|
||||
backgroundColor: '#6a7985',
|
||||
},
|
||||
},
|
||||
formatter: function (params) {
|
||||
const p = params[0]
|
||||
return `<span style="font-size: 1.1rem; font-weight: 600;">${p.axisValue}</span><br/><span style="font-size: 0.9rem; font-weight: 400;">Price: ${p.data}</span>`
|
||||
},
|
||||
triggerOn: 'mousemove',
|
||||
confine: true,
|
||||
hideDelay: 1500
|
||||
},
|
||||
xAxis: {
|
||||
data: xAxisData,
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
inverse: true,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#CCD6EB',
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#323232',
|
||||
fontWeight: 'bold',
|
||||
interval: 'auto',
|
||||
hideOverlap: true
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
position: 'right',
|
||||
interval: 25,
|
||||
// max: 75.0,
|
||||
show: true,
|
||||
axisLabel: {
|
||||
color: '#323232',
|
||||
fontWeight: 'bold',
|
||||
formatter: function (value) {
|
||||
return value > 0 ? value.toFixed(2) : value
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: yAxisData,
|
||||
type: 'line',
|
||||
sampling: 'lttb',
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
color: '#2c6288',
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: '#2c6288',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#F4F6F8',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
markPoint: {
|
||||
symbol: 'image://' + markPointerIcon,
|
||||
symbolSize: 24,
|
||||
data: [],
|
||||
},
|
||||
progressive: 500,
|
||||
progressiveThreshold: 3000,
|
||||
large: true,
|
||||
largeThreshold: 2000
|
||||
},
|
||||
],
|
||||
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
},
|
||||
{
|
||||
type: 'slider',
|
||||
show: true,
|
||||
dataBackground: {
|
||||
lineStyle: {
|
||||
color: '#2C6288',
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 1, color: '#2c6288' },
|
||||
{ offset: 0, color: '#F4F6F8' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
selectedDataBackground: {
|
||||
lineStyle: {
|
||||
color: '#2C6288',
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 1, color: '#2c6288' },
|
||||
{ offset: 0, color: '#F4F6F8' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
fillerColor: 'rgba(44, 98, 136, 0.3)',
|
||||
realtime: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 监听 showTip 事件,动态显示 markPoint
|
||||
myCharts.on('showTip', function (params) {
|
||||
if (params) {
|
||||
const dataIndex = params.dataIndex
|
||||
const x = myCharts.getOption().xAxis[0].data[dataIndex]
|
||||
const y = myCharts.getOption().series[0].data[dataIndex]
|
||||
myCharts.setOption({
|
||||
series: [
|
||||
{
|
||||
markPoint: {
|
||||
symbol: 'image://' + markPointerIcon,
|
||||
symbolSize: 24,
|
||||
data: [{ coord: [x, y] }],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
})
|
||||
// 鼠标移出时,清除 markPoint
|
||||
myCharts.on('globalout', function () {
|
||||
myCharts.setOption({
|
||||
series: [
|
||||
{
|
||||
markPoint: {
|
||||
symbol: 'image://' + markPointerIcon,
|
||||
symbolSize: 24,
|
||||
data: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
myCharts.on('dataZoom', function (params) {
|
||||
// 获取当前 dataZoom 范围
|
||||
const option = myCharts.getOption()
|
||||
const xAxisData = option.xAxis[0].data
|
||||
const dataZoom = option.dataZoom[1] || option.dataZoom[0]
|
||||
|
||||
// 获取 dataZoom 的 startValue 和 endValue
|
||||
let startValue = dataZoom.endValue
|
||||
let endValue = dataZoom.startValue
|
||||
|
||||
// 如果是索引,转为日期
|
||||
if (typeof startValue === 'number') {
|
||||
startValue = xAxisData[startValue]
|
||||
}
|
||||
if (typeof endValue === 'number') {
|
||||
endValue = xAxisData[endValue]
|
||||
}
|
||||
|
||||
// 更新日期选择器
|
||||
state.selectHistoricStartDate = new Date(startValue)
|
||||
state.selectHistoricEndDate = new Date(endValue)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getHistoricalData()
|
||||
})
|
||||
|
||||
//获取历史数据
|
||||
const getHistoricalData = async () => {
|
||||
let now = new Date()
|
||||
let toDate =
|
||||
now.getFullYear() +
|
||||
'-' +
|
||||
String(now.getMonth() + 1).padStart(2, '0') +
|
||||
'-' +
|
||||
String(now.getDate()).padStart(2, '0')
|
||||
let url =
|
||||
'https://common.szjixun.cn/api/stock/history/base/list?from=2009-10-07&to=' +
|
||||
toDate
|
||||
const res = await axios.get(url)
|
||||
if (res.status === 200) {
|
||||
if (res.data.status === 0) {
|
||||
initEcharts(res.data.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 适配倒序数据,返回大于等于目标日期的最近一天索引
|
||||
function findClosestDateIndex(data, targetDateStr) {
|
||||
let left = 0,
|
||||
right = data.length - 1
|
||||
const target = new Date(targetDateStr).getTime()
|
||||
let res = data.length - 1 // 默认返回最后一个
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2)
|
||||
const midTime = new Date(data[mid].date).getTime()
|
||||
if (midTime > target) {
|
||||
left = mid + 1
|
||||
} else {
|
||||
res = mid
|
||||
right = mid - 1
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// 适配倒序数据,返回小于等于目标日期的最近一天索引
|
||||
function findClosestDateIndexDescLeft(data, targetDateStr) {
|
||||
let left = 0,
|
||||
right = data.length - 1
|
||||
const target = new Date(targetDateStr).getTime()
|
||||
let res = -1
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2)
|
||||
const midTime = new Date(data[mid].date).getTime()
|
||||
if (midTime > target) {
|
||||
left = mid + 1 // mid 比目标新,往更旧的方向找
|
||||
} else {
|
||||
res = mid // mid <= target,记录下来,继续往更新的方向找
|
||||
right = mid - 1
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
//点击切换搜索区间
|
||||
const changeSearchRange = (range, dateTime) => {
|
||||
const now = new Date()
|
||||
let startDate = ''
|
||||
let endDate = ''
|
||||
if (range === '1m') {
|
||||
const last = new Date(now)
|
||||
last.setMonth(now.getMonth() - 1)
|
||||
startDate = last.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
} else if (range === '3m') {
|
||||
const last = new Date(now)
|
||||
last.setMonth(now.getMonth() - 3)
|
||||
startDate = last.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
} else if (range === 'YTD') {
|
||||
startDate = new Date(now.getFullYear(), 0, 1).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
} else if (range === '1Y') {
|
||||
const last = new Date(now)
|
||||
last.setFullYear(now.getFullYear() - 1)
|
||||
startDate = last.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
} else if (range === '5Y') {
|
||||
const last = new Date(now)
|
||||
last.setFullYear(now.getFullYear() - 5)
|
||||
startDate = last.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
} else if (range === '10Y') {
|
||||
const last = new Date(now)
|
||||
last.setFullYear(now.getFullYear() - 10)
|
||||
startDate = last.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
} else if (range === 'Max') {
|
||||
startDate = ''
|
||||
endDate = ''
|
||||
} else if (range === 'startDateTime') {
|
||||
startDate = dateTime
|
||||
endDate = ''
|
||||
} else if (range === 'endDateTime') {
|
||||
startDate = ''
|
||||
endDate = dateTime
|
||||
}
|
||||
if (startDate || endDate) {
|
||||
// historicData 和 xAxisData 需在 initEcharts 作用域可用
|
||||
if (
|
||||
typeof historicData !== 'undefined' &&
|
||||
typeof xAxisData !== 'undefined'
|
||||
) {
|
||||
let startValue = xAxisData[0]
|
||||
if (startDate) {
|
||||
const idx = findClosestDateIndex(historicData, startDate)
|
||||
// 用 historicData[idx].date 格式化为 xAxisData 的格式
|
||||
startValue = new Date(historicData[idx].date).toLocaleDateString(
|
||||
'en-US',
|
||||
{
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
},
|
||||
)
|
||||
}
|
||||
let endValue = endDate
|
||||
if (endDate) {
|
||||
// console.warn(endDate)
|
||||
const idx = findClosestDateIndexDescLeft(historicData, endDate)
|
||||
// console.warn(idx)
|
||||
// 用 historicData[idx].date 格式化为 xAxisData 的格式
|
||||
endValue = new Date(historicData[idx].date).toLocaleDateString(
|
||||
'en-US',
|
||||
{
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
},
|
||||
)
|
||||
// console.warn(endValue)
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
myCharts.setOption({
|
||||
dataZoom: {
|
||||
endValue: startValue,
|
||||
},
|
||||
})
|
||||
state.selectHistoricStartDate = new Date(startValue)
|
||||
}
|
||||
if (endDate) {
|
||||
myCharts.setOption({
|
||||
dataZoom: {
|
||||
startValue: endValue,
|
||||
},
|
||||
})
|
||||
state.selectHistoricEndDate = new Date(endValue)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
myCharts.setOption({
|
||||
dataZoom: {
|
||||
startValue: '',
|
||||
endValue: '',
|
||||
},
|
||||
})
|
||||
|
||||
state.selectHistoricStartDate = new Date('2009-10-07')
|
||||
state.selectHistoricEndDate = new Date()
|
||||
}
|
||||
}
|
||||
|
||||
// 禁用2009-10-07之后的日期
|
||||
const disableAfterDate = (date) => {
|
||||
return date < new Date('2009-10-06') || date > new Date()
|
||||
}
|
||||
|
||||
// 禁用过去的日期
|
||||
const disablePreviousDate = (date) => {
|
||||
return date < new Date(state.selectHistoricStartDate) || date > new Date()
|
||||
}
|
||||
|
||||
// 切换搜索区间开始日期
|
||||
const changeSearchRangeStartDate = (date) => {
|
||||
// console.error(date)
|
||||
changeSearchRange(
|
||||
'startDateTime',
|
||||
new Date(date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// 切换搜索区间结束日期
|
||||
const changeSearchRangeEndDate = (date) => {
|
||||
// console.error(date)
|
||||
changeSearchRange(
|
||||
'endDateTime',
|
||||
new Date(date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
}),
|
||||
)
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.custom-echarts {
|
||||
.myChart {
|
||||
width: 100%;
|
||||
height: 25rem;
|
||||
}
|
||||
|
||||
.echarts-header {
|
||||
.echarts-header-title {
|
||||
span {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: #323232;
|
||||
}
|
||||
}
|
||||
.echarts-search-area {
|
||||
padding: 2rem 0 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.echarts-search-byRange {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
.search-range-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
.search-range-list-each {
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #f3f4f6;
|
||||
cursor: pointer;
|
||||
span {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.echarts-search-byDate {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,604 +0,0 @@
|
||||
<template>
|
||||
<div class="custom-echarts">
|
||||
<div>
|
||||
<div class="echarts-header">
|
||||
<div class="echarts-header-title">
|
||||
<span>FiEE, Inc. Stock Price History</span>
|
||||
</div>
|
||||
<div class="echarts-search-area">
|
||||
<div class="echarts-search-byRange">
|
||||
<text style="font-size: 0.9rem; font-weight: 400; color: #666666;">
|
||||
Range
|
||||
</text>
|
||||
<div class="search-range-list">
|
||||
<div
|
||||
class="search-range-list-each"
|
||||
v-for="(item, index) in state.searchRange"
|
||||
:key="index"
|
||||
@click="changeSearchRange(item)"
|
||||
>
|
||||
<span>{{ item }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="echarts-search-byDate">
|
||||
<n-date-picker
|
||||
v-model:value="state.selectHistoricStartDate"
|
||||
type="date"
|
||||
:is-date-disabled="disableAfterDate"
|
||||
@update:value="changeSearchRangeStartDate"
|
||||
input-readonly
|
||||
/>
|
||||
<!-- <n-icon size="30">
|
||||
<ArrowForwardOutline />
|
||||
</n-icon> -->
|
||||
<span>to</span>
|
||||
<n-date-picker
|
||||
v-model:value="state.selectHistoricEndDate"
|
||||
type="date"
|
||||
:is-date-disabled="disablePreviousDate"
|
||||
@update:value="changeSearchRangeEndDate"
|
||||
input-readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="myEcharts" class="myChart"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, watch, reactive } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import markPointerIcon from '@/assets/image/icon/echarts_markPointer.png'
|
||||
import axios from 'axios'
|
||||
import { NDatePicker } from 'naive-ui'
|
||||
import { ArrowForwardOutline } from '@vicons/ionicons5'
|
||||
|
||||
const state = reactive({
|
||||
searchRange: ['1m', '3m', 'YTD', '1Y', '5Y', '10Y', 'Max'],
|
||||
selectHistoricStartDate: '2009-10-07',
|
||||
selectHistoricEndDate: new Date(),
|
||||
})
|
||||
|
||||
let myCharts = null
|
||||
let historicData = []
|
||||
let xAxisData = []
|
||||
|
||||
//初始化eCharts
|
||||
const initEcharts = (data) => {
|
||||
historicData = data
|
||||
xAxisData = data.map((item) => {
|
||||
return new Date(item.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
})
|
||||
const yAxisData = data.map((item) => item.price)
|
||||
// console.error(xAxisData, yAxisData)
|
||||
// 基于准备好的dom,初始化echarts实例
|
||||
myCharts = echarts.init(document.getElementById('myEcharts'))
|
||||
// 绘制图表
|
||||
myCharts.setOption({
|
||||
animation: false,
|
||||
progressive: 500,
|
||||
progressiveThreshold: 3000,
|
||||
// title: {
|
||||
// text: 'FiEE, Inc. Stock Price History',
|
||||
// },
|
||||
grid: {
|
||||
left: '8%', // 或 '2%',根据实际情况调整
|
||||
right: '15%', // 给右侧y轴留空间,数值可根据y轴label宽度调整
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
snap: true,
|
||||
label: {
|
||||
backgroundColor: '#6a7985',
|
||||
},
|
||||
},
|
||||
formatter: function (params) {
|
||||
const p = params[0]
|
||||
return `<span style="font-size: 1.1rem; font-weight: 600;">${p.axisValue}</span><br/><span style="font-size: 0.9rem; font-weight: 400;">Price: ${p.data}</span>`
|
||||
},
|
||||
confine: true,
|
||||
hideDelay: 1500
|
||||
},
|
||||
xAxis: {
|
||||
data: xAxisData,
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
inverse: true,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#CCD6EB',
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#323232',
|
||||
fontWeight: 'bold',
|
||||
// formatter: function (value) {
|
||||
// return value ? value.split('-')[0] : ''
|
||||
// },
|
||||
// interval: function (index, value) {
|
||||
// if (index === 0) return true;
|
||||
// const axisData = this && this.axis && this.axis.data ? this.axis.data : [];
|
||||
// if (!axisData[index - 1]) return true;
|
||||
// return value.split('-')[0] !== axisData[index - 1].split('-')[0];
|
||||
// },
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
position: 'right',
|
||||
interval: 25,
|
||||
// max: 75.0,
|
||||
show: true,
|
||||
axisLabel: {
|
||||
color: '#323232',
|
||||
fontWeight: 'bold',
|
||||
formatter: function (value) {
|
||||
return value > 0 ? value.toFixed(2) : value
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: yAxisData,
|
||||
type: 'line',
|
||||
sampling: 'lttb',
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
color: '#2c6288',
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: '#2c6288',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#F4F6F8',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
markPoint: {
|
||||
symbol: 'image://' + markPointerIcon,
|
||||
symbolSize: 24,
|
||||
data: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
},
|
||||
{
|
||||
type: 'slider',
|
||||
show: true,
|
||||
dataBackground: {
|
||||
lineStyle: {
|
||||
color: '#2C6288',
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 1, color: '#2c6288' },
|
||||
{ offset: 0, color: '#F4F6F8' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
selectedDataBackground: {
|
||||
lineStyle: {
|
||||
color: '#2C6288',
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 1, color: '#2c6288' },
|
||||
{ offset: 0, color: '#F4F6F8' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
fillerColor: 'rgba(44, 98, 136, 0.3)',
|
||||
realtime: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 监听 showTip 事件,动态显示 markPoint
|
||||
myCharts.on('showTip', function (params) {
|
||||
if (params) {
|
||||
const dataIndex = params.dataIndex
|
||||
const x = myCharts.getOption().xAxis[0].data[dataIndex]
|
||||
const y = myCharts.getOption().series[0].data[dataIndex]
|
||||
myCharts.setOption({
|
||||
series: [
|
||||
{
|
||||
markPoint: {
|
||||
symbol: 'image://' + markPointerIcon,
|
||||
symbolSize: 24,
|
||||
data: [{ coord: [x, y] }],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
})
|
||||
// 鼠标移出时,清除 markPoint
|
||||
myCharts.on('globalout', function () {
|
||||
myCharts.setOption({
|
||||
series: [
|
||||
{
|
||||
markPoint: {
|
||||
symbol: 'image://' + markPointerIcon,
|
||||
symbolSize: 24,
|
||||
data: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
myCharts.on('dataZoom', function (params) {
|
||||
// 获取当前 dataZoom 范围
|
||||
const option = myCharts.getOption()
|
||||
const xAxisData = option.xAxis[0].data
|
||||
const dataZoom = option.dataZoom[1] || option.dataZoom[0]
|
||||
|
||||
// 获取 dataZoom 的 startValue 和 endValue
|
||||
let startValue = dataZoom.endValue
|
||||
let endValue = dataZoom.startValue
|
||||
|
||||
// 如果是索引,转为日期
|
||||
if (typeof startValue === 'number') {
|
||||
startValue = xAxisData[startValue]
|
||||
}
|
||||
if (typeof endValue === 'number') {
|
||||
endValue = xAxisData[endValue]
|
||||
}
|
||||
|
||||
// 更新日期选择器
|
||||
state.selectHistoricStartDate = new Date(startValue)
|
||||
state.selectHistoricEndDate = new Date(endValue)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getHistoricalData()
|
||||
})
|
||||
|
||||
//获取历史数据
|
||||
const getHistoricalData = async () => {
|
||||
let now = new Date()
|
||||
let toDate =
|
||||
now.getFullYear() +
|
||||
'-' +
|
||||
String(now.getMonth() + 1).padStart(2, '0') +
|
||||
'-' +
|
||||
String(now.getDate()).padStart(2, '0')
|
||||
let url =
|
||||
'https://common.szjixun.cn/api/stock/history/base/list?from=2009-10-07&to=' +
|
||||
toDate
|
||||
const res = await axios.get(url)
|
||||
if (res.status === 200) {
|
||||
if (res.data.status === 0) {
|
||||
initEcharts(res.data.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 适配倒序数据,返回大于等于目标日期的最近一天索引
|
||||
function findClosestDateIndex(data, targetDateStr) {
|
||||
let left = 0,
|
||||
right = data.length - 1
|
||||
const target = new Date(targetDateStr).getTime()
|
||||
let res = data.length - 1 // 默认返回最后一个
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2)
|
||||
const midTime = new Date(data[mid].date).getTime()
|
||||
if (midTime > target) {
|
||||
left = mid + 1
|
||||
} else {
|
||||
res = mid
|
||||
right = mid - 1
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// 适配倒序数据,返回小于等于目标日期的最近一天索引
|
||||
function findClosestDateIndexDescLeft(data, targetDateStr) {
|
||||
let left = 0,
|
||||
right = data.length - 1
|
||||
const target = new Date(targetDateStr).getTime()
|
||||
let res = -1
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2)
|
||||
const midTime = new Date(data[mid].date).getTime()
|
||||
if (midTime > target) {
|
||||
left = mid + 1 // mid 比目标新,往更旧的方向找
|
||||
} else {
|
||||
res = mid // mid <= target,记录下来,继续往更新的方向找
|
||||
right = mid - 1
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
//点击切换搜索区间
|
||||
const changeSearchRange = (range, dateTime) => {
|
||||
const now = new Date()
|
||||
let startDate = ''
|
||||
let endDate = ''
|
||||
if (range === '1m') {
|
||||
const last = new Date(now)
|
||||
last.setMonth(now.getMonth() - 1)
|
||||
startDate = last.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
} else if (range === '3m') {
|
||||
const last = new Date(now)
|
||||
last.setMonth(now.getMonth() - 3)
|
||||
startDate = last.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
} else if (range === 'YTD') {
|
||||
startDate = new Date(now.getFullYear(), 0, 1).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
} else if (range === '1Y') {
|
||||
const last = new Date(now)
|
||||
last.setFullYear(now.getFullYear() - 1)
|
||||
startDate = last.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
} else if (range === '5Y') {
|
||||
const last = new Date(now)
|
||||
last.setFullYear(now.getFullYear() - 5)
|
||||
startDate = last.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
} else if (range === '10Y') {
|
||||
const last = new Date(now)
|
||||
last.setFullYear(now.getFullYear() - 10)
|
||||
startDate = last.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
endDate = new Date(new Date()).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
} else if (range === 'Max') {
|
||||
startDate = ''
|
||||
endDate = ''
|
||||
} else if (range === 'startDateTime') {
|
||||
startDate = dateTime
|
||||
endDate = ''
|
||||
} else if (range === 'endDateTime') {
|
||||
startDate = ''
|
||||
endDate = dateTime
|
||||
}
|
||||
if (startDate || endDate) {
|
||||
// historicData 和 xAxisData 需在 initEcharts 作用域可用
|
||||
if (
|
||||
typeof historicData !== 'undefined' &&
|
||||
typeof xAxisData !== 'undefined'
|
||||
) {
|
||||
let startValue = xAxisData[0]
|
||||
if (startDate) {
|
||||
const idx = findClosestDateIndex(historicData, startDate)
|
||||
// 用 historicData[idx].date 格式化为 xAxisData 的格式
|
||||
startValue = new Date(historicData[idx].date).toLocaleDateString(
|
||||
'en-US',
|
||||
{
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
},
|
||||
)
|
||||
}
|
||||
let endValue = endDate
|
||||
if (endDate) {
|
||||
// console.warn(endDate)
|
||||
const idx = findClosestDateIndexDescLeft(historicData, endDate)
|
||||
// console.warn(idx)
|
||||
// 用 historicData[idx].date 格式化为 xAxisData 的格式
|
||||
endValue = new Date(historicData[idx].date).toLocaleDateString(
|
||||
'en-US',
|
||||
{
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
},
|
||||
)
|
||||
// console.warn(endValue)
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
myCharts.setOption({
|
||||
dataZoom: {
|
||||
endValue: startValue,
|
||||
},
|
||||
})
|
||||
state.selectHistoricStartDate = new Date(startValue)
|
||||
}
|
||||
if (endDate) {
|
||||
myCharts.setOption({
|
||||
dataZoom: {
|
||||
startValue: endValue,
|
||||
},
|
||||
})
|
||||
state.selectHistoricEndDate = new Date(endValue)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
myCharts.setOption({
|
||||
dataZoom: {
|
||||
startValue: '',
|
||||
endValue: '',
|
||||
},
|
||||
})
|
||||
|
||||
state.selectHistoricStartDate = new Date('2009-10-07')
|
||||
state.selectHistoricEndDate = new Date()
|
||||
}
|
||||
}
|
||||
|
||||
// 禁用2009-10-07之后的日期
|
||||
const disableAfterDate = (date) => {
|
||||
return date < new Date('2009-10-06') || date > new Date()
|
||||
}
|
||||
|
||||
// 禁用过去的日期
|
||||
const disablePreviousDate = (date) => {
|
||||
return date < new Date(state.selectHistoricStartDate) || date > new Date()
|
||||
}
|
||||
|
||||
// 切换搜索区间开始日期
|
||||
const changeSearchRangeStartDate = (date) => {
|
||||
// console.error(date)
|
||||
changeSearchRange(
|
||||
'startDateTime',
|
||||
new Date(date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// 切换搜索区间结束日期
|
||||
const changeSearchRangeEndDate = (date) => {
|
||||
// console.error(date)
|
||||
changeSearchRange(
|
||||
'endDateTime',
|
||||
new Date(date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
}),
|
||||
)
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.custom-echarts {
|
||||
.myChart {
|
||||
width: 100%;
|
||||
height: 25rem;
|
||||
}
|
||||
|
||||
.echarts-header {
|
||||
.echarts-header-title {
|
||||
span {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: #323232;
|
||||
}
|
||||
}
|
||||
.echarts-search-area {
|
||||
padding: 2rem 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
.echarts-search-byRange {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.7rem;
|
||||
.search-range-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.7rem;
|
||||
.search-range-list-each {
|
||||
padding: 0.2rem 0.3rem;
|
||||
border-radius: 5px;
|
||||
background-color: #f3f4f6;
|
||||
cursor: pointer;
|
||||
span {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.echarts-search-byDate {
|
||||
padding: 1.5rem 0 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -3,8 +3,6 @@ import { computed } from 'vue'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
|
||||
import size375 from '@/components/customFooter/size375/index.vue'
|
||||
import size768 from '@/components/customFooter/size768/index.vue'
|
||||
import size1440 from '@/components/customFooter/size1920/index.vue'
|
||||
import size1920 from '@/components/customFooter/size1920/index.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@ -15,13 +13,15 @@ const { t } = useI18n()
|
||||
|
||||
const viewComponent = computed(() => {
|
||||
const viewWidth = width.value
|
||||
if (viewWidth <= 500) {
|
||||
if (viewWidth <= 450) {
|
||||
return size375
|
||||
} else if (viewWidth <= 1100) {
|
||||
return size768
|
||||
} else if (viewWidth <= 1500) {
|
||||
return size1440
|
||||
} else if (viewWidth <= 1920 || viewWidth > 1920) {
|
||||
}
|
||||
// else if (viewWidth <= 1100) {
|
||||
// return size768;
|
||||
// } else if (viewWidth <= 1500) {
|
||||
// return size1440;
|
||||
// }
|
||||
else if (viewWidth <= 1920 || viewWidth > 1920) {
|
||||
return size1920
|
||||
}
|
||||
})
|
||||
|
@ -1,69 +1,26 @@
|
||||
<template>
|
||||
<!-- 通用页脚 -->
|
||||
<div class="custom-footer">
|
||||
<div class="custom-footer-box">
|
||||
<span>© 2025 FiEE, Inc. All Rights Reserved.</span>
|
||||
<div class="footer-links">
|
||||
<span @click="handleLink('privacyPolicy')">Privacy Policy</span>
|
||||
<span @click="handleLink('termsOfUse')">Terms of use</span>
|
||||
<span @click="handleLink('siteMap')">Site Map</span>
|
||||
</div>
|
||||
<!-- 通用页脚 -->
|
||||
<div class="custom-footer">
|
||||
<span>Copyright © 2024-2027 FiEE</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
import privacyPolicy from "@/assets/file/footer/FiEE, Inc. _ Privacy policy.pdf";
|
||||
import termsOfUse from "@/assets/file/footer/FiEE, Inc. _ Terms of Use.pdf";
|
||||
import siteMap from "@/assets/file/footer/FiEE, Inc. _ Site Map.pdf";
|
||||
|
||||
//点击跳转到对应的链接页面
|
||||
const handleLink = (link) => {
|
||||
// if (link === "privacyPolicy") {
|
||||
// window.open(privacyPolicy, "_blank");
|
||||
// } else if (link === "termsOfUse") {
|
||||
// window.open(termsOfUse, "_blank");
|
||||
// } else if (link === "siteMap") {
|
||||
// window.open(siteMap, "_blank");
|
||||
// }
|
||||
router.push(link)
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-footer {
|
||||
width: 100%;
|
||||
background: #f7f8fa;
|
||||
border-top: 1px solid #ececec;
|
||||
z-index: 100;
|
||||
|
||||
.custom-footer-box {
|
||||
max-width: 1700px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
letter-spacing: 1px;
|
||||
color: #888;
|
||||
// font-size: 15px;
|
||||
font-size: 1.05rem;
|
||||
padding: 1rem 40px;
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-footer {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
color: #888;
|
||||
font-size: 15px;
|
||||
background: #f7f8fa;
|
||||
letter-spacing: 1px;
|
||||
border-top: 1px solid #ececec;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
margin: 0.4rem 0 0;
|
||||
span {
|
||||
border-right: 1px solid #d2d2d7;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
span:nth-last-child(1) {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,70 +1,26 @@
|
||||
<template>
|
||||
<!-- 通用页脚 -->
|
||||
<div class="custom-footer">
|
||||
<span>© 2025 FiEE, Inc. All Rights Reserved.</span>
|
||||
<div class="footer-links-box">
|
||||
<div class="footer-links">
|
||||
<span @click="handleLink('privacyPolicy')">Privacy Policy</span>
|
||||
<span @click="handleLink('termsOfUse')">Terms of use</span>
|
||||
<span @click="handleLink('siteMap')">Site Map</span>
|
||||
</div>
|
||||
<!-- 通用页脚 -->
|
||||
<div class="custom-footer">
|
||||
<span>Copyright © 2024-2027 FiEE</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
|
||||
import privacyPolicy from "@/assets/file/footer/FiEE, Inc. _ Privacy policy.pdf";
|
||||
import termsOfUse from "@/assets/file/footer/FiEE, Inc. _ Terms of Use.pdf";
|
||||
import siteMap from "@/assets/file/footer/FiEE, Inc. _ Site Map.pdf";
|
||||
|
||||
//点击跳转到对应的链接页面
|
||||
const handleLink = (link) => {
|
||||
// if (link === "privacyPolicy") {
|
||||
// window.open(privacyPolicy, "_blank");
|
||||
// } else if (link === "termsOfUse") {
|
||||
// window.open(termsOfUse, "_blank");
|
||||
// } else if (link === "siteMap") {
|
||||
// window.open(siteMap, "_blank");
|
||||
// }
|
||||
router.push(link)
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-footer {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 1rem 0;
|
||||
color: #888;
|
||||
font-size: 0.9rem;
|
||||
background: #f7f8fa;
|
||||
letter-spacing: 5px;
|
||||
border-top: 5px solid #ececec;
|
||||
z-index: 100;
|
||||
|
||||
.footer-links-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0.6rem 0 0;
|
||||
|
||||
.footer-links {
|
||||
span {
|
||||
border-right: 1px solid #d2d2d7;
|
||||
padding: 0 0.8rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
}
|
||||
span:nth-last-child(1) {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-footer {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 120px 0;
|
||||
color: #888;
|
||||
font-size: 75px;
|
||||
background: #f7f8fa;
|
||||
letter-spacing: 5px;
|
||||
border-top: 5px solid #ececec;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,59 +0,0 @@
|
||||
<template>
|
||||
<!-- 通用页脚 -->
|
||||
<div class="custom-footer">
|
||||
<span>© 2025 FiEE, Inc. All Rights Reserved.</span>
|
||||
<div class="footer-links">
|
||||
<span @click="handleLink('privacyPolicy')">Privacy Policy</span>
|
||||
<span @click="handleLink('termsOfUse')">Terms of use</span>
|
||||
<span @click="handleLink('siteMap')">Site Map</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
import privacyPolicy from "@/assets/file/footer/FiEE, Inc. _ Privacy policy.pdf";
|
||||
import termsOfUse from "@/assets/file/footer/FiEE, Inc. _ Terms of Use.pdf";
|
||||
import siteMap from "@/assets/file/footer/FiEE, Inc. _ Site Map.pdf";
|
||||
|
||||
//点击跳转到对应的链接页面
|
||||
const handleLink = (link) => {
|
||||
// if (link === "privacyPolicy") {
|
||||
// window.open(privacyPolicy, "_blank");
|
||||
// } else if (link === "termsOfUse") {
|
||||
// window.open(termsOfUse, "_blank");
|
||||
// } else if (link === "siteMap") {
|
||||
// window.open(siteMap, "_blank");
|
||||
// }
|
||||
router.push(link)
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-footer {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
color: #888;
|
||||
// font-size: 15px;
|
||||
font-size: 1.05rem;
|
||||
background: #f7f8fa;
|
||||
letter-spacing: 1px;
|
||||
border-top: 1px solid #ececec;
|
||||
z-index: 100;
|
||||
padding: 1rem 0;
|
||||
|
||||
.footer-links {
|
||||
margin: 0.4rem 0 0;
|
||||
span {
|
||||
border-right: 1px solid #d2d2d7;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
span:nth-last-child(1) {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -3,8 +3,6 @@ import { computed } from 'vue'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
|
||||
import size375 from '@/components/customHeader/size375/index.vue'
|
||||
import size768 from '@/components/customHeader/size375/index.vue'
|
||||
import size1440 from '@/components/customHeader/size1440/index.vue'
|
||||
import size1920 from '@/components/customHeader/size1920/index.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@ -17,11 +15,13 @@ const viewComponent = computed(() => {
|
||||
const viewWidth = width.value
|
||||
if (viewWidth <= 450) {
|
||||
return size375
|
||||
} else if (viewWidth <= 835) {
|
||||
return size768
|
||||
} else if (viewWidth <= 1640) {
|
||||
return size1440
|
||||
} else if (viewWidth <= 1920 || viewWidth > 1920) {
|
||||
}
|
||||
// else if (viewWidth <= 1100) {
|
||||
// return size768;
|
||||
// } else if (viewWidth <= 1500) {
|
||||
// return size1440;
|
||||
// }
|
||||
else if (viewWidth <= 1920 || viewWidth > 1920) {
|
||||
return size1920
|
||||
}
|
||||
})
|
||||
|
@ -1,230 +0,0 @@
|
||||
<template>
|
||||
<!-- 通用页头 -->
|
||||
<NLayoutHeader
|
||||
class="custom-header"
|
||||
:class="{ 'header-scrolled': isScrolled }"
|
||||
>
|
||||
<div class="header-container">
|
||||
<div class="logo" @click="handleToHome">
|
||||
<NImage width="160" height="50" :src="FiEELogo" preview-disabled />
|
||||
</div>
|
||||
<div class="header-menu">
|
||||
<NMenu
|
||||
mode="horizontal"
|
||||
:options="menuOptions"
|
||||
:inverted="isScrolled"
|
||||
v-model:value="selectedKey"
|
||||
@update:value="handleMenuSelect"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</NLayoutHeader>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FiEELogo from '@/assets/image/header/logo.png'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { NMenu, NLayoutHeader, NImage } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useHeaderMenuConfig } from '@/config/headerMenuConfig'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
// 使用统一的菜单配置
|
||||
const menuOptions = useHeaderMenuConfig()
|
||||
const selectedKey = ref(null)
|
||||
|
||||
const isScrolled = ref(false)
|
||||
|
||||
// 递归查找菜单项
|
||||
function findMenuOptionByKey(options, key) {
|
||||
for (const option of options) {
|
||||
if (option.key === key) return option
|
||||
if (option.children) {
|
||||
const found = findMenuOptionByKey(option.children, key)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 菜单点击跳转
|
||||
const handleMenuSelect = (key) => {
|
||||
const option = findMenuOptionByKey(menuOptions, key)
|
||||
if (option && option.href) {
|
||||
router.push(option.href)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听滚动事件
|
||||
const handleScroll = () => {
|
||||
//滚动距离大于100px时,处理对应的header样式
|
||||
isScrolled.value = window.scrollY >= 100
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
|
||||
//点击回到首页
|
||||
const handleToHome = () => {
|
||||
router.push('/myhome')
|
||||
selectedKey.value = null // 重置菜单选中状态
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-header {
|
||||
--header-height: 5rem;
|
||||
--primary-color: #8b59f7;
|
||||
transition: all 0.3s ease;
|
||||
background: transparent;
|
||||
height: var(--header-height);
|
||||
|
||||
&.header-scrolled {
|
||||
background: rgba(220, 207, 248, 0.95);
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.header-container {
|
||||
max-width: 1700px;
|
||||
margin: 0 auto;
|
||||
padding: 0 40px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logo {
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease;
|
||||
margin-right: 100px;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.header-menu {
|
||||
display: block;
|
||||
flex: 1;
|
||||
|
||||
:deep(.n-menu) {
|
||||
background: transparent;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
:deep(.n-menu-item) {
|
||||
position: relative;
|
||||
margin: 0 10px;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 700;
|
||||
// font-size: 16px;
|
||||
font-size: 0.875rem;
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: var(--primary-color);
|
||||
transition: all 0.3s ease;
|
||||
transform: translateX(-50%);
|
||||
opacity: 0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::after {
|
||||
width: 80px;
|
||||
height: 3px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 选中状态的样式
|
||||
&.n-menu-item--selected {
|
||||
&::after {
|
||||
width: 40px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 子菜单样式
|
||||
:deep(.n-submenu) {
|
||||
.n-submenu-children {
|
||||
backdrop-filter: blur(16px);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 12px;
|
||||
padding: 8px 0;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
transform-origin: top;
|
||||
animation: dropDown 0.3s ease;
|
||||
|
||||
.n-menu-item {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--primary-color);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
opacity: 0.1;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.header-menu .n-menu .n-menu-item-content .n-menu-item-content-header {
|
||||
word-break: break-word;
|
||||
white-space: unset !important;
|
||||
}
|
||||
.header-menu .n-menu .n-submenu .n-menu-item-content{
|
||||
padding: 0 8px!important;
|
||||
}
|
||||
</style>
|
@ -41,32 +41,32 @@ const isScrolled = ref(false)
|
||||
// 递归查找菜单项
|
||||
function findMenuOptionByKey(options, key) {
|
||||
for (const option of options) {
|
||||
if (option.key === key) return option
|
||||
if (option.key === key) return option;
|
||||
if (option.children) {
|
||||
const found = findMenuOptionByKey(option.children, key)
|
||||
if (found) return found
|
||||
const found = findMenuOptionByKey(option.children, key);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
// 菜单点击跳转
|
||||
const handleMenuSelect = (key) => {
|
||||
const option = findMenuOptionByKey(menuOptions, key)
|
||||
const option = findMenuOptionByKey(menuOptions, key);
|
||||
if (option && option.href) {
|
||||
router.push(option.href)
|
||||
router.push(option.href);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 监听滚动事件
|
||||
const handleScroll = () => {
|
||||
//滚动距离大于100px时,处理对应的header样式
|
||||
isScrolled.value = window.scrollY >= 100
|
||||
}
|
||||
isScrolled.value = window.scrollY >= 100;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
})
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
@ -74,21 +74,26 @@ onUnmounted(() => {
|
||||
|
||||
//点击回到首页
|
||||
const handleToHome = () => {
|
||||
router.push('/myhome')
|
||||
router.push('/')
|
||||
selectedKey.value = null // 重置菜单选中状态
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-header {
|
||||
--header-height: 5rem;
|
||||
--primary-color: #8b59f7;
|
||||
--header-height: 80px;
|
||||
--primary-color: #8B59F7;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
background: transparent;
|
||||
height: var(--header-height);
|
||||
|
||||
&.header-scrolled {
|
||||
background: rgba(220, 207, 248, 0.95);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
@ -133,8 +138,7 @@ const handleToHome = () => {
|
||||
margin: 0 20px;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 700;
|
||||
// font-size: 16px;
|
||||
font-size: 1.05rem;
|
||||
font-size: 16px;
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
|
||||
|
@ -108,6 +108,11 @@ const handleToHome = () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
background: transparent;
|
||||
height: 320px;
|
||||
@ -187,7 +192,7 @@ const handleToHome = () => {
|
||||
top: 320px;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
background: rgba(220, 207, 248, 0.95);
|
||||
background: #fff;
|
||||
z-index: 1100;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
|
||||
padding: 40px 0 80px 0;
|
||||
|
38
src/components/customSelectSearch/index.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="custom-select-search">
|
||||
<n-select
|
||||
:options="state.selectOptions"
|
||||
v-model:value="state.selectedValue"
|
||||
/>
|
||||
<n-input
|
||||
v-model:value="state.inputValue"
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
/>
|
||||
<n-button type="primary">Go</n-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { reactive } from 'vue'
|
||||
import { NSelect, NInput, NButton } from 'naive-ui'
|
||||
|
||||
const state = reactive({
|
||||
selectedValue: 'all_years', //选中值
|
||||
selectOptions: [
|
||||
{
|
||||
label: 'All Years',
|
||||
value: 'all_years',
|
||||
},
|
||||
], //下拉选项
|
||||
inputValue: '', //输入值
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.custom-select-search {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
@ -1,122 +1,105 @@
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export const useHeaderMenuConfig = () => {
|
||||
const { t } = useI18n();
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
return [
|
||||
{
|
||||
label: t("header_menu.corporate_information.title"),
|
||||
key: "corporate_information",
|
||||
label: t('header_menu.corporate_information.title'),
|
||||
key: 'corporate_information',
|
||||
children: [
|
||||
{
|
||||
label: t("header_menu.corporate_information.company_overview"),
|
||||
key: "company_overview",
|
||||
href: "/companyoverview",
|
||||
label: t('header_menu.corporate_information.company_overview'),
|
||||
key: 'company_overview',
|
||||
},
|
||||
{
|
||||
label: t("header_menu.corporate_information.business_introduction"),
|
||||
key: "business_introduction",
|
||||
href: "/businessservices",
|
||||
label: t('header_menu.corporate_information.business_introduction'),
|
||||
key: 'business_introduction',
|
||||
},
|
||||
{
|
||||
label: t("header_menu.corporate_information.management"),
|
||||
key: "management",
|
||||
href: "/manage",
|
||||
label: t('header_menu.corporate_information.management'),
|
||||
key: 'management',
|
||||
},
|
||||
{
|
||||
label: t("header_menu.corporate_information.board_of_directors"),
|
||||
key: "board_of_directors",
|
||||
href: "/boarddirectors",
|
||||
label: t('header_menu.corporate_information.board_of_directors'),
|
||||
key: 'board_of_directors',
|
||||
},
|
||||
{
|
||||
label: t("header_menu.corporate_information.committee_appointments"),
|
||||
key: "committee_appointments",
|
||||
href: "/committeeappointment",
|
||||
label: t('header_menu.corporate_information.committee_appointments'),
|
||||
key: 'committee_appointments',
|
||||
},
|
||||
{
|
||||
label: t("header_menu.corporate_information.governance"),
|
||||
key: "governance",
|
||||
href: "/govern",
|
||||
},
|
||||
// {
|
||||
// label: t("header_menu.corporate_information.corporate_video"),
|
||||
// key: "corporate_video",
|
||||
// },//暂时隐藏本菜单
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t("header_menu.financial_information.title"),
|
||||
key: "financial_information",
|
||||
children: [
|
||||
{
|
||||
label: t("header_menu.financial_information.sec_filings"),
|
||||
key: "sec_filings",
|
||||
href: "/secfilings",
|
||||
},
|
||||
{
|
||||
label: t("header_menu.financial_information.annual_reports"),
|
||||
key: "annual_reports",
|
||||
href: "/annualreports",
|
||||
label: t('header_menu.corporate_information.governance'),
|
||||
key: 'governance',
|
||||
},
|
||||
{
|
||||
label: t("header_menu.financial_information.quarterly_reports"),
|
||||
key: "quarterly_reports",
|
||||
href: "/quarterlyreports",
|
||||
label: t('header_menu.corporate_information.corporate_video'),
|
||||
key: 'corporate_video',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t("header_menu.stock_information.title"),
|
||||
key: "stock_information",
|
||||
label: t('header_menu.financial_information.title'),
|
||||
key: 'financial_information',
|
||||
children: [
|
||||
{
|
||||
label: t("header_menu.stock_information.stock_quote"),
|
||||
key: "stock_quote",
|
||||
href:'/stock-quote'
|
||||
label: t('header_menu.financial_information.sec_filings'),
|
||||
key: 'sec_filings',
|
||||
href: "/secfilings",
|
||||
},
|
||||
{
|
||||
label: t("header_menu.stock_information.historic_stock_price"),
|
||||
key: "historic_stock_price",
|
||||
href:'/historic-stock'
|
||||
},
|
||||
// {
|
||||
// label: t("header_menu.stock_information.investment_calculator"),
|
||||
// key: "investment_calculator",
|
||||
// href:'/calculator'
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t("header_menu.news_releases.title"),
|
||||
key: "news_releases",
|
||||
children: [
|
||||
{
|
||||
label: t("header_menu.news_releases.press_releases"),
|
||||
key: "press_releases",
|
||||
href: "/press-releases",
|
||||
},
|
||||
{
|
||||
label: t("header_menu.news_releases.events_calendar"),
|
||||
key: "events_calendar",
|
||||
href: "/events-calendar",
|
||||
label: t('header_menu.financial_information.quarterly_results'),
|
||||
key: 'quarterly_results',
|
||||
href: "/quarterlyresults",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t("header_menu.investor_resources.title"),
|
||||
key: "investor_resources",
|
||||
label: t('header_menu.stock_information.title'),
|
||||
key: 'stock_information',
|
||||
children: [
|
||||
{
|
||||
label: t("header_menu.investor_resources.ir_contacts"),
|
||||
key: "ir_contacts",
|
||||
href:'/contacts'
|
||||
label: t('header_menu.stock_information.stock_quote'),
|
||||
key: 'stock_quote',
|
||||
},
|
||||
{
|
||||
label: t("header_menu.investor_resources.email_alerts"),
|
||||
key: "email_alerts",
|
||||
href:'/email-alerts'
|
||||
label: t('header_menu.stock_information.historic_stock_price'),
|
||||
key: 'historic_stock_price',
|
||||
},
|
||||
{
|
||||
label: t('header_menu.stock_information.investment_calculator'),
|
||||
key: 'investment_calculator',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
{
|
||||
label: t('header_menu.news_releases.title'),
|
||||
key: 'news_releases',
|
||||
children: [
|
||||
{
|
||||
label: t('header_menu.news_releases.press_releases'),
|
||||
key: 'press_releases',
|
||||
href: '/press-releases',
|
||||
},
|
||||
{
|
||||
label: t('header_menu.news_releases.events_calendar'),
|
||||
key: 'events_calendar',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('header_menu.investor_resources.title'),
|
||||
key: 'investor_resources',
|
||||
children: [
|
||||
{
|
||||
label: t('header_menu.investor_resources.ir_contacts'),
|
||||
key: 'ir_contacts',
|
||||
},
|
||||
{
|
||||
label: t('header_menu.investor_resources.email_alerts'),
|
||||
key: 'email_alerts',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
35754
src/dict/secFiles.js
@ -1,16 +1,12 @@
|
||||
// router/index.js
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import { setupRouterGuards } from "./router-guards";
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { setupRouterGuards } from './router-guards'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
redirect: '/myhome'
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
name: "index",
|
||||
component: () => import("@/views/index/index.vue"),
|
||||
path: '/',
|
||||
name: 'index',
|
||||
component: () => import('@/views/index/index.vue'),
|
||||
// beforeEnter: (to, from, next) => {
|
||||
|
||||
// localStorage.clear()
|
||||
@ -18,132 +14,59 @@ const routes = [
|
||||
// }
|
||||
children: [
|
||||
{
|
||||
path: "/contacts",
|
||||
name: "contacts",
|
||||
component: () => import("@/views/contacts/index.vue"),
|
||||
path: 'home',
|
||||
name: 'home',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
// beforeEnter: (to, from, next) => {
|
||||
|
||||
// localStorage.clear()
|
||||
// next()
|
||||
// }
|
||||
},
|
||||
{
|
||||
path: '/stock-quote',
|
||||
name: 'stock-quote',
|
||||
component: () => import('@/views/stock-quote/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/historic-stock',
|
||||
name: 'historic-stock',
|
||||
component: () => import('@/views/historic-stock/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/contacts',
|
||||
name: 'contacts',
|
||||
component: () => import('@/views/contacts/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/email-alerts',
|
||||
name: 'email-alerts',
|
||||
component: () => import('@/views/email-alerts/index.vue'),
|
||||
},
|
||||
{
|
||||
path: "/calculator",
|
||||
name: "calculator",
|
||||
component: () => import("@/views/calculator/index.vue"),
|
||||
path: 'press-releases',
|
||||
name: 'press-releases',
|
||||
component: () => import('@/views/press-releases/index.vue'),
|
||||
},
|
||||
{
|
||||
path: "/stock-quote",
|
||||
name: "stock-quote",
|
||||
component: () => import("@/views/stock-quote/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/historic-stock",
|
||||
name: "historic-stock",
|
||||
component: () => import("@/views/historic-stock/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/contacts",
|
||||
name: "contacts",
|
||||
component: () => import("@/views/contacts/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/email-alerts",
|
||||
name: "email-alerts",
|
||||
component: () => import("@/views/email-alerts/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/quarterlyreports",
|
||||
name: "quarterlyreports",
|
||||
path: '/quarterlyresults',
|
||||
name: 'QuarterlyResults',
|
||||
component: () =>
|
||||
import("@/views/financialinformation/quarterlyreports/index.vue"),
|
||||
import('@/views/financialinformation/quarterlyresults/index.vue'),
|
||||
},
|
||||
{
|
||||
path: "/secfilings",
|
||||
name: "SecFilings",
|
||||
path: '/secfilings',
|
||||
name: 'SecFilings',
|
||||
component: () =>
|
||||
import("@/views/financialinformation/secfilings/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/secfilingsDefail",
|
||||
name: "SecFilingsDetail",
|
||||
component: () =>
|
||||
import("@/views/financialinformation/secfilingsdetail/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/annualreports",
|
||||
name: "AnnualReports",
|
||||
component: () =>
|
||||
import("@/views/financialinformation/annualreports/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/press-releases",
|
||||
name: "press-releases",
|
||||
component: () => import("@/views/press-releases/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/news",
|
||||
name: "news",
|
||||
component: () => import("@/views/news/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/events-calendar",
|
||||
name: "events-calendar",
|
||||
component: () => import("@/views/events-calendar/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/companyoverview",
|
||||
name: "companyoverview",
|
||||
component: () => import("@/views/companyoverview/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "myhome",
|
||||
name: "myHome",
|
||||
component: () => import("@/views/myHome/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/businessservices",
|
||||
name: "BusinessServices",
|
||||
component: () => import("@/views/BusinessServices/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/manage",
|
||||
name: "manage",
|
||||
component: () => import("@/views/manage/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/boarddirectors",
|
||||
name: "boarddirectors",
|
||||
component: () => import("@/views/boarddirectors/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/committeeappointment",
|
||||
name: "CommitteeAppointment",
|
||||
component: () => import("@/views/CommitteeAppointment/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/govern",
|
||||
name: "govern",
|
||||
component: () => import("@/views/govern/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/privacyPolicy",
|
||||
name: "privacyPolicy",
|
||||
component: () => import("@/views/footerLinks/privacyPolicy/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/termsOfUse",
|
||||
name: "termsOfUse",
|
||||
component: () => import("@/views/footerLinks/termsOfUse/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/cookiesSettings",
|
||||
name: "cookiesSettings",
|
||||
component: () => import("@/views/footerLinks/cookiesSettings/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/siteMap",
|
||||
name: "siteMap",
|
||||
component: () => import("@/views/footerLinks/siteMap/index.vue"),
|
||||
import('@/views/financialinformation/secfilings/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: '/contacts',
|
||||
name: 'contacts',
|
||||
component: () => import('@/views/contacts/index.vue'),
|
||||
},
|
||||
// {
|
||||
// path: '/companyprofil',
|
||||
// name: 'Companyprofil',
|
||||
@ -169,21 +92,17 @@ const routes = [
|
||||
// name: 'Investorhandbook',
|
||||
// component: () => import('@/views/investorhandbook/index.vue'),
|
||||
// },
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/myhome'
|
||||
}
|
||||
];
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
})
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta?.title) {
|
||||
document.title = to.meta.title;
|
||||
document.title = to.meta.title
|
||||
}
|
||||
next();
|
||||
});
|
||||
setupRouterGuards(router);
|
||||
export default router;
|
||||
next()
|
||||
})
|
||||
setupRouterGuards(router)
|
||||
export default router
|
||||
|
@ -15,7 +15,7 @@ import { useRouter } from "vue-router";
|
||||
import { showImagePreview } from "vant";
|
||||
|
||||
export const useAuth = createGlobalState(() => {
|
||||
// console.log("useRouter", useRouter);
|
||||
console.log("useRouter", useRouter);
|
||||
const router = useRouter();
|
||||
const token = useStorage("token", "", localStorage);
|
||||
const workUid = useStorage("workUid", "", localStorage);
|
||||
@ -44,7 +44,7 @@ export const useAuth = createGlobalState(() => {
|
||||
const millisecondsIn48Hours = 48 * 60 * 60 * 1000;
|
||||
voteToken.value.expireTime = currentTimestamp + millisecondsIn48Hours;
|
||||
voteToken.value.authorization = res.data?.authorization;
|
||||
// console.log("voteToken", voteToken.value);
|
||||
console.log("voteToken", voteToken.value);
|
||||
}
|
||||
};
|
||||
const sendVote = async () => {
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { ref } from 'vue'
|
||||
import { createGlobalState, useLocalStorage } from '@vueuse/core'
|
||||
import axios from 'axios'
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
|
||||
export const useStockQuote = createGlobalState(() => {
|
||||
const stockQuote = useLocalStorage('stockQuote', {
|
||||
@ -17,65 +14,11 @@ export const useStockQuote = createGlobalState(() => {
|
||||
""
|
||||
]
|
||||
})
|
||||
const date = new Date();
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
timeZone: 'America/New_York',
|
||||
timeZoneName: 'short'
|
||||
};
|
||||
let lastTradingDay
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
/*
|
||||
美股的常规发行日(交易日)为周一至周五,遇到法定假日则顺延。
|
||||
如果你只需要“上一个交易日”(不考虑法定假日)的情况下
|
||||
获取当前美东时间。
|
||||
如果今天是周一,则上一个交易日为上周五。
|
||||
如果今天是周日,则上一个交易日为上周五。
|
||||
如果今天是周六,则上一个交易日为周五。
|
||||
其他情况,上一个交易日为昨天。
|
||||
*/
|
||||
|
||||
const getLastTradingDay = async () => {
|
||||
const toDate = dayjs().format('YYYY-MM-DD');
|
||||
const finalFromDate = dayjs().subtract(7, 'day').format('YYYY-MM-DD');
|
||||
let url =
|
||||
'https://common.szjixun.cn/api/stock/history/list?from=' +
|
||||
finalFromDate +
|
||||
'&to=' +
|
||||
toDate;
|
||||
const res = await axios.get(url)
|
||||
if (res.status === 200) {
|
||||
if (res.data.status === 0) {
|
||||
lastTradingDay = dayjs(res.data.data[0].date)
|
||||
}
|
||||
return lastTradingDay.format('MMM D, YYYY') + ' 4:00 PM EDT'
|
||||
}
|
||||
}
|
||||
|
||||
const formatted = ref(null)
|
||||
const init = async () => {
|
||||
formatted.value = await getLastTradingDay()
|
||||
}
|
||||
init()
|
||||
const getStockQuate = async () => {
|
||||
// const res = await axios.get('https://saas-test.szjixun.cn/api/fiee/chart/forward/test')
|
||||
const res = await axios.get('https://common.szjixun.cn/api/stock/company/data')
|
||||
// console.error(res)
|
||||
if (res.status === 200) {
|
||||
if (res.data.status === 0) {
|
||||
stockQuote.value = res.data.data
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
const getStockQuate= async()=>{
|
||||
const res = await axios.get('http://localhost:3213/api/minm/open')
|
||||
stockQuote.value=res.data
|
||||
}
|
||||
return {
|
||||
formatted,
|
||||
getStockQuate,
|
||||
stockQuote
|
||||
}
|
||||
|
@ -1,34 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
|
||||
import size375 from "./size375/index.vue";
|
||||
import size768 from "./size768/index.vue";
|
||||
import size1440 from "./size1440/index.vue";
|
||||
import size1920 from "./size1920/index.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const router = useRouter();
|
||||
const { width } = useWindowSize();
|
||||
const { t } = useI18n();
|
||||
|
||||
const viewComponent = computed(() => {
|
||||
const viewWidth = width.value;
|
||||
if (viewWidth <= 450) {
|
||||
return size375;
|
||||
} else if (viewWidth <= 1100) {
|
||||
return size768;
|
||||
} else if (viewWidth <= 1500) {
|
||||
return size1440;
|
||||
} else if (viewWidth <= 1920 || viewWidth > 1920) {
|
||||
return size1920;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="viewComponent" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@ -1,265 +0,0 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<div class="business-page">
|
||||
<!-- 渐变背景标题区 - 增加层次感 -->
|
||||
<section class="hero-section">
|
||||
<div class="container">
|
||||
<h1 style="font-size: 40px" class="hero-title">
|
||||
{{ $t("BusinessiIntroduction.CONTAIN.TITLEONE.TITLE") }}
|
||||
</h1>
|
||||
<div style="font-size: 18px" class="hero-description">
|
||||
{{ $t("BusinessiIntroduction.CONTAIN.TITLEONE.CONTENT") }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 业务核心解决方案 -->
|
||||
<main style="margin-top: 40px" class="container">
|
||||
<section>
|
||||
<h1 class="hero-title" style="font-size: 22px">
|
||||
{{ $t("BusinessiIntroduction.CONTAIN.TITLEONE.CONTENTTWO") }}
|
||||
</h1>
|
||||
</section>
|
||||
|
||||
<div class="solution-grid">
|
||||
<!-- 统一使用弹性列布局,通过媒体查询控制排列方式 -->
|
||||
<div
|
||||
v-for="(solution, sIndex) in solutions"
|
||||
:key="sIndex"
|
||||
class="featured-solution"
|
||||
>
|
||||
<div
|
||||
class="solution-card"
|
||||
:style="{ '--delay': `${sIndex * 0.2}s` }"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div class="decorative-line"></div>
|
||||
<h2 class="card-title">{{ solution.title }}</h2>
|
||||
</div>
|
||||
<ul class="card-content">
|
||||
<li
|
||||
v-for="(point, pIndex) in solution.points"
|
||||
:key="pIndex"
|
||||
class="content-point"
|
||||
>
|
||||
<div class="point-icon">•</div>
|
||||
<div style="font-size: 18px" class="point-text">
|
||||
{{ point }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed } from "vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const solutions = computed(() => [
|
||||
{
|
||||
title: t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.ONE.TITLE"),
|
||||
points: [
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.ONE.CONTENT"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.ONE.CONTENTTWO"),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.TWO.TITLE"),
|
||||
points: [
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.TWO.CONTENT"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.TWO.CONTENTTWO"),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.THREE.TITLE"),
|
||||
points: [
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.THREE.CONTENT"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.THREE.CONTENTTWO"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.THREE.CONTENTTHREE"),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.FOUR.TITLE"),
|
||||
points: [
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.FOUR.CONTENT"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.FOUR.CONTENTTWO"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.FOUR.CONTENTTHREE"),
|
||||
],
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 基础样式 */
|
||||
|
||||
.hero-title {
|
||||
font-size: 40px;
|
||||
color: black;
|
||||
margin-bottom: 2rem;
|
||||
animation: slideIn 1s ease;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
margin: 0 auto;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
color: black;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary-color: #895bff;
|
||||
--primary-light: #a07cff;
|
||||
--primary-dark: #6a11cb;
|
||||
--primary-gradient: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-light) 0%,
|
||||
var(--primary-color) 100%
|
||||
);
|
||||
}
|
||||
.home-page {
|
||||
background-image: url("@/assets/image/bg.png");
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
/* 标题区 - 紫色渐变 */
|
||||
.hero-section {
|
||||
background: var(--primary-gradient);
|
||||
padding: 5rem 0 0rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.title-decoration {
|
||||
position: absolute;
|
||||
bottom: -15px;
|
||||
left: 0;
|
||||
width: 80%;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.solution-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3rem;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
/* 桌面端布局(>=768px) */
|
||||
@media (min-width: 768px) {
|
||||
.solution-group {
|
||||
flex-direction: row;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.featured-solution {
|
||||
flex: 1;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端布局(<768px) */
|
||||
@media (max-width: 767px) {
|
||||
.home-page {
|
||||
background-image: url("@/assets/image/bg-mobile.png");
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.solution-group {
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.featured-solution {
|
||||
width: 100% !important; /* 强制占满容器 */
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.solution-card {
|
||||
padding: 2rem;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.content-point {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 卡片公共样式 */
|
||||
.solution-card {
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem;
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
animation: cardEnter 0.6s ease forwards;
|
||||
border: 1px solid rgba(137, 91, 255, 0.2);
|
||||
background: linear-gradient(135deg, #f9f6ff 0%, #f0e9ff 100%);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.decorative-line {
|
||||
width: 50px;
|
||||
height: 3px;
|
||||
background: var(--primary-gradient);
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.solution-card:hover .decorative-line {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.3rem;
|
||||
color: #2c0850;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content-point {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid rgba(137, 91, 255, 0.1);
|
||||
}
|
||||
|
||||
.point-icon {
|
||||
color: #4a3a6b;
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.point-text {
|
||||
color: #4a3a6b;
|
||||
line-height: 1.6;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@keyframes cardEnter {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,265 +0,0 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<div class="business-page">
|
||||
<!-- 渐变背景标题区 - 增加层次感 -->
|
||||
<section class="hero-section">
|
||||
<div class="container">
|
||||
<h2 class="hero-title">
|
||||
{{ $t("BusinessiIntroduction.CONTAIN.TITLEONE.TITLE") }}
|
||||
</h2>
|
||||
<div style="font-size: 18px" class="hero-description">
|
||||
{{ $t("BusinessiIntroduction.CONTAIN.TITLEONE.CONTENT") }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 业务核心解决方案 -->
|
||||
<main style="margin-top: 40px" class="container">
|
||||
<section>
|
||||
<h1 style="font-size: 30px" class="hero-title">
|
||||
{{ $t("BusinessiIntroduction.CONTAIN.TITLEONE.CONTENTTWO") }}
|
||||
</h1>
|
||||
</section>
|
||||
|
||||
<div class="solution-grid">
|
||||
<!-- 统一使用弹性列布局,通过媒体查询控制排列方式 -->
|
||||
<div
|
||||
v-for="(solution, sIndex) in solutions"
|
||||
:key="sIndex"
|
||||
class="featured-solution"
|
||||
>
|
||||
<div
|
||||
class="solution-card"
|
||||
:style="{ '--delay': `${sIndex * 0.2}s` }"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div class="decorative-line"></div>
|
||||
<h2 class="card-title">{{ solution.title }}</h2>
|
||||
</div>
|
||||
<ul class="card-content">
|
||||
<li
|
||||
v-for="(point, pIndex) in solution.points"
|
||||
:key="pIndex"
|
||||
class="content-point"
|
||||
>
|
||||
<div class="point-icon">•</div>
|
||||
<div style="font-size: 18px" class="point-text">
|
||||
{{ point }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed } from "vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const solutions = computed(() => [
|
||||
{
|
||||
title: t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.ONE.TITLE"),
|
||||
points: [
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.ONE.CONTENT"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.ONE.CONTENTTWO"),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.TWO.TITLE"),
|
||||
points: [
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.TWO.CONTENT"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.TWO.CONTENTTWO"),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.THREE.TITLE"),
|
||||
points: [
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.THREE.CONTENT"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.THREE.CONTENTTWO"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.THREE.CONTENTTHREE"),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.FOUR.TITLE"),
|
||||
points: [
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.FOUR.CONTENT"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.FOUR.CONTENTTWO"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.FOUR.CONTENTTHREE"),
|
||||
],
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 基础样式 */
|
||||
|
||||
.hero-title {
|
||||
font-size: 40px;
|
||||
color: black;
|
||||
margin-bottom: 2rem;
|
||||
animation: slideIn 1s ease;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
margin: 0 auto;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
color: black;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary-color: #895bff;
|
||||
--primary-light: #a07cff;
|
||||
--primary-dark: #6a11cb;
|
||||
--primary-gradient: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-light) 0%,
|
||||
var(--primary-color) 100%
|
||||
);
|
||||
}
|
||||
.home-page {
|
||||
background-image: url("@/assets/image/bg.png");
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
/* 标题区 - 紫色渐变 */
|
||||
.hero-section {
|
||||
background: var(--primary-gradient);
|
||||
padding: 5rem 0 0rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.title-decoration {
|
||||
position: absolute;
|
||||
bottom: -15px;
|
||||
left: 0;
|
||||
width: 80%;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.solution-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3rem;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
/* 桌面端布局(>=768px) */
|
||||
@media (min-width: 768px) {
|
||||
.solution-group {
|
||||
flex-direction: row;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.featured-solution {
|
||||
flex: 1;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端布局(<768px) */
|
||||
@media (max-width: 767px) {
|
||||
.home-page {
|
||||
background-image: url("@/assets/image/bg-mobile.png");
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.solution-group {
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.featured-solution {
|
||||
width: 100% !important; /* 强制占满容器 */
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.solution-card {
|
||||
padding: 2rem;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.content-point {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 卡片公共样式 */
|
||||
.solution-card {
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem;
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
animation: cardEnter 0.6s ease forwards;
|
||||
border: 1px solid rgba(137, 91, 255, 0.2);
|
||||
background: linear-gradient(135deg, #f9f6ff 0%, #f0e9ff 100%);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.decorative-line {
|
||||
width: 50px;
|
||||
height: 3px;
|
||||
background: var(--primary-gradient);
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.solution-card:hover .decorative-line {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.3rem;
|
||||
color: #2c0850;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content-point {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid rgba(137, 91, 255, 0.1);
|
||||
}
|
||||
|
||||
.point-icon {
|
||||
color: #4a3a6b;
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.point-text {
|
||||
color: #4a3a6b;
|
||||
line-height: 1.6;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@keyframes cardEnter {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,250 +0,0 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<div class="business-page">
|
||||
<!-- 渐变背景标题区 -->
|
||||
<section class="hero-section">
|
||||
<div class="container">
|
||||
<h1 style="font-size: 40px" class="hero-title">
|
||||
{{ $t("BusinessiIntroduction.CONTAIN.TITLEONE.TITLE") }}
|
||||
</h1>
|
||||
<div style="font-size: 18px" class="hero-description">
|
||||
{{ $t("BusinessiIntroduction.CONTAIN.TITLEONE.CONTENT") }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 业务核心解决方案 -->
|
||||
<main style="margin-top: 40px" class="container">
|
||||
<section>
|
||||
<h1 style="font-size: 40px" class="hero-title">
|
||||
{{ $t("BusinessiIntroduction.CONTAIN.TITLEONE.CONTENTTWO") }}
|
||||
</h1>
|
||||
</section>
|
||||
|
||||
<!-- 解决方案网格 - 响应式弹性布局 -->
|
||||
<div class="solution-grid">
|
||||
<!-- 统一使用弹性列布局,通过媒体查询控制排列方式 -->
|
||||
<div
|
||||
v-for="(solution, sIndex) in solutions"
|
||||
:key="sIndex"
|
||||
class="featured-solution"
|
||||
>
|
||||
<div
|
||||
class="solution-card"
|
||||
:style="{ '--delay': `${sIndex * 0.2}s` }"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div class="decorative-line"></div>
|
||||
<h2 class="card-title">{{ solution.title }}</h2>
|
||||
</div>
|
||||
<ul class="card-content">
|
||||
<li
|
||||
v-for="(point, pIndex) in solution.points"
|
||||
:key="pIndex"
|
||||
class="content-point"
|
||||
>
|
||||
<div class="point-icon">•</div>
|
||||
<div style="font-size: 18px" class="point-text">
|
||||
{{ point }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed } from "vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const solutions = computed(() => [
|
||||
{
|
||||
title: t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.ONE.TITLE"),
|
||||
points: [
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.ONE.CONTENT"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.ONE.CONTENTTWO"),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.TWO.TITLE"),
|
||||
points: [
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.TWO.CONTENT"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.TWO.CONTENTTWO"),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.THREE.TITLE"),
|
||||
points: [
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.THREE.CONTENT"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.THREE.CONTENTTWO"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.THREE.CONTENTTHREE"),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.FOUR.TITLE"),
|
||||
points: [
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.FOUR.CONTENT"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.FOUR.CONTENTTWO"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.FOUR.CONTENTTHREE"),
|
||||
],
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
/* 标题区 - 紫色渐变 */
|
||||
.hero-section {
|
||||
background: var(--primary-gradient);
|
||||
padding: 5rem 0 0rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 40px;
|
||||
color: black;
|
||||
margin-bottom: 2rem;
|
||||
animation: slideIn 1s ease;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
margin: 0 auto;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
color: black;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary-color: #895bff;
|
||||
--primary-light: #a07cff;
|
||||
--primary-dark: #6a11cb;
|
||||
--primary-gradient: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-light) 0%,
|
||||
var(--primary-color) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.home-page {
|
||||
background-image: url("@/assets/image/bg-mobile.png");
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
/* 解决方案网格 - 响应式布局 */
|
||||
.solution-grid {
|
||||
padding: 0rem 0 4rem;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr; /* 默认单列(移动端) */
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
/* 中等屏幕(768px-1439px) - 单列布局 */
|
||||
@media (min-width: 768px) and (max-width: 1439px) {
|
||||
.solution-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 大屏幕(≥1440px) - 双列布局 */
|
||||
@media (min-width: 1440px) {
|
||||
.solution-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.solution-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem;
|
||||
box-shadow: 0 10px 40px rgba(137, 91, 255, 0.1);
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
animation: cardEnter 0.6s ease forwards;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(137, 91, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.solution-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: var(--primary-gradient);
|
||||
}
|
||||
|
||||
.solution-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 15px 50px rgba(137, 91, 255, 0.2);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.decorative-line {
|
||||
width: 50px;
|
||||
height: 3px;
|
||||
background: var(--primary-gradient);
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.solution-card:hover .decorative-line {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.3rem;
|
||||
color: #2c0850;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content-point {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid rgba(137, 91, 255, 0.1);
|
||||
}
|
||||
|
||||
.content-point:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.point-icon {
|
||||
color: #4a3a6b;
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.point-text {
|
||||
color: #4a3a6b;
|
||||
line-height: 1.6;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@keyframes cardEnter {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,251 +0,0 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<div class="business-page">
|
||||
<!-- 渐变背景标题区 -->
|
||||
<section class="hero-section">
|
||||
<div class="container">
|
||||
<h1 style="font-size: 40px" class="hero-title">
|
||||
{{ $t("BusinessiIntroduction.CONTAIN.TITLEONE.TITLE") }}
|
||||
</h1>
|
||||
<div style="font-size: 18px" class="hero-description">
|
||||
{{ $t("BusinessiIntroduction.CONTAIN.TITLEONE.CONTENT") }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 业务核心解决方案 -->
|
||||
<main style="margin-top: 40px" class="container">
|
||||
<section>
|
||||
<h1 style="font-size: 40px" class="hero-title">
|
||||
{{ $t("BusinessiIntroduction.CONTAIN.TITLEONE.CONTENTTWO") }}
|
||||
</h1>
|
||||
</section>
|
||||
|
||||
<!-- 解决方案网格 - 响应式弹性布局 -->
|
||||
<div class="solution-grid">
|
||||
<!-- 统一使用弹性列布局,通过媒体查询控制排列方式 -->
|
||||
<div
|
||||
v-for="(solution, sIndex) in solutions"
|
||||
:key="sIndex"
|
||||
class="featured-solution"
|
||||
>
|
||||
<div
|
||||
class="solution-card"
|
||||
:style="{ '--delay': `${sIndex * 0.2}s` }"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div class="decorative-line"></div>
|
||||
<h2 class="card-title">{{ solution.title }}</h2>
|
||||
</div>
|
||||
<ul class="card-content">
|
||||
<li
|
||||
v-for="(point, pIndex) in solution.points"
|
||||
:key="pIndex"
|
||||
class="content-point"
|
||||
>
|
||||
<div class="point-icon">•</div>
|
||||
<div style="font-size: 18px" class="point-text">
|
||||
{{ point }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed } from "vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const solutions = computed(() => [
|
||||
{
|
||||
title: t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.ONE.TITLE"),
|
||||
points: [
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.ONE.CONTENT"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.ONE.CONTENTTWO"),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.TWO.TITLE"),
|
||||
points: [
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.TWO.CONTENT"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.TWO.CONTENTTWO"),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.THREE.TITLE"),
|
||||
points: [
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.THREE.CONTENT"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.THREE.CONTENTTWO"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.THREE.CONTENTTHREE"),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.FOUR.TITLE"),
|
||||
points: [
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.FOUR.CONTENT"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.FOUR.CONTENTTWO"),
|
||||
t("BusinessiIntroduction.CONTAIN.TITLEONE.paragraph.FOUR.CONTENTTHREE"),
|
||||
],
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
/* 标题区 - 紫色渐变 */
|
||||
.hero-section {
|
||||
background: var(--primary-gradient);
|
||||
padding: 5rem 0 0rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 40px;
|
||||
color: black;
|
||||
margin-bottom: 2rem;
|
||||
animation: slideIn 1s ease;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
margin: 0 auto;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
color: black;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary-color: #895bff;
|
||||
--primary-light: #a07cff;
|
||||
--primary-dark: #6a11cb;
|
||||
--primary-gradient: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-light) 0%,
|
||||
var(--primary-color) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.home-page {
|
||||
background-image: url("@/assets/image/bg-mobile.png");
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
/* 解决方案网格 - 响应式布局 */
|
||||
.solution-grid {
|
||||
padding: 0rem 0 4rem;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr; /* 默认单列(移动端) */
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
/* 中等屏幕(768px-1439px) - 单列布局 */
|
||||
@media (min-width: 768px) and (max-width: 1439px) {
|
||||
.solution-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 大屏幕(≥1440px) - 双列布局 */
|
||||
@media (min-width: 1440px) {
|
||||
.solution-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.solution-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem;
|
||||
box-shadow: 0 10px 40px rgba(137, 91, 255, 0.1);
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
animation: cardEnter 0.6s ease forwards;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(137, 91, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.solution-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: var(--primary-gradient);
|
||||
}
|
||||
|
||||
.solution-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 15px 50px rgba(137, 91, 255, 0.2);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.decorative-line {
|
||||
width: 50px;
|
||||
height: 3px;
|
||||
background: var(--primary-gradient);
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.solution-card:hover .decorative-line {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.3rem;
|
||||
color: #2c0850;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content-point {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid rgba(137, 91, 255, 0.1);
|
||||
}
|
||||
|
||||
.content-point:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.point-icon {
|
||||
color: #4a3a6b;
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.point-text {
|
||||
color: #4a3a6b;
|
||||
line-height: 1.6;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@keyframes cardEnter {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,34 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
|
||||
import size375 from "./size375/index.vue";
|
||||
import size768 from "./size768/index.vue";
|
||||
import size1440 from "./size1440/index.vue";
|
||||
import size1920 from "./size1920/index.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const router = useRouter();
|
||||
const { width } = useWindowSize();
|
||||
const { t } = useI18n();
|
||||
|
||||
const viewComponent = computed(() => {
|
||||
const viewWidth = width.value;
|
||||
if (viewWidth <= 450) {
|
||||
return size375;
|
||||
} else if (viewWidth <= 1100) {
|
||||
return size768;
|
||||
} else if (viewWidth <= 1500) {
|
||||
return size1440;
|
||||
} else if (viewWidth <= 1920 || viewWidth > 1920) {
|
||||
return size1920;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="viewComponent" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@ -1,373 +0,0 @@
|
||||
<template>
|
||||
<div class="committees-page">
|
||||
<!-- 标题区 -->
|
||||
|
||||
<div class="title mb-[50px] text-center">
|
||||
<h1 style="font-size: 40px; margin-top: 60px">Committee Composition</h1>
|
||||
</div>
|
||||
|
||||
<!-- 委员会表格 -->
|
||||
<div class="container">
|
||||
<div class="committees-table">
|
||||
<!-- 表头 - 委员会名称 -->
|
||||
<div class="table-header">
|
||||
<div class="director-cell"></div>
|
||||
<div class="committee-cell">
|
||||
<h3>Audit Committee</h3>
|
||||
</div>
|
||||
<div class="committee-cell">
|
||||
<h3>Compensation Committee</h3>
|
||||
</div>
|
||||
<div class="committee-cell">
|
||||
<h3>Nominating and Corporate Governance Committee</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格内容 - 每位董事 -->
|
||||
<div
|
||||
class="table-row"
|
||||
v-for="(director, index) in otherDirectors"
|
||||
:key="index"
|
||||
>
|
||||
<!-- 董事姓名 -->
|
||||
<div class="director-cell">
|
||||
<div class="director-info">
|
||||
<div class="avatar"></div>
|
||||
<div>
|
||||
<router-link
|
||||
:to="'/boarddirectors'"
|
||||
style="font-size: 18px"
|
||||
class="director-link"
|
||||
>
|
||||
{{ director.name }}
|
||||
</router-link>
|
||||
<!-- <p class="director-title">{{ director.title }}</p> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 委员会职位 -->
|
||||
<div class="committee-cell">
|
||||
<div class="role-badges">
|
||||
<template v-if="getCommitteeRole(director.name, 'Audit')">
|
||||
<div
|
||||
class="role-badge"
|
||||
:class="{
|
||||
[getCommitteeRole(
|
||||
director.name,
|
||||
'Audit'
|
||||
)?.toLowerCase()]: true,
|
||||
chair: getCommitteeRole(director.name, 'Audit') === 'Chair',
|
||||
}"
|
||||
>
|
||||
{{ getCommitteeRole(director.name, "Audit") }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="committee-cell">
|
||||
<div class="role-badges">
|
||||
<template v-if="getCommitteeRole(director.name, 'Compensation')">
|
||||
<div
|
||||
class="role-badge"
|
||||
:class="{
|
||||
[getCommitteeRole(
|
||||
director.name,
|
||||
'Compensation'
|
||||
)?.toLowerCase()]: true,
|
||||
chair:
|
||||
getCommitteeRole(director.name, 'Compensation') ===
|
||||
'Chair',
|
||||
}"
|
||||
>
|
||||
{{ getCommitteeRole(director.name, "Compensation") }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="committee-cell">
|
||||
<div class="role-badges">
|
||||
<template v-if="getCommitteeRole(director.name, 'Governance')">
|
||||
<div
|
||||
class="role-badge"
|
||||
:class="{
|
||||
[getCommitteeRole(
|
||||
director.name,
|
||||
'Governance'
|
||||
)?.toLowerCase()]: true,
|
||||
chair:
|
||||
getCommitteeRole(director.name, 'Governance') === 'Chair',
|
||||
}"
|
||||
>
|
||||
{{ getCommitteeRole(director.name, "Governance") }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const otherDirectors = [
|
||||
// {
|
||||
// name: "Cao Yu",
|
||||
// title: "Chief Financial Officer, Secretary, Treasurer and Director",
|
||||
// },
|
||||
// { name: "David Lazar", title: "Director" },
|
||||
{ name: "Hu Bin", title: "Director" },
|
||||
{ name: "David Natan", title: "Director" },
|
||||
{ name: "Chan Oi Fat", title: "Director" },
|
||||
];
|
||||
|
||||
// Updated committee roles according to requirements
|
||||
const committeeRoles = {
|
||||
"Cao Yu": {},
|
||||
"David Lazar": {},
|
||||
"Hu Bin": {
|
||||
Audit: "Member",
|
||||
Compensation: "Member",
|
||||
Governance: "Member",
|
||||
},
|
||||
"David Natan": {
|
||||
Audit: "Chair",
|
||||
Compensation: "Member",
|
||||
Governance: "Member",
|
||||
},
|
||||
"Chan Oi Fat": {
|
||||
Audit: "Member",
|
||||
Compensation: "Chair",
|
||||
Governance: "Chair",
|
||||
},
|
||||
};
|
||||
|
||||
const getCommitteeRole = (name, committee) => {
|
||||
return committeeRoles[name]?.[committee] || null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-badge.chair {
|
||||
color: orange;
|
||||
}
|
||||
.title h1 {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -14px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 80px;
|
||||
height: 3px;
|
||||
background: #895bff;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
/* 紫色主题变量 */
|
||||
:root {
|
||||
--primary: #895bff;
|
||||
--primary-light: #a07cff;
|
||||
--primary-dark: #6a11cb;
|
||||
--primary-transparent: rgba(137, 91, 255, 0.1);
|
||||
}
|
||||
|
||||
.committees-page {
|
||||
background-image: url("@/assets/image/bg.png");
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 标题区设计 */
|
||||
.hero-section {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-light) 0%,
|
||||
var(--primary) 100%
|
||||
);
|
||||
padding: 6rem 2rem;
|
||||
text-align: center;
|
||||
color: #895bff;
|
||||
}
|
||||
|
||||
.hero-section h1 {
|
||||
font-size: 2.8rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hero-section p {
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
/* 表格设计 */
|
||||
.committees-table {
|
||||
margin: 4rem 0;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(137, 91, 255, 0.08);
|
||||
}
|
||||
|
||||
.table-header,
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr repeat(3, 1fr);
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background: #f9f6ff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.committee-cell {
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.committee-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.committee-cell h3 {
|
||||
color: var(--primary-dark);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.director-cell {
|
||||
padding: 1.5rem;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.director-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
.director-info h4 {
|
||||
font-size: 1.1rem;
|
||||
color: #333;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.director-title {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 职位徽章设计 */
|
||||
.role-badges {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.badge-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 0.5rem;
|
||||
background: currentColor;
|
||||
mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l5 5L20 7"/></svg>')
|
||||
no-repeat center;
|
||||
}
|
||||
|
||||
/* 悬停效果 */
|
||||
.table-row {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background: #fdfcff;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 5px 15px rgba(137, 91, 255, 0.05);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1024px) {
|
||||
.committees-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table-header,
|
||||
.table-row {
|
||||
grid-template-columns: 250px repeat(3, 200px);
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-section {
|
||||
padding: 4rem 1rem;
|
||||
}
|
||||
|
||||
.hero-section h1 {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.director-info {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.director-cell {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.committee-cell {
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
}
|
||||
.director-link {
|
||||
color: #895bff;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.director-link:hover {
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
@ -1,370 +0,0 @@
|
||||
<template>
|
||||
<div class="committees-page">
|
||||
<!-- 标题区 -->
|
||||
<div class="title mb-[50px] text-center">
|
||||
<h1 style="font-size: 40px; margin-top: 60px">Committee Composition</h1>
|
||||
</div>
|
||||
|
||||
<!-- 委员会表格 -->
|
||||
<div class="container">
|
||||
<div class="committees-table">
|
||||
<!-- 表头 - 委员会名称 -->
|
||||
<div class="table-header">
|
||||
<div class="director-cell"></div>
|
||||
<div class="committee-cell">
|
||||
<h3>Audit Committee</h3>
|
||||
</div>
|
||||
<div class="committee-cell">
|
||||
<h3>Compensation Committee</h3>
|
||||
</div>
|
||||
<div class="committee-cell">
|
||||
<h3>Nominating and Corporate Governance Committee</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格内容 - 每位董事 -->
|
||||
<div
|
||||
class="table-row"
|
||||
v-for="(director, index) in otherDirectors"
|
||||
:key="index"
|
||||
>
|
||||
<!-- 董事姓名 -->
|
||||
<div class="director-cell">
|
||||
<div class="director-info">
|
||||
<div class="avatar"></div>
|
||||
<div>
|
||||
<router-link
|
||||
:to="'/boarddirectors'"
|
||||
style="font-size: 18px"
|
||||
class="director-link"
|
||||
>
|
||||
{{ director.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 委员会职位 -->
|
||||
<div class="committee-cell">
|
||||
<div class="role-badges">
|
||||
<template v-if="getCommitteeRole(director.name, 'Audit')">
|
||||
<div
|
||||
class="role-badge"
|
||||
:class="{
|
||||
[getCommitteeRole(
|
||||
director.name,
|
||||
'Audit'
|
||||
)?.toLowerCase()]: true,
|
||||
chair: getCommitteeRole(director.name, 'Audit') === 'Chair',
|
||||
}"
|
||||
>
|
||||
{{ getCommitteeRole(director.name, "Audit") }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="committee-cell">
|
||||
<div class="role-badges">
|
||||
<template v-if="getCommitteeRole(director.name, 'Compensation')">
|
||||
<div
|
||||
class="role-badge"
|
||||
:class="{
|
||||
[getCommitteeRole(
|
||||
director.name,
|
||||
'Compensation'
|
||||
)?.toLowerCase()]: true,
|
||||
chair:
|
||||
getCommitteeRole(director.name, 'Compensation') ===
|
||||
'Chair',
|
||||
}"
|
||||
>
|
||||
{{ getCommitteeRole(director.name, "Compensation") }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="committee-cell">
|
||||
<div class="role-badges">
|
||||
<template v-if="getCommitteeRole(director.name, 'Governance')">
|
||||
<div
|
||||
class="role-badge"
|
||||
:class="{
|
||||
[getCommitteeRole(
|
||||
director.name,
|
||||
'Governance'
|
||||
)?.toLowerCase()]: true,
|
||||
chair:
|
||||
getCommitteeRole(director.name, 'Governance') === 'Chair',
|
||||
}"
|
||||
>
|
||||
{{ getCommitteeRole(director.name, "Governance") }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const otherDirectors = [
|
||||
// {
|
||||
// name: "Cao Yu",
|
||||
// title: "Chief Financial Officer, Secretary, Treasurer and Director",
|
||||
// },
|
||||
// { name: "David Lazar", title: "Director" },
|
||||
{ name: "Hu Bin", title: "Director" },
|
||||
{ name: "David Natan", title: "Director" },
|
||||
{ name: "Chan Oi Fat", title: "Director" },
|
||||
];
|
||||
|
||||
// Updated committee roles according to requirements
|
||||
const committeeRoles = {
|
||||
"Cao Yu": {},
|
||||
"David Lazar": {},
|
||||
"Hu Bin": {
|
||||
Audit: "Member",
|
||||
Compensation: "Member",
|
||||
Governance: "Member",
|
||||
},
|
||||
"David Natan": {
|
||||
Audit: "Chair",
|
||||
Compensation: "Member",
|
||||
Governance: "Member",
|
||||
},
|
||||
"Chan Oi Fat": {
|
||||
Audit: "Member",
|
||||
Compensation: "Chair",
|
||||
Governance: "Chair",
|
||||
},
|
||||
};
|
||||
|
||||
const getCommitteeRole = (name, committee) => {
|
||||
return committeeRoles[name]?.[committee] || null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 紫色主题变量 */
|
||||
.role-badge.chair {
|
||||
color: orange;
|
||||
}
|
||||
:root {
|
||||
--primary: #895bff;
|
||||
--primary-light: #a07cff;
|
||||
--primary-dark: #6a11cb;
|
||||
--primary-transparent: rgba(137, 91, 255, 0.1);
|
||||
}
|
||||
|
||||
.committees-page {
|
||||
background-image: url("@/assets/image/bg.png");
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.title h1 {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -14px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 80px;
|
||||
height: 3px;
|
||||
background: #895bff;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
/* 标题区设计 */
|
||||
.hero-section {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-light) 0%,
|
||||
var(--primary) 100%
|
||||
);
|
||||
padding: 6rem 2rem;
|
||||
text-align: center;
|
||||
color: #895bff;
|
||||
}
|
||||
|
||||
.hero-section h1 {
|
||||
font-size: 2.8rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hero-section p {
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
/* 表格设计 */
|
||||
.committees-table {
|
||||
margin: 4rem 0;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(137, 91, 255, 0.08);
|
||||
}
|
||||
|
||||
.table-header,
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr repeat(3, 1fr);
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background: #f9f6ff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.committee-cell {
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.committee-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.committee-cell h3 {
|
||||
color: var(--primary-dark);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.director-cell {
|
||||
padding: 1.5rem;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.director-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
.director-info h4 {
|
||||
font-size: 1.1rem;
|
||||
color: #333;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.director-title {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 职位徽章设计 */
|
||||
.role-badges {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.badge-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 0.5rem;
|
||||
background: currentColor;
|
||||
mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l5 5L20 7"/></svg>')
|
||||
no-repeat center;
|
||||
}
|
||||
|
||||
/* 悬停效果 */
|
||||
.table-row {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background: #fdfcff;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 5px 15px rgba(137, 91, 255, 0.05);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1024px) {
|
||||
.committees-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table-header,
|
||||
.table-row {
|
||||
grid-template-columns: 250px repeat(3, 200px);
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-section {
|
||||
padding: 4rem 1rem;
|
||||
}
|
||||
|
||||
.hero-section h1 {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.director-info {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.director-cell {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.committee-cell {
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
}
|
||||
.director-link {
|
||||
color: #895bff;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.director-link:hover {
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
@ -1,337 +0,0 @@
|
||||
<template>
|
||||
<div class="board-members-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="title mb-[50px] text-center">
|
||||
<h1 style="font-size: 40px; margin-top: 60px">Committee Composition</h1>
|
||||
</div>
|
||||
<!-- 移动端视图 -->
|
||||
<div class="container">
|
||||
<div
|
||||
class="director-card"
|
||||
v-for="(director, index) in otherDirectors"
|
||||
:key="director.name"
|
||||
:style="{ '--delay': index * 0.1 + 's' }"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div class="director-info">
|
||||
<div class="avatar">
|
||||
<span class="initials">{{ getInitials(director.name) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<router-link
|
||||
:to="`/boarddirectors/${director.name}`"
|
||||
class="director-name"
|
||||
>
|
||||
{{ director.name }}
|
||||
</router-link>
|
||||
<!-- <p class="director-title">{{ director.title }}</p> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="committee-groups">
|
||||
<!-- 委员会职位 -->
|
||||
<div
|
||||
class="committee-group"
|
||||
v-if="getCommittees(director.name).length > 0"
|
||||
>
|
||||
<div class="role-badges">
|
||||
<template
|
||||
v-for="(committee, idx) in getCommittees(director.name)"
|
||||
:key="idx"
|
||||
>
|
||||
<div class="committee-position">
|
||||
<div
|
||||
class="role-badge"
|
||||
:class="
|
||||
getCommitteeRole(director.name, committee).toLowerCase()
|
||||
"
|
||||
>
|
||||
<span>{{ getCommitteeShortName(committee) }}</span>
|
||||
</div>
|
||||
<div style="font-size: 16px" class="role-title">
|
||||
{{ getCommitteeRole(director.name, committee) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
// 董事会成员数据
|
||||
const otherDirectors = [
|
||||
// {
|
||||
// name: "Cao Yu",
|
||||
// title: "Chief Financial Officer, Secretary, Treasurer and Director",
|
||||
// },
|
||||
// { name: "David Lazar", title: "Director" },
|
||||
{ name: "Hu Bin", title: "Director" },
|
||||
{ name: "David Natan", title: "Director" },
|
||||
{ name: "Chan Oi Fat", title: "Director" },
|
||||
];
|
||||
|
||||
// 委员会角色数据 - 现在包含职位类型 (Chair/Member)
|
||||
const committeeRoles = {
|
||||
"Cao Yu": {},
|
||||
"David Lazar": {},
|
||||
"Hu Bin": {
|
||||
Audit: "Member",
|
||||
Compensation: "Member",
|
||||
Governance: "Member",
|
||||
},
|
||||
"David Natan": {
|
||||
Audit: "Chair",
|
||||
Compensation: "Member",
|
||||
Governance: "Member",
|
||||
},
|
||||
"Chan Oi Fat": {
|
||||
Audit: "Member",
|
||||
Compensation: "Chair",
|
||||
Governance: "Chair",
|
||||
},
|
||||
};
|
||||
|
||||
// 委员会完整名称
|
||||
const committeeFullNames = {
|
||||
Audit: "Audit Committee",
|
||||
Compensation: "Compensation Committee",
|
||||
Governance: "Nominating and Corporate Governance Committee",
|
||||
};
|
||||
|
||||
// 获取委员会列表
|
||||
const getCommittees = (name) => {
|
||||
return Object.keys(committeeRoles[name] || {});
|
||||
};
|
||||
|
||||
// 获取委员会角色 (Chair/Member)
|
||||
const getCommitteeRole = (name, committee) => {
|
||||
return committeeRoles[name]?.[committee] || "";
|
||||
};
|
||||
|
||||
// 获取委员会简称
|
||||
const getCommitteeShortName = (committee) => {
|
||||
const names = {
|
||||
Audit: "Audit",
|
||||
Compensation: "Comp.",
|
||||
Governance: "Governance",
|
||||
};
|
||||
return names[committee] || committee;
|
||||
};
|
||||
|
||||
// 获取姓名首字母
|
||||
const getInitials = (name) => {
|
||||
return name
|
||||
.split(" ")
|
||||
.map((word) => word[0])
|
||||
.join("")
|
||||
.toUpperCase();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 添加这些样式来显示职位类型 */
|
||||
.committee-position {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.role-title {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
/* 保持原有的角色徽章样式 */
|
||||
.role-badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.role-badge.chair {
|
||||
background-color: #e6f2ff;
|
||||
color: #0066cc;
|
||||
}
|
||||
|
||||
.role-badge.member {
|
||||
background-color: #f0f0f0;
|
||||
color: #555;
|
||||
}
|
||||
.title h1 {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -14px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 80px;
|
||||
height: 3px;
|
||||
background: #895bff;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
/* 基础变量 */
|
||||
:root {
|
||||
--primary: #895bff;
|
||||
--primary-light: #a07cff;
|
||||
--primary-dark: #6a11cb;
|
||||
--text-primary: #333;
|
||||
--text-secondary: #666;
|
||||
--bg-light: #f9f6ff;
|
||||
--border-radius: 12px;
|
||||
}
|
||||
|
||||
/* 页面样式 */
|
||||
.board-members-page {
|
||||
background-image: url("@/assets/image/bg-mobile.png");
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 16px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.hero-section {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-light) 0%,
|
||||
var(--primary) 100%
|
||||
);
|
||||
padding: 3rem 1rem;
|
||||
text-align: center;
|
||||
color: #895bff;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: clamp(1.75rem, 5vw, 2.25rem);
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: clamp(1rem, 3vw, 1.25rem);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 董事卡片 */
|
||||
.director-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 5px 20px rgba(137, 91, 255, 0.08);
|
||||
overflow: hidden;
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.5s ease forwards;
|
||||
animation-delay: var(--delay);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 1.25rem;
|
||||
background: var(--bg-light);
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.director-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: var(--primary-transparent);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.initials {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.director-name {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.director-title {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* 委员会职位 */
|
||||
.committee-groups {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.role-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 16px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
background: rgba(137, 91, 255, 0.08);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.role-badge.chair {
|
||||
background: rgba(137, 91, 255, 0.15);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.committee-name {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
@ -1,337 +0,0 @@
|
||||
<template>
|
||||
<div class="board-members-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="title mb-[50px] text-center">
|
||||
<h1 style="font-size: 40px; margin-top: 60px">Committee Composition</h1>
|
||||
</div>
|
||||
<!-- 移动端视图 -->
|
||||
<div class="container">
|
||||
<div
|
||||
class="director-card"
|
||||
v-for="(director, index) in otherDirectors"
|
||||
:key="director.name"
|
||||
:style="{ '--delay': index * 0.1 + 's' }"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div class="director-info">
|
||||
<div class="avatar">
|
||||
<span class="initials">{{ getInitials(director.name) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<router-link
|
||||
:to="`/boarddirectors/${director.name}`"
|
||||
class="director-name"
|
||||
>
|
||||
{{ director.name }}
|
||||
</router-link>
|
||||
<!-- <p class="director-title">{{ director.title }}</p> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="committee-groups">
|
||||
<!-- 委员会职位 -->
|
||||
<div
|
||||
class="committee-group"
|
||||
v-if="getCommittees(director.name).length > 0"
|
||||
>
|
||||
<div class="role-badges">
|
||||
<template
|
||||
v-for="(committee, idx) in getCommittees(director.name)"
|
||||
:key="idx"
|
||||
>
|
||||
<div class="committee-position">
|
||||
<div
|
||||
class="role-badge"
|
||||
:class="
|
||||
getCommitteeRole(director.name, committee).toLowerCase()
|
||||
"
|
||||
>
|
||||
<span>{{ getCommitteeShortName(committee) }}</span>
|
||||
</div>
|
||||
<div style="font-size: 16px" class="role-title">
|
||||
{{ getCommitteeRole(director.name, committee) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
// 董事会成员数据
|
||||
const otherDirectors = [
|
||||
// {
|
||||
// name: "Cao Yu",
|
||||
// title: "Chief Financial Officer, Secretary, Treasurer and Director",
|
||||
// },
|
||||
// { name: "David Lazar", title: "Director" },
|
||||
{ name: "Hu Bin", title: "Director" },
|
||||
{ name: "David Natan", title: "Director" },
|
||||
{ name: "Chan Oi Fat", title: "Director" },
|
||||
];
|
||||
|
||||
// 委员会角色数据 - 现在包含职位类型 (Chair/Member)
|
||||
const committeeRoles = {
|
||||
"Cao Yu": {},
|
||||
"David Lazar": {},
|
||||
"Hu Bin": {
|
||||
Audit: "Member",
|
||||
Compensation: "Member",
|
||||
Governance: "Member",
|
||||
},
|
||||
"David Natan": {
|
||||
Audit: "Chair",
|
||||
Compensation: "Member",
|
||||
Governance: "Member",
|
||||
},
|
||||
"Chan Oi Fat": {
|
||||
Audit: "Member",
|
||||
Compensation: "Chair",
|
||||
Governance: "Chair",
|
||||
},
|
||||
};
|
||||
|
||||
// 委员会完整名称
|
||||
const committeeFullNames = {
|
||||
Audit: "Audit Committee",
|
||||
Compensation: "Compensation Committee",
|
||||
Governance: "Nominating and Corporate Governance Committee",
|
||||
};
|
||||
|
||||
// 获取委员会列表
|
||||
const getCommittees = (name) => {
|
||||
return Object.keys(committeeRoles[name] || {});
|
||||
};
|
||||
|
||||
// 获取委员会角色 (Chair/Member)
|
||||
const getCommitteeRole = (name, committee) => {
|
||||
return committeeRoles[name]?.[committee] || "";
|
||||
};
|
||||
|
||||
// 获取委员会简称
|
||||
const getCommitteeShortName = (committee) => {
|
||||
const names = {
|
||||
Audit: "Audit",
|
||||
Compensation: "Comp.",
|
||||
Governance: "Governance",
|
||||
};
|
||||
return names[committee] || committee;
|
||||
};
|
||||
|
||||
// 获取姓名首字母
|
||||
const getInitials = (name) => {
|
||||
return name
|
||||
.split(" ")
|
||||
.map((word) => word[0])
|
||||
.join("")
|
||||
.toUpperCase();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 添加这些样式来显示职位类型 */
|
||||
.committee-position {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.role-title {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
/* 保持原有的角色徽章样式 */
|
||||
.role-badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.role-badge.chair {
|
||||
background-color: #e6f2ff;
|
||||
color: #0066cc;
|
||||
}
|
||||
|
||||
.role-badge.member {
|
||||
background-color: #f0f0f0;
|
||||
color: #555;
|
||||
}
|
||||
.title h1 {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -14px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 80px;
|
||||
height: 3px;
|
||||
background: #895bff;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
/* 基础变量 */
|
||||
:root {
|
||||
--primary: #895bff;
|
||||
--primary-light: #a07cff;
|
||||
--primary-dark: #6a11cb;
|
||||
--text-primary: #333;
|
||||
--text-secondary: #666;
|
||||
--bg-light: #f9f6ff;
|
||||
--border-radius: 12px;
|
||||
}
|
||||
|
||||
/* 页面样式 */
|
||||
.board-members-page {
|
||||
background-image: url("@/assets/image/bg-mobile.png");
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 16px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.hero-section {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-light) 0%,
|
||||
var(--primary) 100%
|
||||
);
|
||||
padding: 3rem 1rem;
|
||||
text-align: center;
|
||||
color: #895bff;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: clamp(1.75rem, 5vw, 2.25rem);
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: clamp(1rem, 3vw, 1.25rem);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 董事卡片 */
|
||||
.director-card {
|
||||
background: white;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 5px 20px rgba(137, 91, 255, 0.08);
|
||||
overflow: hidden;
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.5s ease forwards;
|
||||
animation-delay: var(--delay);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 1.25rem;
|
||||
background: var(--bg-light);
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.director-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: var(--primary-transparent);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.initials {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.director-name {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.director-title {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* 委员会职位 */
|
||||
.committee-groups {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.role-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 16px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
background: rgba(137, 91, 255, 0.08);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.role-badge.chair {
|
||||
background: rgba(137, 91, 255, 0.15);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.committee-name {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
@ -1,22 +0,0 @@
|
||||
<script setup>
|
||||
import size1920 from "@/views/boarddirectors/size1920/index.vue";
|
||||
import size375 from "@/views/boarddirectors/size375/index.vue";
|
||||
import { computed } from "vue";
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
|
||||
const { width } = useWindowSize();
|
||||
const viewComponent = computed(() => {
|
||||
const viewWidth = width.value;
|
||||
if (viewWidth <= 450) {
|
||||
return size375;
|
||||
} else if (viewWidth <= 1920 || viewWidth > 1920) {
|
||||
return size1920;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="viewComponent" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@ -1,137 +0,0 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<div class="directors-page">
|
||||
<h1 class="page-title">Board of Directors</h1>
|
||||
<n-divider />
|
||||
|
||||
<div class="directors-list">
|
||||
<div
|
||||
v-for="(director, index) in otherDirectors"
|
||||
:key="index"
|
||||
class="director-item"
|
||||
>
|
||||
<n-h2 style="font-size: 18px" class="director-name">{{
|
||||
director.name
|
||||
}}</n-h2>
|
||||
<n-text style="font-size: 16px" class="director-title">{{
|
||||
director.title
|
||||
}}</n-text>
|
||||
<n-divider class="divider" />
|
||||
<n-p class="director-bio">{{ director.contain }}</n-p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const otherDirectors = [
|
||||
{
|
||||
name: "Hu Bin",
|
||||
title: "Chairman of the Board of Directors",
|
||||
contain:
|
||||
"Served as a director of DC International Service Trade GmbH since December 2024. Prior to that, Mr. Hu worked as a freelancer in the tourism industry from April 2001 to October 2024. From April 1994 to October 2000, he served as the general manager of Suzhou Wintime Advertising Co., Ltd. Before that, he served as the general manager of Suzhou Bauhaus Advertising Design Co., Ltd. from August 1992 to April 1994, where he was engaged in computer-aided design and 3D computer animation production. Mr. Hu began his career at Suzhou Advertising Company in October 1989, where he worked as a designer responsible for graphic design, platemaking, printing, and interior decoration. Mr. Hu graduated from Suzhou Academy of Arts in 1989.",
|
||||
},
|
||||
{
|
||||
name: "Cao Yu",
|
||||
title: "Chief Financial Officer, Secretary, Treasurer and Director",
|
||||
contain:
|
||||
"Previously served as the treasury director of Taifeng Cultural Communication Co., Ltd where she oversees its financial matters from November 2018 to November 2024. Prior to that, Ms. Cao served as a business manager of Yangfeng Art Exchange Co., Ltd from February 2016 to October 2018. From March 2011 to January 2016, she served as the treasury officer of financial department of Suzhou Industrial Park Xinfushida Plastic Profile Products Co., Ltd.",
|
||||
},
|
||||
{
|
||||
name: "David E. Lazar",
|
||||
title: "Director",
|
||||
contain:
|
||||
"Served as the Chief Executive Officer of OpGen, Inc., a precision medicine company listed on the Nasdaq (OPGN) since April 11, 2024, where he also servs as a director and board chairman, beginning on March 25, 2024. Mr. Lazar served as the Chief Executive Officer of Titan Pharmaceuticals Inc. listed on the Nasdaq (TTNP) from August 2022 through April 11, 2024, where he also served as a director and board chairman from August 2022 until October 2023. He has also served as the CEO of Custodian Ventures LLC, a company which specializes in assisting distressed public companies through custodianship, since February 2018, and Activist Investing LLC, an actively managed private investment fund, since March 2018. Previously, Mr. Lazar served as Managing Partner at Zenith Partners International Inc., a boutique consulting firm, from July 2012 to April 2018. In his role as Chief Executive Officer of Custodian Ventures LLC, Mr. Lazar has successfully served as a custodian to numerous public companies across a wide range of industries.",
|
||||
},
|
||||
|
||||
{
|
||||
name: "David Natan",
|
||||
title: "Director",
|
||||
contain:
|
||||
"Currently serves as President and Chief Executive Officer of Natan & Associates, LLC, a consulting firm offering chief financial officer services to public and private companies in a variety of industries, since 2007. Mr. Natan previously served as a Director of the Company from November 2023 to February 2025. From February 2010 to May 2020, Mr. Natan served as Chief Executive Officer of ForceField Energy, Inc. (OTCMKTS: FNRG), a company focused on the solar industry and LED lighting products. From February 2002 to November 2007, Mr. Natan served as Executive Vice President of Reporting and Chief Financial Officer of PharmaNet Development Group, Inc., a drug development services company, and, from June 1995 to February 2002, as Chief Financial Officer and Vice President of Global Technovations, Inc., a manufacturer and marketer of oil analysis instruments and speakers and speaker components. Prior to that, Mr. Natan served in various roles of increasing responsibility with Deloitte & Touche LLP, a global consulting firm. Mr. Natan currently serves as a member of the Board of Directors and Chair of the Audit Committee of Sunshine Biopharma, Inc. (Nasdaq: SBFM), a pharmaceutical and nutritional supplement company, since February 2022. Previously, Mr. Natan has served as a director for the following public companies: Global Technovations, Forcefield Energy, Titan Pharmaceuticals (Nasdaq: TTNP), Vivakor Inc. (Nasdaq: VIVK), NetBrands Corp. (OTC: NBND), and OpGen Inc. (OTC: OPGN), and Cyclacel Pharmaceuticals (Nasdaq: CYCC). Mr. Natan holds a B.A. in Economics from Boston University.",
|
||||
},
|
||||
{
|
||||
name: "Chan Oi Fat",
|
||||
title: "Director",
|
||||
contain:
|
||||
"Served as Vice President – Finance of SML Group Corporation since March 2018 and as Company Secretary of China Leon Inspection Holding Limited (HKEX: 1586) since February 2018 and of Raily Aesthetic Medicine International Holdings Limited (HKEX: 2135) since November 2020. He is an independent non-executive director of Huajin International Holdings Limited (HKEX: 2738) (since March 2025) and UBoT Holding Limited (HKEX GEM: 8529) (since May 2024) and previously served as an independent non-executive director of China Saftower International Holding Group Limited (HKEX GEM: 8623) from June 2020 to December 2023 and Shanghai Prime Machinery Company Limited (HKEX: 2345) from June 2014 to January 2021. Mr. Chan holds a B.B.A. (Hons) in Accountancy from the City University of Hong Kong (2000) and is a member of the Association of Chartered Certified Accountants (since 2003) and the Hong Kong Institute of Certified Public Accountants (since 2004).",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-page {
|
||||
background-image: url("@/assets/image/bg.png");
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.directors-page {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 60px 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2.5rem; /* 18px */
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.directors-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 48px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.director-item {
|
||||
padding-bottom: 48px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.director-item:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.director-name {
|
||||
margin-bottom: 8px;
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.director-title {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 16px 0;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.director-bio {
|
||||
line-height: 1.8;
|
||||
color: #4a4a4a;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.directors-page {
|
||||
padding: 80px 40px;
|
||||
}
|
||||
|
||||
.director-name {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.director-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|