Browse Source

all-commit

master
xuhuajiao 2 weeks ago
parent
commit
a6f5a3a7ef
  1. 9
      .env.development
  2. 8
      .env.production
  3. 197
      .eslintrc.js
  4. 23
      .gitignore
  5. 120
      .trae/documents/traffic-flow-screen-plan.md
  6. 11
      babel.config.js
  7. 57
      package.json
  8. BIN
      public/favicon.ico
  9. 19
      public/index.html
  10. 19
      public/static/branches.json
  11. 4
      public/static/config.js
  12. 16
      src/App.vue
  13. 13
      src/api/library.js
  14. BIN
      src/assets/fonts/YouSheBiaoTiHei.ttf
  15. BIN
      src/assets/fonts/ZhenyanGB.ttf
  16. 13
      src/assets/fonts/fonts.css
  17. 68
      src/assets/iconfont/iconfont.css
  18. 1
      src/assets/iconfont/iconfont.js
  19. 100
      src/assets/iconfont/iconfont.json
  20. 45
      src/assets/iconfont/iconfont.svg
  21. BIN
      src/assets/iconfont/iconfont.ttf
  22. BIN
      src/assets/iconfont/iconfont.woff
  23. BIN
      src/assets/iconfont/iconfont.woff2
  24. BIN
      src/assets/images/default-img.png
  25. BIN
      src/assets/images/header-bg.png
  26. BIN
      src/assets/images/header-left.png
  27. BIN
      src/assets/images/header-right.png
  28. BIN
      src/assets/images/notice.gif
  29. BIN
      src/assets/images/num-img.png
  30. BIN
      src/assets/images/star-1.png
  31. BIN
      src/assets/images/star-2.png
  32. BIN
      src/assets/images/star-3.png
  33. BIN
      src/assets/images/title-big.png
  34. BIN
      src/assets/images/title.png
  35. BIN
      src/assets/images/top-line.png
  36. BIN
      src/assets/images/top.png
  37. BIN
      src/assets/logo.png
  38. 116
      src/assets/styles/ball-running-animate.css
  39. 46
      src/assets/styles/font-some.css
  40. 710
      src/assets/styles/index.scss
  41. 93
      src/assets/styles/style.scss
  42. 147
      src/common/flexible.js
  43. 49
      src/main.js
  44. 22
      src/router/index.js
  45. 15
      src/store/index.js
  46. 100
      src/utils/index.js
  47. 39
      src/utils/request.js
  48. 26
      src/utils/resizeMixins.js
  49. 75
      src/views/header/index.vue
  50. 26
      src/views/index.vue
  51. 29
      src/views/trafficFlow/components/DayInFlowCard.vue
  52. 159
      src/views/trafficFlow/components/MonthInFlowChart.vue
  53. 106
      src/views/trafficFlow/components/TotalFlowSummary.vue
  54. 157
      src/views/trafficFlow/components/YearInFlowChart.vue
  55. 118
      src/views/trafficFlow/index.vue
  56. 68
      vue.config.js

9
.env.development

@ -0,0 +1,9 @@
ENV = 'development'
# 接口地址
# 许镇-本地服地址
VUE_APP_BASE_API = 'http://192.168.99.63:8080'
VUE_APP_WS_API = 'ws://192.168.99.63:8081'
# 是否启用 babel-plugin-dynamic-import-node插件
VUE_CLI_BABEL_TRANSPILE_MODULES = true

8
.env.production

@ -0,0 +1,8 @@
ENV = 'production'
# 如果使用 Nginx 代理后端接口,那么此处需要改为 '/',文件查看 Docker 部署篇,Nginx 配置
# 接口地址,注意协议,如果你没有配置 ssl,需要将 https 改为 http
VUE_APP_BASE_API = 'http://172.22.0.23:8080'
# VUE_APP_BASE_API = 'http://127.0.0.1:8080'
# 如果接口是 http 形式, wss 需要改为 ws
VUE_APP_WS_API = 'ws://172.22.0.23:7071'

197
.eslintrc.js

