Index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. <template>
  2. <DefaultLayout>
  3. <!-- banner -->
  4. <section class="banner-section">
  5. <img src="@assets/about-banner-s1.png" alt="" srcset="">
  6. </section>
  7. <section class="banner-section about-us">
  8. <img src="@assets/about-banner-s2.png" alt="" srcset="">
  9. <!-- 数据模块 -->
  10. <div class="data-stats" ref="dataStatsRef">
  11. <div class="stats-item">
  12. <div class="stats-number">{{ animatedStats.avgAge }}</div>
  13. <div class="stats-label">员工平均年龄</div>
  14. </div>
  15. <div class="stats-divider"></div>
  16. <div class="stats-item">
  17. <div class="stats-number">{{ animatedStats.education }}%</div>
  18. <div class="stats-label">本科学历以上占比</div>
  19. </div>
  20. <div class="stats-divider"></div>
  21. <div class="stats-item">
  22. <div class="stats-number">{{ animatedStats.technical }}%</div>
  23. <div class="stats-label">专业技术人员占比</div>
  24. </div>
  25. <div class="stats-divider"></div>
  26. <div class="stats-item">
  27. <div class="stats-number">{{ animatedStats.professional }}%</div>
  28. <div class="stats-label">行业专业技术人才占比</div>
  29. </div>
  30. <div class="stats-divider"></div>
  31. <div class="stats-item">
  32. <div class="stats-number">{{ animatedStats.senior }}%</div>
  33. <div class="stats-label">中高级职称人员占比</div>
  34. </div>
  35. </div>
  36. </section>
  37. <section class="banner-section">
  38. <img src="@assets/about-banner-s3.png" alt="" srcset="">
  39. </section>
  40. <!-- 企业资质 -->
  41. <section class="qualification-section">
  42. <div class="container">
  43. <!-- 标题 -->
  44. <div class="section-title">
  45. <h2>企业资质</h2>
  46. <p class="title-en">ENTERPRISE QUALIFICATION</p>
  47. </div>
  48. <!-- 证书切换按钮 -->
  49. <div class="certificate-tabs">
  50. <button v-for="(category, index) in certificateCategories" :key="index"
  51. :class="['tab-btn', { active: activeTab === index }]" @click="switchTab(index)">
  52. {{ category.name }}
  53. </button>
  54. </div>
  55. <!-- 证书列表 -->
  56. <div class="certificate-container">
  57. <!-- 加载状态 -->
  58. <div v-if="loading" class="loading-state">
  59. <div class="loading-spinner"></div>
  60. <p>正在加载证书数据...</p>
  61. </div>
  62. <!-- 无数据状态 -->
  63. <div v-else-if="currentCertificates.length === 0" class="empty-state">
  64. <p>暂无{{ certificateCategories[activeTab]?.name }}数据</p>
  65. </div>
  66. <!-- 证书内容 -->
  67. <div v-else>
  68. <div class="certificate-slider"
  69. :style="{ transform: `translateX(-${currentSlide * itemWidth * itemsPerPage}px)` }">
  70. <div v-for="(certificate, index) in currentCertificates" :key="certificate.id || index" class="certificate-item">
  71. <img :src="imgHost+certificate.image" :alt="certificate.title" @error="handleImageError" />
  72. <!-- <p class="certificate-title">{{ certificate.title }}</p> -->
  73. </div>
  74. </div>
  75. <!-- 左右切换按钮 -->
  76. <button v-if="showNavButtons && currentSlide > 0" class="nav-btn nav-btn-left" @click="prevSlide">
  77. <img src="@assets/slide-arrow-left.png" alt="上一页" />
  78. </button>
  79. <button v-if="showNavButtons && currentSlide < maxSlide" class="nav-btn nav-btn-right" @click="nextSlide">
  80. <img src="@assets/slide-arrow-right.png" alt="下一页" />
  81. </button>
  82. </div>
  83. </div>
  84. </div>
  85. </section>
  86. </DefaultLayout>
  87. </template>
  88. <script>
  89. import { ref, onMounted, computed, onUnmounted } from 'vue'
  90. import { getCompanyCertList } from '@/api/modules/home'
  91. import DefaultLayout from '@/layouts/DefaultLayout.vue'
  92. import { animateNumber, createNumberAnimationObserver } from '@/utils/numberAnimation'
  93. export default {
  94. name: 'AboutPage',
  95. components: {
  96. DefaultLayout
  97. },
  98. setup() {
  99. // 环境变量
  100. const imgHost = import.meta.env.VITE_APP_IMG_HOST
  101. // 当前激活的标签页
  102. const activeTab = ref(0)
  103. // 当前滑动位置
  104. const currentSlide = ref(0)
  105. // 每页显示的证书数量
  106. const itemsPerPage = ref(5)
  107. // 每个证书项的宽度(包括间距)
  108. const itemWidth = ref(220)
  109. // 加载状态
  110. const loading = ref(false)
  111. // 数据统计相关
  112. const dataStatsRef = ref(null)
  113. const animationObserver = ref(null)
  114. // 动画数字状态
  115. const animatedStats = ref({
  116. avgAge: 0,
  117. education: 0,
  118. technical: 0,
  119. professional: 0,
  120. senior: 0
  121. })
  122. // 目标数字
  123. const targetStats = {
  124. avgAge: 31,
  125. education: 83,
  126. technical: 57,
  127. professional: 38,
  128. senior: 53
  129. }
  130. // 证书分类数据
  131. const certificateCategories = ref([
  132. {
  133. name: '授权证书',
  134. certType: 0,
  135. certificates: []
  136. },
  137. {
  138. name: '著作证书',
  139. certType: 1,
  140. certificates: []
  141. },
  142. {
  143. name: '荣誉证书',
  144. certType: 2,
  145. certificates: []
  146. }
  147. ])
  148. // 当前分类的证书列表
  149. const currentCertificates = computed(() => {
  150. return certificateCategories.value[activeTab.value]?.certificates || []
  151. })
  152. // 是否显示导航按钮
  153. const showNavButtons = computed(() => {
  154. return currentCertificates.value.length > itemsPerPage.value
  155. })
  156. // 最大滑动页数
  157. const maxSlide = computed(() => {
  158. return Math.max(0, Math.ceil(currentCertificates.value.length / itemsPerPage.value) - 1)
  159. })
  160. // 切换标签页
  161. const switchTab = (index) => {
  162. activeTab.value = index
  163. currentSlide.value = 0 // 重置滑动位置
  164. }
  165. // 上一页
  166. const prevSlide = () => {
  167. if (currentSlide.value > 0) {
  168. currentSlide.value--
  169. }
  170. }
  171. // 下一页
  172. const nextSlide = () => {
  173. if (currentSlide.value < maxSlide.value) {
  174. currentSlide.value++
  175. }
  176. }
  177. // 图片加载错误处理
  178. const handleImageError = (event) => {
  179. // 设置默认图片或隐藏
  180. event.target.src = 'https://via.placeholder.com/256x354?text=证书图片'
  181. }
  182. // 加载证书数据
  183. const loadCertificates = async () => {
  184. try {
  185. loading.value = true
  186. const response = await getCompanyCertList({companyId:1})
  187. // console.log('证书数据',response)
  188. if (response && response.rows) {
  189. // 清空现有证书数据
  190. certificateCategories.value.forEach(category => {
  191. category.certificates = []
  192. })
  193. // 根据certType分类证书
  194. response.rows.forEach(cert => {
  195. const category = certificateCategories.value.find(cat => cat.certType === cert.certType)
  196. if (category) {
  197. category.certificates.push({
  198. id: cert.id,
  199. title: cert.certName || cert.title,
  200. image: cert.certFront || cert.certBack,
  201. certType: cert.certType
  202. })
  203. }
  204. // console.log('证书数据category',category)
  205. })
  206. }
  207. } catch (error) {
  208. console.error('加载证书数据失败:', error)
  209. } finally {
  210. loading.value = false
  211. }
  212. }
  213. // 启动数字动画
  214. const startStatsAnimation = () => {
  215. // 为每个统计数字创建动画,添加延迟以创建连续效果
  216. const delays = [0, 200, 400, 600, 800]
  217. Object.keys(targetStats).forEach((key, index) => {
  218. setTimeout(() => {
  219. animateNumber(
  220. (value) => {
  221. animatedStats.value[key] = value
  222. },
  223. targetStats[key],
  224. 2000 // 2秒动画时长
  225. )
  226. }, delays[index])
  227. })
  228. }
  229. // 设置数字动画观察器
  230. const setupStatsAnimation = () => {
  231. if (dataStatsRef.value) {
  232. animationObserver.value = createNumberAnimationObserver(
  233. '.data-stats',
  234. startStatsAnimation,
  235. {
  236. threshold: 0.3,
  237. rootMargin: '0px 0px -50px 0px'
  238. }
  239. )
  240. }
  241. }
  242. onMounted(() => {
  243. loadCertificates()
  244. // 延迟设置动画观察器,确保DOM已渲染
  245. setTimeout(setupStatsAnimation, 100)
  246. })
  247. onUnmounted(() => {
  248. // 清理观察器
  249. if (animationObserver.value) {
  250. animationObserver.value.disconnect()
  251. }
  252. })
  253. return {
  254. imgHost,
  255. activeTab,
  256. currentSlide,
  257. itemWidth,
  258. itemsPerPage,
  259. loading,
  260. certificateCategories,
  261. currentCertificates,
  262. showNavButtons,
  263. maxSlide,
  264. switchTab,
  265. prevSlide,
  266. nextSlide,
  267. handleImageError,
  268. // 数字动画相关
  269. dataStatsRef,
  270. animatedStats
  271. }
  272. }
  273. }
  274. </script>
  275. <style lang="scss" scoped>
  276. // banner区域
  277. .banner-section {
  278. position: relative;
  279. img {
  280. width: 100%;
  281. }
  282. // 数据统计模块
  283. &.about-us {
  284. .data-stats {
  285. position: absolute;
  286. bottom: 98px;
  287. left: 10%;
  288. right: 10%;
  289. background: rgba(255, 255, 255, 0.5);
  290. backdrop-filter: blur(10px);
  291. padding: 30px 0;
  292. display: flex;
  293. align-items: center;
  294. justify-content: center;
  295. border-radius: 10px;
  296. gap: 60px;
  297. .stats-item {
  298. text-align: center;
  299. color: #0054FF;
  300. .stats-number {
  301. font-size: 48px;
  302. font-weight: 700;
  303. line-height: 1;
  304. margin-bottom: 8px;
  305. }
  306. .stats-label {
  307. font-size: 14px;
  308. font-weight: 400;
  309. color: #333;
  310. white-space: nowrap;
  311. }
  312. }
  313. .stats-divider {
  314. width: 1px;
  315. height: 60px;
  316. background: linear-gradient(to bottom, transparent, #ddd 20%, #ddd 80%, transparent);
  317. }
  318. }
  319. }
  320. }
  321. // 企业资质区域
  322. .qualification-section {
  323. padding: 80px 0;
  324. position: relative;
  325. &::before {
  326. position: absolute;
  327. content: '';
  328. left: 0;
  329. right: 0;
  330. top: -3px;
  331. bottom: -138px;
  332. background: url('@assets/about-bg1.png') no-repeat center center;
  333. ;
  334. background-size: cover;
  335. }
  336. .container {
  337. margin: 0 auto;
  338. padding: 0 20px;
  339. }
  340. // 标题样式
  341. .section-title {
  342. text-align: center;
  343. margin-bottom: 125px;
  344. h2 {
  345. font-size: 36px;
  346. font-weight: 600;
  347. color: #333;
  348. margin-bottom: 10px;
  349. position: relative;
  350. }
  351. .title-en {
  352. font-size: 14px;
  353. color: #999;
  354. letter-spacing: 2px;
  355. margin-top: 15px;
  356. }
  357. }
  358. // 证书切换标签
  359. .certificate-tabs {
  360. display: flex;
  361. justify-content: space-between;
  362. margin-bottom: 50px;
  363. // gap: 20px;
  364. .tab-btn {
  365. padding: 12px 30px;
  366. border: 2px solid #F5F9FD;
  367. background: #F5F9FD;
  368. color: #666;
  369. font-size: 16px;
  370. font-weight: 500;
  371. border-radius: 10px;
  372. cursor: pointer;
  373. transition: all 0.3s ease;
  374. box-sizing: border-box;
  375. width: 440px;
  376. &:hover {
  377. border-color: #2E64AF;
  378. color: #0862C8;
  379. }
  380. &.active {
  381. background: #EAF2FA;
  382. border-color: #2E64AF;
  383. color: #0862C8;
  384. box-shadow: 0 4px 15px rgba(74, 144, 226, 0.3);
  385. }
  386. }
  387. }
  388. // 证书容器
  389. .certificate-container {
  390. position: relative;
  391. overflow: hidden;
  392. padding: 60px; // 为导航按钮留出空间
  393. box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
  394. background-color: rgba(255, 255, 255, 0.9);
  395. border-radius: 10px;
  396. min-height: 400px;
  397. // 加载状态
  398. .loading-state {
  399. display: flex;
  400. flex-direction: column;
  401. align-items: center;
  402. justify-content: center;
  403. height: 300px;
  404. color: #666;
  405. .loading-spinner {
  406. width: 40px;
  407. height: 40px;
  408. border: 3px solid #f3f3f3;
  409. border-top: 3px solid #0862C8;
  410. border-radius: 50%;
  411. animation: spin 1s linear infinite;
  412. margin-bottom: 20px;
  413. }
  414. p {
  415. font-size: 16px;
  416. margin: 0;
  417. }
  418. }
  419. // 空状态
  420. .empty-state {
  421. display: flex;
  422. align-items: center;
  423. justify-content: center;
  424. height: 300px;
  425. color: #999;
  426. font-size: 16px;
  427. p {
  428. margin: 0;
  429. }
  430. }
  431. .certificate-slider {
  432. display: flex;
  433. transition: transform 0.5s ease;
  434. gap: 44px;
  435. }
  436. .certificate-item {
  437. flex: 0 0 256px;
  438. text-align: center;
  439. // background: #fff;
  440. border-radius: 12px;
  441. // padding: 20px;
  442. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  443. transition: all 0.3s ease;
  444. &:hover {
  445. transform: translateY(-5px);
  446. box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
  447. }
  448. img {
  449. width: 100%;
  450. height: 354px;
  451. object-fit: cover;
  452. // border-radius: 8px;
  453. // margin-bottom: 15px;
  454. border: 1px solid #f0f0f0;
  455. }
  456. .certificate-title {
  457. font-size: 14px;
  458. color: #333;
  459. font-weight: 500;
  460. line-height: 1.4;
  461. margin: 0;
  462. }
  463. }
  464. // 导航按钮
  465. .nav-btn {
  466. position: absolute;
  467. top: 50%;
  468. transform: translateY(-50%);
  469. width: 40px;
  470. height: 40px;
  471. background: rgba(255, 255, 255, 0.9);
  472. border: 1px solid #e0e0e0;
  473. border-radius: 50%;
  474. cursor: pointer;
  475. display: flex;
  476. align-items: center;
  477. justify-content: center;
  478. transition: all 0.3s ease;
  479. z-index: 10;
  480. &:hover {
  481. background: #fff;
  482. box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
  483. transform: translateY(-50%) scale(1.1);
  484. }
  485. img {
  486. width: 16px;
  487. height: 16px;
  488. }
  489. &.nav-btn-left {
  490. left: 10px;
  491. }
  492. &.nav-btn-right {
  493. right: 10px;
  494. }
  495. }
  496. }
  497. }
  498. // 响应式设计
  499. @media (max-width: 768px) {
  500. .banner-section.about-us {
  501. .data-stats {
  502. padding: 20px 15px;
  503. gap: 30px;
  504. flex-wrap: wrap;
  505. .stats-item {
  506. .stats-number {
  507. font-size: 36px;
  508. }
  509. .stats-label {
  510. font-size: 12px;
  511. }
  512. }
  513. .stats-divider {
  514. height: 40px;
  515. }
  516. }
  517. }
  518. .qualification-section {
  519. padding: 60px 0;
  520. .section-title {
  521. margin-bottom: 40px;
  522. h2 {
  523. font-size: 28px;
  524. }
  525. }
  526. .certificate-tabs {
  527. flex-wrap: wrap;
  528. gap: 10px;
  529. margin-bottom: 30px;
  530. .tab-btn {
  531. padding: 10px 20px;
  532. font-size: 14px;
  533. }
  534. }
  535. .certificate-container {
  536. padding: 0 50px;
  537. .certificate-item {
  538. flex: 0 0 160px;
  539. padding: 15px;
  540. img {
  541. height: 200px;
  542. }
  543. .certificate-title {
  544. font-size: 12px;
  545. }
  546. }
  547. }
  548. }
  549. }
  550. @media (max-width: 480px) {
  551. .banner-section.about-us {
  552. .data-stats {
  553. padding: 15px 10px;
  554. gap: 20px;
  555. .stats-item {
  556. .stats-number {
  557. font-size: 28px;
  558. }
  559. .stats-label {
  560. font-size: 11px;
  561. }
  562. }
  563. .stats-divider {
  564. height: 30px;
  565. }
  566. }
  567. }
  568. .qualification-section {
  569. .certificate-container {
  570. padding: 0 40px;
  571. .certificate-item {
  572. flex: 0 0 140px;
  573. padding: 12px;
  574. img {
  575. height: 180px;
  576. }
  577. }
  578. }
  579. }
  580. }
  581. // 动画
  582. @keyframes spin {
  583. 0% { transform: rotate(0deg); }
  584. 100% { transform: rotate(360deg); }
  585. }
  586. </style>