@ -0,0 +1,197 @@
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true,
},
extends: ['plugin:vue/recommended', 'eslint:recommended'],
rules: {
"vue/max-attributes-per-line": [2, {
"singleline": 10,
"multiline": {
"max": 1,
"allowFirstLine": false
}
}],
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline":"off",
"vue/name-property-casing": ["error", "PascalCase"],
"vue/no-v-html": "off",
'accessor-pairs': 2,
'arrow-spacing': [2, {
'before': true,
'after': true
}],
'block-spacing': [2, 'always'],
'brace-style': [2, '1tbs', {
'allowSingleLine': true
}],
'camelcase': [0, {
'properties': 'always'
}],
'comma-dangle': [2, 'never'],
'comma-spacing': [2, {
'before': false,
'after': true
}],
'comma-style': [2, 'last'],
'constructor-super': 2,
'curly': [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
'eqeqeq': ["error", "always", {"null": "ignore"}],
'generator-star-spacing': [2, {
'before': true,
'after': true
}],
'handle-callback-err': [2, '^(err|error)$'],
'indent': [2, 2, {
'SwitchCase': 1
}],
'jsx-quotes': [2, 'prefer-single'],
'key-spacing': [2, {
'beforeColon': false,
'afterColon': true
}],
'keyword-spacing': [2, {
'before': true,
'after': true
}],
'new-cap': [2, {
'newIsCap': true,
'capIsNew': false
}],
'new-parens': 2,
'no-array-constructor': 2,
'no-caller': 2,
'no-console': 'off',
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
'no-control-regex': 0,
'no-delete-var': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-empty-pattern': 2,
'no-eval': 2,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': [2, 'functions'],
'no-fallthrough': 2,
'no-floating-decimal': 2,
'no-func-assign': 2,
'no-implied-eval': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [2, {
'allowLoop': false,
'allowSwitch': false
}],
'no-lone-blocks': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [2, {
'max': 1
}],
'no-native-reassign': 2,
'no-negated-in-lhs': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-new-symbol': 2,
'no-new-wrappers': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-octal-escape': 2,
'no-path-concat': 2,
'no-proto': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-return-assign': [2, 'except-parens'],
'no-self-assign': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-shadow-restricted-names': 2,
'no-spaced-func': 2,
'no-sparse-arrays': 2,
'no-this-before-super': 2,
'no-throw-literal': 2,
'no-trailing-spaces': 2,
'no-undef': 2,
'no-undef-init': 2,
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [2, {
'defaultAssignment': false
}],
'no-unreachable': 2,
'no-unsafe-finally': 2,
'no-unused-vars': [2, {
'vars': 'all',
'args': 'none'
}],
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-useless-escape': 0,
'no-whitespace-before-property': 2,
'no-with': 2,
'one-var': [2, {
'initialized': 'never'
}],
'operator-linebreak': [2, 'after', {
'overrides': {
'?': 'before',
':': 'before'
}
}],
'padded-blocks': [2, 'never'],
'quotes': [2, 'single', {
'avoidEscape': true,
'allowTemplateLiterals': true
}],
'semi': [2, 'never'],
'semi-spacing': [2, {
'before': false,
'after': true
}],
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [2, {
'words': true,
'nonwords': false
}],
'spaced-comment': [2, 'always', {
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
'template-curly-spacing': [2, 'never'],
'use-isnan': 2,
'valid-typeof': 2,
'wrap-iife': [2, 'any'],
'yield-star-spacing': [2, 'both'],
'yoda': [2, 'never'],
'prefer-const': 2,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'object-curly-spacing': [2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never']
}
}

23
.gitignore

@ -0,0 +1,23 @@
.DS_Store
node_modules
dist/
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
package-lock.json

120
.trae/documents/traffic-flow-screen-plan.md

@ -0,0 +1,120 @@
# 客流量大屏改造方案
## Context(背景与目标)
现有项目 `hs-flow‑screen` 是一个深色科技风的"借阅/通知大屏"(三栏布局:总借阅、借阅排行、进馆、阅读之星、通知、新书推荐)。用户希望仿照目标页面 `http://libraryu.seaeverit.com:15000/main?token=...` 做一个**客流量大屏**,目标页面布局为:顶部 Header → "客流量展示"标签 → 三列卡片(当日进馆折线图 / 当月进馆折线图 / 当年进馆数字翻牌)→ 全宽"累计客流量汇总"翻牌器。
经确认的改造口径:
1. **保留深色科技风视觉**(深蓝背景 `#010326`、白色文字、蓝色发光 `#1AC9FF`、`common-title`/`module-content` 蓝色样式),**不**改成目标页的白底橙色,只仿照**布局结构**。
2. **保留现有 Header 组件**(动画线条 + 天气 iframe + 渐变标题),标题用"黄石图书馆"。
3. **删除不再使用的旧组件目录**
4. **数据先用 mock**,预留接口对接位置。
## 关键约束(基于源码确认)
- 适配:rem 方案(`src/common/flexible.js`,1920 设计稿,1rem≈80px,范围 1366–2560),所有尺寸用 rem。
- echarts 4:`this.$echarts` 全局已注册(`main.js`),用 `this.$echarts.init(this.$refs.chart)` + ref,**不要用 `getElementById`**(旧 `components/echart/*` 用 id 会有冲突)。
- `resizeMixins.js`(`src/utils/resizeMixins.js`)提供自动 resize(要求组件 `data.chart` 字段),但其 `removeEventListener('reisze'…)` 拼写错误且不 dispose → 新组件须在 `beforeDestroy` 手动 `this.chart.dispose(); this.chart=null`
- 数字翻牌:`vue-count-to` 的 `CountTo`,用法 `<count-to :start-val="0" :end-val="num" :duration="3200" />`
- 滚动:`vue-seamless-scroll` 已 `Vue.use`,组件名 `<seamless>`
- `.common-title` / `.module-content``.screen-main` 的后代选择器 → 新布局根容器必须仍是 `.screen-main`(叠加 `.traffic-main`)。
- Header 标题 prop:`headerTitle`,`getCurrentTime()` 返回"YYYY年MM月DD日 HH:mm:ss"。
- 端口 8080,`npm run serve` 自动开浏览器。
## 新文件结构
```
src/views/
├── index.vue # 改写:Header + <TrafficFlow/>,删除旧的高度计算逻辑
├── header/index.vue # 保留不动
└── trafficFlow/
├── index.vue # 新建:.screen-main.traffic-main 布局容器
└── components/
├── DayInFlowChart.vue # 当日进馆折线图(按小时)
├── MonthInFlowChart.vue # 当月进馆折线图(按日)
├── YearInFlowCard.vue # 当年进馆数字翻牌(单个大数字)
└── TotalFlowSummary.vue # 累计客流量汇总(8 位翻牌卡片,横向可滚动)
```
## 实施步骤
### 1. 新建 trafficFlow 4 个子组件 + index.vue(先建骨架,不破坏现有)
- `DayInFlowChart.vue` / `MonthInFlowChart.vue`:`<div class="chart-box" ref="chart"/>`,`mixins:[resize]`,`data.chart=null` + mock 数据,`mounted` → `initChart()``setOption()`,`beforeDestroy` dispose。setOption 用蓝色系(线 `#1AC9FF`、轴 `#2C75E0`、分割线 `rgba(44,117,224,0.3)` 虚线、白色文字、`areaStyle` 用 `this.$echarts.graphic.LinearGradient` 蓝色渐变、`smooth:true`)。
- `YearInFlowCard.vue`:`<count-to :end-val="yearNum"/>`,mock `yearNum:1286500`
- `TotalFlowSummary.vue`:`totalNum:98765432` → `computed.digitList``padStart(8,'0').split('')` 固定 8 位,每位一个 `<count-to>`,外裹 `<seamless :data :class-option>` 横向滚动(`direction:2`)。预留 `fetchData()` 注释。
- `trafficFlow/index.vue`:模板见下方。
### 2. 在 `src/assets/styles/index.scss` 末尾追加客流量布局样式(不动现有任何规则)
新增 `// ===== 客流量大屏布局 =====` 段,关键规则:
- `.traffic-main { flex-direction:column; justify-content:flex-start; gap:0.125rem; }`(叠加在 `.screen-main` 上)
- `.traffic-section-title`(顶部"客流量展示"标签,用 `title-big.png`
- `.traffic-row { display:flex; }`、`.traffic-row-3 { flex:1; gap:0.25rem; min-height:0; }`(min-height:0 防 echarts 高度坍缩)
- `.traffic-card { flex:1; display:flex; flex-direction:column; min-width:0; .module-content{flex:1; overflow:hidden; padding:0.125rem 0.2rem;} }`
- `.traffic-summary { height:1.6rem; }`
- `.chart-box { width:100%; height:100%; }`
- `.year-num`(当年大数字,`#1AC9FF` + text-shadow)、`.summary-digits`/`.digit-card`(每位翻牌,`num-img.png` 背景)
### 3. 改写 `src/views/index.vue` 为极简入口
```vue
<template>
<div id="screen-container">
<Header :header-title="headerTitle" />
<TrafficFlow />
</div>
</template>
<script>
import Header from '@/views/header/index.vue'
import TrafficFlow from '@/views/trafficFlow/index.vue'
export default {
name: 'Home',
components: { Header, TrafficFlow },
data() { return { headerTitle: '黄石图书馆' } }
}
</script>
<style lang="scss">@import "~@/assets/styles/index.scss";</style>
```
(删除原 `calcXxxHeight`/`$refs`/resize 监听等所有逻辑)
`trafficFlow/index.vue` 模板骨架:
```vue
<template>
<div class="screen-main traffic-main">
<div class="common-title traffic-section-title">客流量展示</div>
<div class="traffic-row traffic-row-3">
<div class="traffic-card"><div class="common-title">当日进馆</div><div class="module-content"><DayInFlowChart/></div></div>
<div class="traffic-card"><div class="common-title">当月进馆</div><div class="module-content"><MonthInFlowChart/></div></div>
<div class="traffic-card"><div class="common-title">当年进馆</div><div class="module-content year-content"><YearInFlowCard/></div></div>
</div>
<div class="traffic-row traffic-summary"><TotalFlowSummary/></div>
</div>
</template>
```
### 4. 启动验证一轮(`npm run serve`)
确认:Header + "客流量展示" + 三卡片(两折线图初始化、一翻牌)+ 汇总翻牌滚动;控制台无 echarts 报错。
### 5. 删除旧组件目录
- `src/views/accessToLibrary/`、`totalLending/`、`lengingRanking/`、`newBookRecommend/`、`notice/`、`readStar/`、`todayBorrowed/`
### 6. 可选清理
- 删 `src/components/echart/`(删旧组件后无引用)、`src/common/scrollMixins.js`(全项目无引用)。
- 清 `index.scss` 中三栏 `.screen-left/.screen-right/.screen-middle` 及旧组件死 CSS(留着不影响,但建议删三栏那三条以免误导)。
## 接口对接预留(mock 阶段不做)
- `src/api/library.js``FetchInitIntoNum` 已存在,保留不删。后续在 4 个组件的 `fetchData()`(已注释)中对接,字段参考 `historyPeopleNum`(累计)、`dayPeopleNum`/`monthPeopleNum`(当日/当月总数);按小时/按日粒度折线数据需后端补接口。
## 验证
1. `npm run serve``http://localhost:8080`
- Header 显示"黄石图书馆" + 天气 + 每秒跳的时间
- 顶部"客流量展示"标签
- 第一行三列:左/中蓝色平滑折线图(带渐变面积)、右"当年进馆"0→1286500 翻牌动画
- 第二行"累计客流量汇总"8 位翻牌(num-img.png 蓝牌背景)横向滚动
2. F12 控制台无 echarts init/id 重复/resize 报错
3. 拖拽窗口:折线图随 resizeMixins 自动 resize,rem 尺寸随 flexible 重算
4. 改 mock 数字热更新不残留旧 echarts 实例(dispose 生效)
5. `npm run build` 无打包错误
## 风险规避
- echarts 容器高度坍缩:`.traffic-row-3 { min-height:0 }` + `.traffic-card .module-content { flex:1 }` 必须同时有;必要时给 `.chart-box` 兜底 `min-height:2rem`
- `seamless` 横向滚动可能因 8 位内容不够触发:调小 `limitMoveNum:1`,或不要滚动直接静态居中("可滚动"非硬需求)。
- 顺序:先建新→改入口→验证→再删旧,避免白屏。

11
babel.config.js

@ -0,0 +1,11 @@
const plugins = ['@vue/babel-plugin-transform-vue-jsx', '@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-nullish-coalescing-operator']
// 生产环境移除console
if (process.env.NODE_ENV === 'production') {
plugins.push('transform-remove-console')
}
module.exports = {
plugins: plugins,
presets: [
'@vue/cli-plugin-babel/preset'
]
}

57
package.json

@ -0,0 +1,57 @@
{
"name": "intelligence-screen",
"version": "1.0.0",
"description": "人流数据大屏",
"scripts": {
"serve": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve",
"build": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build",
"lint": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service lint"
},
"dependencies": {
"@babel/core": "^7.19.0",
"@babel/preset-env": "^7.19.0",
"@jiaminghi/data-view": "^2.7.3",
"@types/echarts": "^4.4.3",
"axios": "^0.27.2",
"core-js": "^3.6.4",
"echarts": "^4.6.0",
"element-ui": "^2.15.10",
"vue": "^2.6.11",
"vue-awesome": "^4.0.2",
"vue-count-to": "^1.0.13",
"vue-router": "^3.1.5",
"vue-seamless-scroll": "^1.1.23",
"vuex": "^3.1.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.2.0",
"@vue/cli-plugin-eslint": "^4.2.0",
"@vue/cli-service": "^4.2.0",
"babel-eslint": "^10.0.3",
"babel-plugin-transform-remove-console": "^6.9.4",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.1.2",
"sass": "^1.25.0",
"sass-loader": "^8.0.2",
"vue-awesome-swiper": "^3.1.3",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}

BIN
public/favicon.ico

19
public/index.html

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="referrer" content="no-referrer">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script src="<%= BASE_URL %>static/config.js"></script>
<!-- built files will be auto injected -->
</body>
</html>

19
public/static/branches.json

@ -0,0 +1,19 @@
{
"schemaVersion": "1.0",
"description": "图书馆大屏网点配置:网点编号与网点名称映射",
"branches": [
{ "code": "HWXD", "name": "宏伟星都城市书房" },
{ "code": "JTSF", "name": "中窑江滩城市书房" },
{ "code": "QYG", "name": "群艺馆城市书房" },
{ "code": "QSH", "name": "青山湖城市书房" },
{ "code": "HSGQ", "name": "黄石港区城市书房" },
{ "code": "SDGY", "name": "湿地公园城市书房" },
{ "code": "SNSQ", "name": "神牛社区城市书房" },
{ "code": "TSSF", "name": "铁山城市书房" },
{ "code": "WSW", "name": "未苏湾城市书房" },
{ "code": "SMZJ", "name": "市民之家城市书房" },
{ "code": "RTFLY", "name": "儿童福利院城市书房" },
{ "code": "D3、D4", "name": "保利时光绘城市书房" },
{ "code": "D5", "name": "春湖公园城市书房" }
]
}

4
public/static/config.js

@ -0,0 +1,4 @@
window.g = {
AXIOS_TIMEOUT: 10000,
ApiUrl: 'http://192.168.99.63:8080' // 配置服务器地址
}

16
src/App.vue

@ -0,0 +1,16 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<style lang="scss">
body {
margin: 0;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>

13
src/api/library.js

@ -0,0 +1,13 @@
import request from '@/utils/request'
export function FetchInitIntoNumHS(params) {
return request({
url: '/dxhtsg/initIntoNumHS',
method: 'get',
params: params
})
}
export default {
FetchInitIntoNumHS
}

BIN
src/assets/fonts/YouSheBiaoTiHei.ttf

BIN
src/assets/fonts/ZhenyanGB.ttf

13
src/assets/fonts/fonts.css

@ -0,0 +1,13 @@
@font-face {
font-family: "ZhenyanGB";
src: url('ZhenyanGB.ttf');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "YouSheBiaoTiHei";
src: url('YouSheBiaoTiHei.ttf');
font-weight: normal;
font-style: normal;
}

68
src/assets/iconfont/iconfont.css

@ -0,0 +1,68 @@
@font-face {
font-family: "iconfont"; /* Project id 3646564 */
src: url('iconfont.woff2?t=1663379994606') format('woff2'),
url('iconfont.woff?t=1663379994606') format('woff'),
url('iconfont.ttf?t=1663379994606') format('truetype'),
url('iconfont.svg?t=1663379994606#iconfont') format('svg');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-a-no21:before {
content: "\e636";
}
.icon-a-21:before {
content: "\e637";
}
.icon-jianpan:before {
content: "\e634";
}
.icon-guanbi:before {
content: "\e630";
}
.icon-zuo:before {
content: "\e631";
}
.icon-benjiatushu:before {
content: "\e632";
}
.icon-remen:before {
content: "\e633";
}
.icon-you:before {
content: "\e62f";
}
.icon-tongzhi:before {
content: "\e628";
}
.icon-a-no3:before {
content: "\e629";
}
.icon-a-no1:before {
content: "\e62b";
}
.icon-a-3:before {
content: "\e62d";
}
.icon-a-1:before {
content: "\e62e";
}

1
src/assets/iconfont/iconfont.js
File diff suppressed because it is too large
View File

100
src/assets/iconfont/iconfont.json

@ -0,0 +1,100 @@
{
"id": "3646564",
"name": "智慧屏",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "31879702",
"name": "no.2",
"font_class": "a-no21",
"unicode": "e636",
"unicode_decimal": 58934
},
{
"icon_id": "31879708",
"name": "2",
"font_class": "a-21",
"unicode": "e637",
"unicode_decimal": 58935
},
{
"icon_id": "31879044",
"name": "键盘",
"font_class": "jianpan",
"unicode": "e634",
"unicode_decimal": 58932
},
{
"icon_id": "31859389",
"name": "关闭",
"font_class": "guanbi",
"unicode": "e630",
"unicode_decimal": 58928
},
{
"icon_id": "31859390",
"name": "左",
"font_class": "zuo",
"unicode": "e631",
"unicode_decimal": 58929
},
{
"icon_id": "31859393",
"name": "本架图书",
"font_class": "benjiatushu",
"unicode": "e632",
"unicode_decimal": 58930
},
{
"icon_id": "31859394",
"name": "热门",
"font_class": "remen",
"unicode": "e633",
"unicode_decimal": 58931
},
{
"icon_id": "31859388",
"name": "右",
"font_class": "you",
"unicode": "e62f",
"unicode_decimal": 58927
},
{
"icon_id": "31804863",
"name": "通知",
"font_class": "tongzhi",
"unicode": "e628",
"unicode_decimal": 58920
},
{
"icon_id": "31804864",
"name": "no.3",
"font_class": "a-no3",
"unicode": "e629",
"unicode_decimal": 58921
},
{
"icon_id": "31804866",
"name": "no.1",
"font_class": "a-no1",
"unicode": "e62b",
"unicode_decimal": 58923
},
{
"icon_id": "31804868",
"name": "3",
"font_class": "a-3",
"unicode": "e62d",
"unicode_decimal": 58925
},
{
"icon_id": "31804869",
"name": "1",
"font_class": "a-1",
"unicode": "e62e",
"unicode_decimal": 58926
}
]
}

45
src/assets/iconfont/iconfont.svg
File diff suppressed because it is too large
View File

BIN
src/assets/iconfont/iconfont.ttf

BIN
src/assets/iconfont/iconfont.woff

BIN
src/assets/iconfont/iconfont.woff2

BIN
src/assets/images/default-img.png

After

Width: 66  |  Height: 83  |  Size: 4.2 KiB

BIN
src/assets/images/header-bg.png

After

Width: 1268  |  Height: 86  |  Size: 26 KiB

BIN
src/assets/images/header-left.png

After

Width: 496  |  Height: 84  |  Size: 3.8 KiB

BIN
src/assets/images/header-right.png

After

Width: 496  |  Height: 84  |  Size: 3.0 KiB

BIN
src/assets/images/notice.gif

After

Width: 1024  |  Height: 920  |  Size: 129 KiB

BIN
src/assets/images/num-img.png

After

Width: 220  |  Height: 68  |  Size: 6.1 KiB

BIN
src/assets/images/star-1.png

After

Width: 700  |  Height: 40  |  Size: 6.4 KiB

BIN
src/assets/images/star-2.png

After

Width: 700  |  Height: 40  |  Size: 5.5 KiB

BIN
src/assets/images/star-3.png

After

Width: 700  |  Height: 40  |  Size: 6.7 KiB

BIN
src/assets/images/title-big.png

After

Width: 740  |  Height: 38  |  Size: 5.9 KiB

BIN
src/assets/images/title.png

After

Width: 510  |  Height: 38  |  Size: 4.2 KiB

BIN
src/assets/images/top-line.png

After

Width: 634  |  Height: 86  |  Size: 2.7 KiB

BIN
src/assets/images/top.png

After

Width: 1896  |  Height: 91  |  Size: 44 KiB

BIN
src/assets/logo.png

After

Width: 200  |  Height: 200  |  Size: 6.7 KiB

116
src/assets/styles/ball-running-animate.css

@ -0,0 +1,116 @@
.ball-running-dots {
position: absolute;
top: 0.2rem;
}
.left-dots {
left: 136px;
}
.right-dots {
right: 200px;
}
.ball-running-dots > div {
position: relative;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.ball-running-dots {
display: block;
font-size: 0;
color: #316fd9;
}
.ball-running-dots > div {
display: inline-block;
float: none;
background-color: currentColor;
border: 0 solid currentColor;
}
.ball-running-dots {
width: 16px;
height: 16px;
}
.ball-running-dots > div {
position: absolute;
margin-left: 30px;
border-radius: 100%;
}
.left-dots > div {
animation: ball-running-dots-animate 2s linear infinite;
}
.right-dots > div {
animation: ball-running-dots-animate2 2s linear infinite;
}
.ball-running-dots > div:nth-child(1) {
animation-delay: 0s;
}
.ball-running-dots > div:nth-child(2) {
animation-delay: -0.8s;
}
.ball-running-dots > div:nth-child(3) {
animation-delay: -1.2s;
}
.ball-running-dots > div:nth-child(4) {
animation-delay: -1.6s;
}
@keyframes ball-running-dots-animate {
0%{
width: 100%;
height: 100%;
transform: translateY(0) translateX(-500%) scale(-.1);
opacity: 0.5;
}
80% {
transform: translateY(0) translateX(0);
}
85% {
width: 100%;
height: 100%;
transform: translateY(-125%) translateX(0);
}
90% {
width: 200%;
height: 75%;
}
95% {
width: 100%;
height: 100%;
transform: translateY(-100%) translateX(-500%);
}
100% {
width: 100%;
height: 100%;
transform: translateY(0) translateX(-500%);
opacity: 1;
}
}
@keyframes ball-running-dots-animate2 {
0%{
width: 100%;
height: 100%;
transform: translateY(0) translateX(500%) scale(-.1);
opacity: 0.5;
}
80% {
transform: translateY(0) translateX(0);
}
85% {
width: 100%;
height: 100%;
transform: translateY(-125%) translateX(0);
}
90% {
width: 200%;
height: 75%;
}
95% {
width: 100%;
height: 100%;
transform: translateY(-100%) translateX(500%);
}
100% {
width: 100%;
height: 100%;
transform: translateY(0) translateX(500%);
opacity: 1;
}
}

46
src/assets/styles/font-some.css

@ -0,0 +1,46 @@
.chartNum{
display: flex;
justify-content: center;
margin-top: 0.25rem;
}
.box-items {
position: relative;
display: flex;
height: 0.6rem;
text-align: center;
}
/*滚动数字设置*/
.number-item {
display: flex;
justify-content: center;
text-align: center;
width: 0.45rem;
height: 0.6rem;
line-height: 0.6rem;
margin-right: 0.125rem;
border: 1px solid #2C75E0;
}
.number-item span{
position: relative;
display: inline-block;
width: 100%;
height: 100%;
writing-mode: vertical-rl;
text-orientation: upright;
overflow: hidden;
}
.number-item span i{
font-style: normal;
position: absolute;
top: 0.1rem;
left: 50%;
font-size: 0.325rem;
font-weight: bold;
color: #1AC9FF;
transform: translate(-50%,0);
transition: transform 1s ease-in-out;
letter-spacing: 10px;
}
.number-item:last-child {
margin-right: 0;
}

710
src/assets/styles/index.scss

@ -0,0 +1,710 @@
#screen-container {
width: 100%;
height: 100vh;
padding: 0 0.175rem;
font-size: 0.25rem;
line-height: 0.35rem;
color: #fff;
background-color: #010326;
overflow: hidden;
}
// 头部主题标题
.header-container{
position: relative;
width: 100%;
height: 1.125rem;
font-size: 0.3rem;
color: #fff;
&::before,
&::after{
content: "";
position: absolute;
top: 0;
width: 6.2rem;
height: 1.05rem;
}
&::before{
left: 0;
background: url('~@/assets/images/header-left.png') no-repeat top center;
}
&::after{
right: 0;
background: url('~@/assets/images/header-right.png') no-repeat top center;
}
.header-title{
position: relative;
width: 15.85rem;
height: 1.075rem;
margin: 0 auto;
background: url('~@/assets/images/header-bg.png') no-repeat top center;
background-size: cover;
h2{
font-size: 0.6rem;
font-family: "ZhenyanGB";
font-weight: normal;
line-height: 1.125rem;
text-align: center;
letter-spacing: 0.2rem;
background: linear-gradient(180deg, #FFFFFF 0%, #1AC9FF 100%);
background-clip: text;
color: transparent;
}
}
.header-text{
position: absolute;
top: 0.65rem;
}
.header-weather {
left: 0.6rem;
}
.header-date {
right: 0.375rem;
}
}
// 头部动态效果
.line1,
.line2{
position: absolute;
top: 0;
}
.line1{
right: 632px;
transform: rotateY(-180deg);
}
.line2{
left: 632px;
}
.top-line1,
.top-line2{
display: block;
width: 7.925rem;
height: 1.075rem;
animation: topline 4s linear infinite both;
}
.top-line1{
background: url('~@/assets/images/top-line.png') no-repeat;
}
.top-line2{
background: url('~@/assets/images/top-line.png') no-repeat;
}
@keyframes topline {
from {
width: 0;
}
50%,
to {
width: 643px;
}
}
// 主题内部部分
.screen-main{
display: flex;
justify-content: space-between;
height: calc(100% - 1.38rem);
margin: 0.0625rem 0 0.5rem 0;
padding: 0 0.325rem;
overflow: hidden;
.screen-left,
.screen-right{
width: 6.375rem;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.screen-middle{
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
margin: 0 0.5rem;
}
.screen-item{
margin-top: 0.25rem;
}
.common-title{
width: 100%;
height: 0.475rem;
line-height: 0.475rem;
padding-left: 0.525rem;
font-size: 0.325rem;
background: url('~@/assets/images/title.png') no-repeat center center;
background-size: cover;
font-family: 'YouSheBiaoTiHei';
letter-spacing: 0.1rem;
}
.module-content{
border: 1px solid #2C75E0;
box-shadow: inset 0px 0px 38px 1px rgba(44,117,224,0.5);
}
.small-module{
height: 3.75rem;
}
.medium-module{
height: 4.25rem;
}
.big-module{
// height: 6.8125rem;
}
//排行榜-推荐 - list
.lending-ranking{
flex: 1;
.module-content{
padding: 0 0.25rem;
overflow: hidden;
}
}
.book-list-item{
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.1rem 0.25rem;
.book-img{
width: 0.825rem;
height: 1.075rem;
margin-right: 0.35rem;
background: url('~@/assets/images/default-img.png') no-repeat center center;
background-size: contain;
display: flex;
align-items: center;
overflow: hidden;
img{
display: block;
width: 100%;
// height: 100%;
}
}
.book-info{
flex: 1;
margin-right: 0.35rem;
h4{
font-size: 0.25rem;
font-weight: normal;
}
p{
font-size: 0.2rem;
}
}
.ranking-num{
display: flex;
justify-content: space-between;
align-items: center;
.icon{
width: 0.5625rem;
height: 0.5625rem;
margin-right: 0.175rem;
}
}
}
// 进出馆人数
.accessToLib-container{
display: flex;
justify-content: space-between;
text-align: center;
align-items: center;
margin: 0.375rem 0 .25rem 0;
.accessToLib-item{
width: 2.75rem;
.panel-num{
display: block;
height: 0.85rem;
margin-top: 0.125rem;
line-height: 0.85rem;
font-size: 0.325rem;
font-weight: bold;
text-align: center;
color: #1AC9FF;
background: url('~@/assets/images/num-img.png') no-repeat center center;
background-size: cover;
}
}
}
// 视频
.video-box{
width: 100%;
height: 5.15rem;
}
// 阅读之星
.read-star{
flex: 1;
.common-title{
background: url('~@/assets/images/title-big.png') no-repeat center center;
background-size: cover;
}
.module-content{
padding: .25rem;
overflow: hidden;
.readstart-item{
display: flex;
justify-content: space-between;
align-items: center;
height: 0.5rem;
padding: 0 0.25rem;
font-size: 0.2rem;
margin-bottom: 0.125rem;
background: linear-gradient(90deg, #0C2157 0%, rgba(44,117,224,0) 51%, #0C2157 100%);
.icon-star-1{
width: 0.35rem;
height: 0.3rem;
margin-left: -0.05rem;
margin-right: 0.2rem;
}
.icon-star-2{
width: 0.375rem;
height: 0.3rem;
margin-left: -0.0625rem;
margin-right: 0.2rem;
}
.icon-star-3{
width: 0.275rem;
height: 0.3rem;
margin-right: 0.25rem;
}
.star-num{
display: block;
width: 0.25rem;
height: 0.25rem;
margin-right: 0.25rem;
font-size: 0.2rem;
line-height: 0.25rem;
text-align: center;
border-radius: 50%;
color: #1AC9FF;
border: 1px solid #1AC9FF;
}
.star-info{
flex: 1;
margin-right: 0.25rem;
}
&.star1-bg{
background: url('~@/assets/images/star-1.png') no-repeat left top;
background-size: contain;
}
&.star2-bg{
background: url('~@/assets/images/star-2.png') no-repeat left top;
background-size: contain;
}
&.star3-bg{
background: url('~@/assets/images/star-3.png') no-repeat left top;
background-size: contain;
}
}
}
}
// 通知公告
// .notice{
// .module-content{
// display: flex;
// justify-content: space-between;
// width: 100%;
// padding: 0.25rem 0.375rem 0.5rem 0.375rem;
// .notice-icon-gif{
// display: block;
// width: 2.375rem;
// height: 2.875rem;
// background: url('~@/assets/images/notice.gif') no-repeat left top;
// background-size: contain;
// margin-right: 0.15rem;
// }
// .seamless-warp{
// width: calc(100vw);
// height: 3.1rem;
// overflow: hidden;
// .notice-txt {
// animation: myMove 20s linear infinite;
// animation-fill-mode: forwards;
// }
// @keyframes myMove {
// 0% {
// transform: translateY(2rem);
// }
// 100% {
// transform: translateY(-6rem);
// }
// }
// }
// }
// }
// 通知公告
.notice{
.module-content{
display: flex;
justify-content: space-between;
width: 100%;
padding: 0.25rem 0.375rem 0.5rem 0.375rem;
.notice-icon-gif{
display: block;
width: 2.375rem;
height: 2.875rem;
background: url('~@/assets/images/notice.gif') no-repeat left top;
background-size: contain;
margin-right: 0.15rem;
}
.seamless-warp{
width: calc(100vw);
height: 3.1rem;
overflow: hidden;
}
}
}
}
// ===== 客流量大屏布局 =====
// 单栏新布局叠加在 .screen-main 基础上不改动原有三栏规则
.traffic-main{
flex-direction: column;
justify-content: flex-start;
gap: 0.3rem;
padding: 0.25rem;
}
// 顶部分类标签
.traffic-section-title{
height: 0.55rem;
line-height: 0.55rem;
font-size: 0.375rem;
background: url('~@/assets/images/title-big.png') no-repeat center center;
background-size: cover;
}
// 行容器
.traffic-row{
display: flex;
}
.traffic-row-3{
flex: 1;
gap: 0.25rem;
min-height: 0;
}
// 卡片参考 HTML card 样式角标 + 顶部渐变线
.traffic-card{
position: relative;
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
background: rgba(0, 18, 52, 0.45);
border: 1px solid rgba(26, 201, 255, 0.22);
border-radius: 0.2rem;
box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.06);
overflow: hidden;
&::before{
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, #1AC9FF, transparent);
opacity: 0.6;
z-index: 2;
}
.card-corner{
position: absolute;
width: 0.175rem;
height: 0.175rem;
border: 2px solid #1AC9FF;
opacity: 0.8;
z-index: 2;
&.tl{ top: -1px; left: -1px; border-right: none; border-bottom: none; }
&.tr{ top: -1px; right: -1px; border-left: none; border-bottom: none; }
&.bl{ bottom: -1px; left: -1px; border-right: none; border-top: none; }
&.br{ bottom: -1px; right: -1px; border-left: none; border-top: none; }
}
.card-header{
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.2rem 0.275rem 0;
flex-shrink: 0;
}
.card-label{
display: flex;
align-items: center;
gap: 0.15rem;
font-size: 0.3rem;
font-weight: 600;
color: #f1f5f9;
&::before{
content: "";
width: 0.075rem;
height: 0.3rem;
background: #1AC9FF;
border-radius: 3px;
box-shadow: 0 0 0.1rem #1AC9FF;
}
}
.card-date{
font-size: 0.225rem;
color: #64748b;
font-variant-numeric: tabular-nums;
}
.module-content{
flex: 1;
overflow: hidden;
padding: 0.15rem 0.275rem 0.275rem;
border: none;
box-shadow: none;
}
.year-content{
display: flex;
align-items: center;
justify-content: center;
}
}
// 折线图容器
.chart-box{
width: 100%;
height: 100%;
min-height: 2rem;
}
// 当日进馆 hero 卡片特殊背景 + 发光边框 + 数字脉冲仿参考页
.card-hero{
background:
radial-gradient(130% 110% at 50% 0%, rgba(26, 201, 255, 0.22) 0%, rgba(26, 201, 255, 0.05) 42%, transparent 72%),
linear-gradient(160deg, #042a52 0%, #021a36 58%, #020f24 100%);
border: 1px solid rgba(26, 201, 255, 0.55);
box-shadow:
0 0 0.3rem rgba(26, 201, 255, 0.28),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
&::before{
height: 3px;
opacity: 1;
background: linear-gradient(90deg, transparent, #1AC9FF, #7dd3fc, #1AC9FF, transparent);
}
.card-corner{
border-color: #1AC9FF;
opacity: 1;
width: 0.2rem;
height: 0.2rem;
}
.card-label::before{
box-shadow: 0 0 0.14rem #1AC9FF;
}
.stat-number{
text-shadow: 0 0 0.55rem rgba(26, 201, 255, 0.65);
animation: heroPulse 2.6s ease-in-out infinite;
}
}
@keyframes heroPulse{
0%, 100% { text-shadow: 0 0 0.42rem rgba(26, 201, 255, 0.42); }
50% { text-shadow: 0 0 0.62rem rgba(26, 201, 255, 0.82); }
}
// 当日进馆 - 数字卡片
.stat-card{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
text-align: center;
.live-row{
margin-bottom: 0.225rem;
display: flex;
align-items: center;
.live-text{ font-size: 0.275rem; color: #94a3b8; }
}
.stat-number{
font-size: 1.5rem;
font-weight: 600;
line-height: 1;
font-variant-numeric: tabular-nums;
background: linear-gradient(180deg, #fff 0%, #7dd3fc 60%, #1AC9FF 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 0.5rem rgba(26, 201, 255, 0.25);
margin-bottom: 0.15rem;
}
.stat-unit{
font-size: 0.3rem;
color: #94a3b8;
font-weight: 500;
}
}
.live-dot{
display: inline-block;
width: 0.15rem;
height: 0.15rem;
background: #34d399;
border-radius: 50%;
box-shadow: 0 0 0.125rem #34d399;
margin-right: 0.1rem;
animation: livePulse 1.4s infinite;
}
@keyframes livePulse{
0%, 100%{ opacity: 1; transform: scale(1); }
50%{ opacity: 0.6; transform: scale(1.3); }
}
// 汇总行
.traffic-summary{
flex: 1.1;
min-height: 0;
.traffic-summary-inner{
position: relative;
flex: 1;
display: flex;
flex-direction: column;
width: 100%;
background: rgba(0, 18, 52, 0.45);
border: 1px solid rgba(26, 201, 255, 0.22);
border-radius: 0.2rem;
box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.06);
overflow: hidden;
&::before{
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, #1AC9FF, transparent);
opacity: 0.6;
z-index: 2;
}
.card-corner{
position: absolute;
width: 0.175rem;
height: 0.175rem;
border: 2px solid #1AC9FF;
opacity: 0.8;
z-index: 2;
&.tl{ top: -1px; left: -1px; border-right: none; border-bottom: none; }
&.tr{ top: -1px; right: -1px; border-left: none; border-bottom: none; }
&.bl{ bottom: -1px; left: -1px; border-right: none; border-top: none; }
&.br{ bottom: -1px; right: -1px; border-left: none; border-top: none; }
}
}
.module-content{
flex: 1;
display: flex;
align-items: center;
overflow: hidden;
border: none;
box-shadow: none;
padding: 0;
}
}
// 累计汇总 - 三栏
.cumulative-card{
display: grid;
grid-template-columns: 4.25rem 1fr 3.25rem;
width: 100%;
height: 100%;
padding: 0.35rem 0.45rem;
gap: 0.4rem;
align-items: center;
}
.cumulative-left{
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.2rem;
// padding-right: 0.4rem;
border-right: 1px solid rgba(26, 201, 255, 0.22);
.cumulative-title{ font-size: 0.5rem; font-weight: 700; color: #f1f5f9; }
.cumulative-subtitle{ font-size: 0.24rem; color: #94a3b8; line-height: 1.5; }
}
.mini-bars{
display: flex;
align-items: flex-end;
gap: 0.2rem;
margin-top: 0.125rem;
.mini-item{ display: flex; flex-direction: column; align-items: center; gap: 0.1rem; }
.mini-col{ height: 1.3rem; display: flex; align-items: flex-end; }
.mini-bar{
width: 0.375rem;
border-radius: 4px 4px 0 0;
background: linear-gradient(180deg, #1AC9FF 0%, rgba(26, 201, 255, 0.15) 100%);
box-shadow: 0 0 0.125rem rgba(26, 201, 255, 0.25);
}
.mini-label{ font-size: 0.15rem; color: #64748b; }
}
.cumulative-right{
display: flex;
align-items: center;
justify-content: center;
gap: 0.2rem;
position: relative;
&::before{
content: "";
position: absolute;
width: 6.5rem;
height: 3.25rem;
background: radial-gradient(ellipse at center, rgba(26, 201, 255, 0.12) 0%, transparent 70%);
pointer-events: none;
}
.digit-box{
width: 1.5rem;
height: 2.05rem;
background: linear-gradient(180deg, rgba(26, 201, 255, 0.22) 0%, rgba(26, 201, 255, 0.06) 100%);
border: 1px solid rgba(26, 201, 255, 0.22);
border-radius: 0.125rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.35rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
color: #f1f5f9;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 4px 0.25rem rgba(0, 0, 0, 0.25);
text-shadow: 0 0 0.25rem rgba(26, 201, 255, 0.35);
position: relative;
overflow: hidden;
&::after{
content: "";
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: rgba(0, 0, 0, 0.25);
z-index: 2;
}
// 翻牌滚动列表
.digit-roller{
display: flex;
flex-direction: column;
transition: transform 1.4s cubic-bezier(0.22, 1, 0.36, 1);
.digit-cell{
display: flex;
align-items: center;
justify-content: center;
height: 2.05rem;
flex-shrink: 0;
}
}
}
}
.cumulative-illustration{
display: flex;
align-items: center;
justify-content: center;
padding-left: 0.25rem;
svg{
width: 3rem;
height: 3rem;
filter: drop-shadow(0 0 0.3rem rgba(26, 201, 255, 0.3));
}
}

93
src/assets/styles/style.scss

@ -0,0 +1,93 @@
@import '~@/assets/iconfont/iconfont.css';
// 全局样式
* {
margin: 0;
padding: 0;
list-style-type: none;
box-sizing: border-box;
outline: none;
}
html {
margin: 0;
padding: 0;
}
body {
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
background-color: #f1f1f1;
margin: 0;
padding: 0;
}
a {
color: #343440;
text-decoration: none;
}
ul{
margin: 0;
padding: 0;
li{
list-style: none;
}
}
img[src=""],img:not([src]){
opacity: 0;
border:none;
visibility: hidden;
max-width: none;
}
.clearfix {
&::after {
content: "";
display: table;
height: 0;
line-height: 0;
visibility: hidden;
clear: both;
}
}
pre{
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
word-wrap: break-word;
white-space: pre-wrap;
}
/**滚动条的宽度*/
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
//滚动条的滑块
::-webkit-scrollbar-thumb {
background-color: #13439E;
border-radius: 3px;
}
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
//浮动
.float-r {
float: right;
}
//文章一行显示多余省略号显示
.title-item {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
text-overflow: ellipsis;
}

147
src/common/flexible.js

@ -0,0 +1,147 @@
(function(win, lib) {
var doc = win.document
var docEl = doc.documentElement
var metaEl = doc.querySelector('meta[name="viewport"]')
var flexibleEl = doc.querySelector('meta[name="flexible"]')
var dpr = 0
var scale = 0
var tid
var flexible = lib.flexible || (lib.flexible = {})
if (metaEl) {
console.warn('将根据已有的meta标签来设置缩放比例')
var match = metaEl
.getAttribute('content')
// eslint-disable-next-line no-useless-escape
.match(/initial\-scale=([\d\.]+)/)
if (match) {
scale = parseFloat(match[1])
dpr = parseInt(1 / scale)
}
} else if (flexibleEl) {
var content = flexibleEl.getAttribute('content')
if (content) {
// eslint-disable-next-line no-useless-escape
var initialDpr = content.match(/initial\-dpr=([\d\.]+)/)
// eslint-disable-next-line no-useless-escape
var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/)
if (initialDpr) {
dpr = parseFloat(initialDpr[1])
scale = parseFloat((1 / dpr).toFixed(2))
}
if (maximumDpr) {
dpr = parseFloat(maximumDpr[1])
scale = parseFloat((1 / dpr).toFixed(2))
}
}
}
if (!dpr && !scale) {
// eslint-disable-next-line no-unused-vars
var isAndroid = win.navigator.appVersion.match(/android/gi)
var isIPhone = win.navigator.appVersion.match(/iphone/gi)
var devicePixelRatio = win.devicePixelRatio
if (isIPhone) {
// iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)) {
dpr = 2
} else {
dpr = 1
}
} else {
// 其他设备下,仍旧使用1倍的方案
dpr = 1
}
scale = 1 / dpr
}
docEl.setAttribute('data-dpr', dpr)
if (!metaEl) {
metaEl = doc.createElement('meta')
metaEl.setAttribute('name', 'viewport')
metaEl.setAttribute(
'content',
'initial-scale=' +
scale +
', maximum-scale=' +
scale +
', minimum-scale=' +
scale +
', user-scalable=no'
)
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl)
} else {
var wrap = doc.createElement('div')
wrap.appendChild(metaEl)
doc.write(wrap.innerHTML)
}
}
function refreshRem() {
var width = docEl.getBoundingClientRect().width
// 最小1366px,最大适配2560px
if (width / dpr < 1366) {
width = 1366 * dpr
} else if (width / dpr > 2560) {
width = 2560 * dpr
}
// 设置成24等份,设计稿时1920px的,这样1rem就是80px
var rem = width / 24
docEl.style.fontSize = rem + 'px'
flexible.rem = win.rem = rem
}
win.addEventListener(
'resize',
function() {
clearTimeout(tid)
tid = setTimeout(refreshRem, 300)
},
false
)
win.addEventListener(
'pageshow',
function(e) {
if (e.persisted) {
clearTimeout(tid)
tid = setTimeout(refreshRem, 300)
}
},
false
)
if (doc.readyState === 'complete') {
doc.body.style.fontSize = 12 * dpr + 'px'
} else {
doc.addEventListener(
'DOMContentLoaded',
// eslint-disable-next-line no-unused-vars
function(e) {
doc.body.style.fontSize = 12 * dpr + 'px'
},
false
)
}
refreshRem()
flexible.dpr = win.dpr = dpr
flexible.refreshRem = refreshRem
flexible.rem2px = function(d) {
var val = parseFloat(d) * this.rem
if (typeof d === 'string' && d.match(/rem$/)) {
val += 'px'
}
return val
}
flexible.px2rem = function(d) {
var val = parseFloat(d) / this.rem
if (typeof d === 'string' && d.match(/px$/)) {
val += 'rem'
}
return val
}
})(window, window['lib'] || (window['lib'] = {}))

49
src/main.js

@ -0,0 +1,49 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
// 适配flex
import '@/common/flexible.js'
// 引入全局css
import './assets/styles/style.scss'
import './assets/iconfont/iconfont.js'
import './assets/fonts/fonts.css'
// 按需引入然后注册在vue原型上
import { Message } from 'element-ui'
Vue.prototype.$message = Message
// 全局注册过滤 - 时间
import { parseTime, getFormattedDate } from '@/utils/index.js'
Vue.filter('parseTime', function(time, cFormat) {
return parseTime(time, cFormat)
})
Vue.prototype.getFormattedDate = getFormattedDate
// 馆代码 1201为东西湖馆代码 / 本地测试用FTZN
// Vue.prototype.libcode = 'FTZN'
Vue.prototype.libcode = '1201'
import axios from 'axios'
Vue.prototype.$axios = axios
// 引入echart
import echarts from 'echarts'
Vue.prototype.$echarts = echarts
import scroll from 'vue-seamless-scroll'
Vue.use(scroll)
Vue.config.productionTip = false
Vue.component(Message.name, Message)
new Vue({
router,
store,
render: (h) => h(App)
}).$mount('#app')

22
src/router/index.js

@ -0,0 +1,22 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'index',
component: () => import('../views/index.vue'),
meta: {
title: '人流数据大屏'
}
}
]
const router = new VueRouter({
mode: 'hash',
routes
})
export default router

15
src/store/index.js

@ -0,0 +1,15 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})

100
src/utils/index.js

@ -0,0 +1,100 @@
export function debounce(func, wait, immediate) {
let timeout, args, context, timestamp, result
const later = function() {
// 据上一次触发时间间隔
const last = +new Date() - timestamp
// 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last)
} else {
timeout = null
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
if (!immediate) {
result = func.apply(context, args)
if (!timeout) context = args = null
}
}
}
return function(...args) {
context = this
timestamp = +new Date()
const callNow = immediate && !timeout
// 如果延时不存在,重新设定延时
if (!timeout) timeout = setTimeout(later, wait)
if (callNow) {
result = func.apply(context, args)
context = args = null
}
return result
}
}
/**
* Parse the time to string
* @param {(Object|string|number)} time
* @param {string} cFormat
* @returns {string}
*/
export function parseTime(time, cFormat) {
if (arguments.length === 0) {
return null
}
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'undefined' || time === null || time === 'null') {
return ''
} else if (typeof time === 'object') {
date = time
} else {
if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
time = parseInt(time)
}
if ((typeof time === 'number') && (time.toString().length === 10)) {
time = time * 1000
}
date = new Date(time)
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
let value = formatObj[key]
// Note: getDay() returns 0 on Sunday
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
if (result.length > 0 && value < 10) {
value = '0' + value
}
return value || 0
})
return time_str
}
// 获取当前日期时间
export function getCurrentTime() {
const yy = new Date().getFullYear()
const mm = new Date().getMonth() + 1
const dd = new Date().getDate()
const hh = new Date().getHours()
const mf = new Date().getMinutes() < 10 ? '0' + new Date().getMinutes() : new Date().getMinutes()
const ss = new Date().getSeconds() < 10 ? '0' + new Date().getSeconds() : new Date().getSeconds()
const time = yy + '年' + mm + '月' + dd + '日 ' + hh + ':' + mf + ':' + ss
return time
}
export function getFormattedDate(date, yearOffset = 0) {
date.setFullYear(date.getFullYear() + yearOffset)
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
return `${year}-${month}-${day}`
}

39
src/utils/request.js

@ -0,0 +1,39 @@
import axios from 'axios'
// import { Message } from 'element-ui'
// 创建axios实例
// 生产环境 baseURL 走 public/static/config.js 的 window.g.ApiUrl,部署后改 config 即可切换后端,无需重新打包
const service = axios.create({
baseURL: process.env.NODE_ENV === 'production' ? ((window.g && window.g.ApiUrl) || '') : '/',
timeout: (window.g && window.g.AXIOS_TIMEOUT) || 1000 * 30, // 请求超时时间
headers: {
'Content-Type': 'application/json'
}
})
// request拦截器
service.interceptors.request.use(
config => {
// if (getToken()) {
// config.headers['Authorization'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
// }
const token = '' // 登录后生成用于识别用户身份,项目不需要直接去掉
config.headers['Authorization'] = token || ''
return config
},
error => {
console.error('error: ', error)
Promise.reject(error)
}
)
// response 拦截器
service.interceptors.response.use(
response => {
return response.data
},
error => {
return Promise.reject(error)
}
)
export default service

26
src/utils/resizeMixins.js

@ -0,0 +1,26 @@
// 混入代码 resize-mixins.js
import { debounce } from '@/utils/index'
const resizeChartMethod = '$__resizeChartMethod'
export default {
data() {
// 在组件内部将图表init的引用映射到chart属性上
return {
chart: null
}
},
created() {
window.addEventListener('resize', this[resizeChartMethod], false)
},
beforeDestroy() {
window.removeEventListener('reisze', this[resizeChartMethod])
},
methods: {
// 通过lodash的防抖函数来控制resize的频率
[resizeChartMethod]: debounce(function() {
if (this.chart) {
this.chart.resize()
}
}, 100)
}
}

75
src/views/header/index.vue

@ -0,0 +1,75 @@
<template>
<div class="header-container">
<div class="header-title">
<div class="line1">
<span class="top-line1" />
</div>
<div class="ball-running-dots left-dots">
<div />
<div />
<div />
<div />
</div>
<h2>{{ headerName }}</h2>
<div class="ball-running-dots right-dots">
<div />
<div />
<div />
<div />
</div>
<div class="line2">
<span class="top-line2" />
</div>
</div>
<!-- 天气api: https://www.tianqi.com/plugin -->
<!-- <div class="header-text header-weather">
<iframe id="weather" width="400" scrolling="no" height="30" frameborder="0" allowtransparency="true" src="https://i.tianqi.com?c=code&id=34&color=%23FFFFFF&icon=1&py=wuhan&site=18" />
</div> -->
<div class="header-text header-date">{{ nowDate }}</div>
</div>
</template>
<script>
import { getCurrentTime } from '@/utils/index'
export default {
name: 'Header',
data() {
return {
nowDate: '',
branches: []
}
},
computed: {
// URL query doorCodes branches.json
headerName() {
const code = (this.$route.query && this.$route.query.doorCodes) || ''
const hit = this.branches.find((b) => b.code === code)
return hit ? hit.name : ''
}
},
created() {
this.timer = setInterval(() => {
this.nowDate = getCurrentTime()
}, 1000)
fetch(`${process.env.BASE_URL}static/branches.json`)
.then((r) => r.json())
.then((res) => {
this.branches = res.branches || []
})
.catch((err) => {
console.error('加载 branches.json 失败', err)
})
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
}
}
</script>
<style lang="scss">
@import "~@/assets/styles/index.scss";
@import "~@/assets/styles/ball-running-animate.css";
</style>

26
src/views/index.vue

@ -0,0 +1,26 @@
<template>
<div id="screen-container">
<Header />
<TrafficFlow />
</div>
</template>
<script>
import Header from '@/views/header/index.vue'
import TrafficFlow from '@/views/trafficFlow/index.vue'
export default {
name: 'Home',
components: {
Header,
TrafficFlow
},
data() {
return {
}
}
}
</script>
<style lang="scss">
@import "~@/assets/styles/index.scss";
</style>

29
src/views/trafficFlow/components/DayInFlowCard.vue

@ -0,0 +1,29 @@
<template>
<div class="stat-card">
<div class="live-row">
<span class="live-dot" />
<span class="live-text">实时客流</span>
</div>
<count-to class="stat-number" :start-val="0" :end-val="todayTotal" :duration="3200" separator="," />
<div class="stat-unit">人次</div>
</div>
</template>
<script>
import CountTo from 'vue-count-to'
export default {
name: 'DayInFlowCard',
components: { CountTo },
props: {
todayTotal: {
type: Number,
default: 0
}
}
}
</script>
<style lang="scss">
@import "~@/assets/styles/index.scss";
</style>

159
src/views/trafficFlow/components/MonthInFlowChart.vue

@ -0,0 +1,159 @@
<template>
<div ref="chart" class="chart-box" />
</template>
<script>
import { debounce } from '@/utils/index'
export default {
name: 'MonthInFlowChart',
props: {
monthData: {
type: Array,
default: () => []
}
},
data() {
return {
chart: null,
ro: null
}
},
computed: {
xAxis() {
return this.monthData.map((i) => i.day)
},
series() {
return this.monthData.map((i) => i.count || 0)
}
},
watch: {
monthData() {
this.setOption()
}
},
mounted() {
this.$nextTick(() => this.initChart())
},
beforeDestroy() {
this.stopTipLoop()
if (this.ro) {
this.ro.disconnect()
this.ro = null
}
if (this.chart) {
this.chart.dispose()
this.chart = null
}
},
methods: {
initChart() {
this.chart = this.$echarts.init(this.$refs.chart)
this.setOption()
// ResizeObserver window.resize
const doResize = debounce(() => {
if (this.chart) this.chart.resize()
}, 100)
this.ro = new ResizeObserver(() => doResize())
this.ro.observe(this.$refs.chart)
},
setOption() {
if (!this.chart) return
//
const idx = Math.max(0, this.series.length - 1)
const lastVal = this.series[idx]
this.chart.setOption({
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(11, 26, 48, 0.92)',
borderColor: '#FACC15',
textStyle: { color: '#e2e8f0', fontSize: 14 },
axisPointer: { type: 'line', lineStyle: { color: '#FACC15' }}
},
grid: { left: 40, right: 20, top: 50, bottom: 30, containLabel: false },
xAxis: {
type: 'category',
data: this.xAxis,
boundaryGap: false,
axisLine: { lineStyle: { color: 'rgba(250,204,21,0.4)' }},
axisTick: { show: false },
axisLabel: { color: 'rgba(255,255,255,0.7)', fontSize: 12, interval: 'auto' },
splitLine: { show: false }
},
yAxis: {
type: 'value',
min: 0,
max: (v) => v.max === 0 ? 100 : Math.ceil(v.max * 1.25),
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { color: 'rgba(255,255,255,0.6)', fontSize: 12 },
splitLine: { lineStyle: { color: 'rgba(250,204,21,0.12)', type: 'dashed' }}
},
series: [{
name: '进馆人次',
type: 'line',
smooth: true,
symbol: 'none',
showSymbol: false,
data: this.series,
lineStyle: { color: '#FACC15', width: 3, shadowColor: '#FACC15', shadowBlur: 12 },
itemStyle: { color: '#FACC15' },
areaStyle: {
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(250,204,21,0.45)' },
{ offset: 1, color: 'rgba(250,204,21,0.02)' }
])
},
// + 仿 markPoint
markPoint: {
symbol: 'circle',
symbolSize: 12,
label: {
position: 'top',
distance: 12,
formatter: () => String(lastVal),
fontSize: 26,
fontWeight: 800,
color: '#fff',
textShadowColor: 'rgba(250,204,21,0.8)',
textShadowBlur: 10
},
itemStyle: {
color: '#FACC15',
shadowColor: 'rgba(250,204,21,1)',
shadowBlur: 12
},
data: [{ coord: [idx, lastVal] }]
}
}]
})
this.startTipLoop()
},
// tooltip
startTipLoop() {
this.stopTipLoop()
if (!this.series.length) return
this.tipIndex = -1
this.tipTimer = setInterval(() => {
if (!this.chart || !this.series.length) return
this.tipIndex = (this.tipIndex + 1) % this.series.length
this.chart.dispatchAction({
type: 'showTip',
seriesIndex: 0,
dataIndex: this.tipIndex
})
}, 2500)
},
stopTipLoop() {
if (this.tipTimer) {
clearInterval(this.tipTimer)
this.tipTimer = null
}
}
}
}
</script>
<style lang="scss">
@import "~@/assets/styles/index.scss";
</style>

106
src/views/trafficFlow/components/TotalFlowSummary.vue

@ -0,0 +1,106 @@
<template>
<div class="traffic-summary-inner">
<span class="card-corner tl" />
<span class="card-corner tr" />
<span class="card-corner bl" />
<span class="card-corner br" />
<div class="module-content">
<div class="cumulative-card">
<!-- 统计标题 + 副标题 + 近6个月柱状 -->
<div class="cumulative-left">
<div class="cumulative-title">统计</div>
<div class="cumulative-subtitle">累计客流量汇总</div>
<div class="mini-bars">
<div v-for="(h, i) in miniBars" :key="i" class="mini-item">
<div class="mini-col">
<div class="mini-bar" :style="{ height: h + '%' }" />
</div>
<div class="mini-label">{{ i + 5 }}</div>
</div>
</div>
</div>
<!-- 数字翻牌 -->
<div class="cumulative-right">
<span v-for="(d, i) in digitList" :key="i" class="digit-box">
<count-to :start-val="0" :end-val="Number(d)" :duration="2000" />
</span>
</div>
<!-- 书本插画 -->
<div class="cumulative-illustration" v-html="svgIcon" />
</div>
</div>
</div>
</template>
<script>
import CountTo from 'vue-count-to'
const SVG_ICON = `<svg viewBox="0 0 240 240" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="illuGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="rgba(26,201,255,0.40)"/>
<stop offset="55%" stop-color="rgba(26,201,255,0.10)"/>
<stop offset="100%" stop-color="rgba(26,201,255,0)"/>
</radialGradient>
<linearGradient id="illuBook" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#7dd3fc"/>
<stop offset="100%" stop-color="#0ea5e9"/>
</linearGradient>
<linearGradient id="illuPage" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f0fbff"/>
<stop offset="100%" stop-color="#bae6fd"/>
</linearGradient>
<linearGradient id="illuRibbon" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#38bdf8"/>
<stop offset="100%" stop-color="#0369a1"/>
</linearGradient>
</defs>
<circle cx="120" cy="120" r="112" fill="url(#illuGlow)"/>
<circle cx="120" cy="120" r="98" stroke="rgba(26,201,255,0.12)" stroke-width="1.5"/>
<circle cx="120" cy="120" r="86" stroke="rgba(26,201,255,0.20)" stroke-width="1.5" stroke-dasharray="2 8"/>
<path d="M120,172 C94,159 60,157 34,166 L34,176 C60,167 94,169 120,182 C146,169 180,167 206,176 L206,166 C180,157 146,159 120,172 Z" fill="url(#illuBook)"/>
<path d="M120,166 C95,154 62,152 36,160 L36,118 C62,110 95,112 120,126 Z" fill="url(#illuPage)" stroke="#bae6fd" stroke-width="1.5"/>
<path d="M120,166 C145,154 178,152 204,160 L204,118 C178,110 145,112 120,126 Z" fill="url(#illuPage)" stroke="#bae6fd" stroke-width="1.5"/>
<path d="M52,134 C76,128 100,130 116,138" stroke="#38bdf8" stroke-width="1.6" stroke-linecap="round" opacity="0.75"/>
<path d="M52,144 C76,138 100,140 116,148" stroke="#38bdf8" stroke-width="1.6" stroke-linecap="round" opacity="0.5"/>
<path d="M52,154 C76,148 100,150 116,158" stroke="#38bdf8" stroke-width="1.6" stroke-linecap="round" opacity="0.35"/>
<path d="M124,138 C140,130 164,128 188,134" stroke="#38bdf8" stroke-width="1.6" stroke-linecap="round" opacity="0.75"/>
<path d="M124,148 C140,140 164,138 188,144" stroke="#38bdf8" stroke-width="1.6" stroke-linecap="round" opacity="0.5"/>
<path d="M124,158 C140,150 164,148 188,154" stroke="#38bdf8" stroke-width="1.6" stroke-linecap="round" opacity="0.35"/>
<path d="M120,126 L120,172" stroke="#0284c7" stroke-width="2.5" stroke-linecap="round"/>
<path d="M152,114 L152,150 L158,143 L164,150 L164,114 Z" fill="url(#illuRibbon)"/>
<circle cx="202" cy="66" r="3" fill="#bae6fd"/>
<circle cx="40" cy="76" r="2.5" fill="#bae6fd"/>
<circle cx="212" cy="150" r="2" fill="#bae6fd"/>
<circle cx="32" cy="158" r="2" fill="#bae6fd"/>
<path d="M188,94 l3,9 9,3 -9,3 -3,9 -3,-9 -9,-3 9,-3 z" fill="#7dd3fc" opacity="0.9"/>
<path d="M54,102 l2.5,7 7,2.5 -7,2.5 -2.5,7 -2.5,-7 -7,-2.5 7,-2.5 z" fill="#38bdf8" opacity="0.85"/>
</svg>`
export default {
name: 'TotalFlowSummary',
components: { CountTo },
props: {
historyTotal: {
type: Number,
default: 0
}
},
data() {
return {
svgIcon: SVG_ICON,
//
miniBars: [42, 68, 55, 82, 73, 95]
}
},
computed: {
digitList() {
return String(this.historyTotal).padStart(8, '0').split('')
}
}
}
</script>
<style lang="scss">
@import "~@/assets/styles/index.scss";
</style>

157
src/views/trafficFlow/components/YearInFlowChart.vue

@ -0,0 +1,157 @@
<template>
<div ref="chart" class="chart-box" />
</template>
<script>
import { debounce } from '@/utils/index'
export default {
name: 'YearInFlowChart',
props: {
yearData: {
type: Array,
default: () => []
}
},
data() {
return {
chart: null,
ro: null
}
},
computed: {
xAxis() {
return this.yearData.map((i) => i.month + '月')
},
series() {
return this.yearData.map((i) => i.count || 0)
}
},
watch: {
yearData() {
this.setOption()
}
},
mounted() {
this.$nextTick(() => this.initChart())
},
beforeDestroy() {
this.stopTipLoop()
if (this.ro) {
this.ro.disconnect()
this.ro = null
}
if (this.chart) {
this.chart.dispose()
this.chart = null
}
},
methods: {
initChart() {
this.chart = this.$echarts.init(this.$refs.chart)
this.setOption()
// ResizeObserver window.resize
const doResize = debounce(() => {
if (this.chart) this.chart.resize()
}, 100)
this.ro = new ResizeObserver(() => doResize())
this.ro.observe(this.$refs.chart)
},
setOption() {
if (!this.chart) return
const idx = Math.max(0, this.series.length - 1)
const lastVal = this.series[idx]
this.chart.setOption({
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(11, 26, 48, 0.92)',
borderColor: '#18E3A7',
textStyle: { color: '#e2e8f0', fontSize: 14 },
axisPointer: { type: 'line', lineStyle: { color: '#18E3A7' }}
},
grid: { left: 40, right: 20, top: 50, bottom: 30, containLabel: false },
xAxis: {
type: 'category',
data: this.xAxis,
boundaryGap: false,
axisLine: { lineStyle: { color: 'rgba(24,227,167,0.4)' }},
axisTick: { show: false },
axisLabel: { color: 'rgba(255,255,255,0.7)', fontSize: 12, interval: 'auto' },
splitLine: { show: false }
},
yAxis: {
type: 'value',
min: 0,
max: (v) => v.max === 0 ? 100 : Math.ceil(v.max * 1.25),
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { color: 'rgba(255,255,255,0.6)', fontSize: 12 },
splitLine: { lineStyle: { color: 'rgba(24,227,167,0.12)', type: 'dashed' }}
},
series: [{
name: '进馆人次',
type: 'line',
smooth: true,
symbol: 'none',
showSymbol: false,
data: this.series,
lineStyle: { color: '#18E3A7', width: 3, shadowColor: '#18E3A7', shadowBlur: 12 },
itemStyle: { color: '#18E3A7' },
areaStyle: {
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(24,227,167,0.45)' },
{ offset: 1, color: 'rgba(24,227,167,0.02)' }
])
},
markPoint: {
symbol: 'circle',
symbolSize: 12,
label: {
position: 'top',
distance: 12,
formatter: () => String(lastVal),
fontSize: 26,
fontWeight: 800,
color: '#fff',
textShadowColor: 'rgba(24,227,167,0.8)',
textShadowBlur: 10
},
itemStyle: {
color: '#18E3A7',
shadowColor: 'rgba(24,227,167,1)',
shadowBlur: 12
},
data: [{ coord: [idx, lastVal] }]
}
}]
})
this.startTipLoop()
},
// tooltip
startTipLoop() {
this.stopTipLoop()
if (!this.series.length) return
this.tipIndex = -1
this.tipTimer = setInterval(() => {
if (!this.chart || !this.series.length) return
this.tipIndex = (this.tipIndex + 1) % this.series.length
this.chart.dispatchAction({
type: 'showTip',
seriesIndex: 0,
dataIndex: this.tipIndex
})
}, 2500)
},
stopTipLoop() {
if (this.tipTimer) {
clearInterval(this.tipTimer)
this.tipTimer = null
}
}
}
}
</script>
<style lang="scss">
@import "~@/assets/styles/index.scss";
</style>

118
src/views/trafficFlow/index.vue

@ -0,0 +1,118 @@
<template>
<div class="screen-main traffic-main">
<!-- 第一行三列等宽 -->
<div class="traffic-row traffic-row-3">
<div class="traffic-card card-hero">
<span class="card-corner tl" />
<span class="card-corner tr" />
<span class="card-corner bl" />
<span class="card-corner br" />
<div class="card-header">
<div class="card-label">进馆 · 当日进馆</div>
<div class="card-date">{{ todayDate }}</div>
</div>
<div class="module-content year-content">
<DayInFlowCard :today-total="todayTotal" />
</div>
</div>
<div class="traffic-card">
<span class="card-corner tl" />
<span class="card-corner tr" />
<span class="card-corner bl" />
<span class="card-corner br" />
<div class="card-header">
<div class="card-label">进馆 · 当月进馆</div>
<div class="card-date">{{ monthDate }}</div>
</div>
<div class="module-content">
<MonthInFlowChart :month-data="monthData" />
</div>
</div>
<div class="traffic-card">
<span class="card-corner tl" />
<span class="card-corner tr" />
<span class="card-corner bl" />
<span class="card-corner br" />
<div class="card-header">
<div class="card-label">进馆 · 本年进馆</div>
<div class="card-date">{{ yearDate }}</div>
</div>
<div class="module-content">
<YearInFlowChart :year-data="yearData" />
</div>
</div>
</div>
<!-- 第二行全宽汇总 -->
<div class="traffic-row traffic-summary">
<TotalFlowSummary :history-total="historyTotal" />
</div>
</div>
</template>
<script>
import DayInFlowCard from './components/DayInFlowCard.vue'
import MonthInFlowChart from './components/MonthInFlowChart.vue'
import YearInFlowChart from './components/YearInFlowChart.vue'
import TotalFlowSummary from './components/TotalFlowSummary.vue'
import { FetchInitIntoNumHS } from '@/api/library'
export default {
name: 'TrafficFlow',
components: { DayInFlowCard, MonthInFlowChart, YearInFlowChart, TotalFlowSummary },
data() {
return {
todayDate: '',
monthDate: '',
yearDate: '',
//
todayTotal: 0,
monthData: [],
yearData: [],
historyTotal: 0,
timer: null
}
},
created() {
const now = new Date()
const y = now.getFullYear()
const m = String(now.getMonth() + 1).padStart(2, '0')
const d = String(now.getDate()).padStart(2, '0')
this.todayDate = `${y}-${m}-${d}`
this.monthDate = `${y}-${m}`
this.yearDate = `${y}`
this.fetchData()
this.timer = setInterval(() => this.fetchData(), 15000)
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
},
methods: {
fetchData() {
// doorCodes URL query http://localhost:8080/#/?doorCodes=D3D4
const doorCodes = this.$route.query.doorCodes || ''
const params = {
'doorCodes': doorCodes
}
FetchInitIntoNumHS(params).then((res) => {
if (res.errCode === 0 && res.data) {
this.todayTotal = res.data.todayTotal || 0
this.monthData = res.data.monthData || []
this.yearData = res.data.yearData || []
this.historyTotal = res.data.historyTotal || 0
}
}).catch((err) => {
console.error('客流量接口异常', err)
})
}
}
}
</script>
<style lang="scss">
@import "~@/assets/styles/index.scss";
</style>

68
vue.config.js

@ -0,0 +1,68 @@
const path = require('path')
const resolve = dir => {
return path.join(__dirname, dir)
}
const name = '人流数据大屏'
module.exports = {
publicPath: process.env.NODE_ENV === 'development' ? '/' : './',
outputDir: 'dist',
assetsDir: 'static',
lintOnSave: process.env.NODE_ENV === 'development',
productionSourceMap: false,
devServer: {
port: 8080,
open: true,
overlay: {
warnings: false,
errors: true
},
proxy: {
'/dxhtsg/': {
target: process.env.VUE_APP_BASE_API,
changeOrigin: true,
pathRewrite: {
'^/dxhtsg': 'dxhtsg'
}
},
'/qyzt/': {
target: process.env.VUE_APP_BASE_API,
changeOrigin: true,
pathRewrite: {
'^/qyzt': 'qyzt'
}
},
'/auth/': {
target: process.env.VUE_APP_BASE_API,
changeOrigin: true,
pathRewrite: {
'^/auth': 'auth'
}
}
}
},
configureWebpack: {
name: name,
resolve: {
alias: {
'@': resolve('src')
}
},
performance: {
hints: 'warning',
// 入口起点的最大体积
maxEntrypointSize: 50000000,
// 生成文件的最大体积
maxAssetSize: 30000000
}
},
chainWebpack: config => {
config.resolve
.alias.set('_c', resolve('src/components'))
config.plugin('html')
.tap(args => {
args[0].title = '人流数据大屏'
return args
})
}
}
Loading…
Cancel
Save