Merge pull request '数据看板页面实现' (#4) from lm/feat/dashboard into master

Reviewed-on: #4
This commit is contained in:
tangweijie 2026-01-18 16:10:27 +08:00
commit 060cd5590b
25 changed files with 2305 additions and 3 deletions

View File

@ -115,6 +115,7 @@
"lint-staged": "^15.2.2", "lint-staged": "^15.2.2",
"postcss": "^8.4.35", "postcss": "^8.4.35",
"postcss-html": "^1.6.0", "postcss-html": "^1.6.0",
"postcss-pxtorem": "^6.1.0",
"postcss-scss": "^4.0.9", "postcss-scss": "^4.0.9",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-eslint": "^16.3.0", "prettier-eslint": "^16.3.0",

12
pnpm-lock.yaml generated
View File

@ -270,6 +270,9 @@ importers:
postcss-html: postcss-html:
specifier: ^1.6.0 specifier: ^1.6.0
version: 1.7.0 version: 1.7.0
postcss-pxtorem:
specifier: ^6.1.0
version: 6.1.0(postcss@8.4.49)
postcss-scss: postcss-scss:
specifier: ^4.0.9 specifier: ^4.0.9
version: 4.0.9(postcss@8.4.49) version: 4.0.9(postcss@8.4.49)
@ -4249,6 +4252,11 @@ packages:
resolution: {integrity: sha512-MfcMpSUIaR/nNgeVS8AyvyDugXlADjN9AcV7e5rDfrF1wduIAGSkL4q2+wgrZgA3sHVAHLDO9FuauHhZYW2nBw==} resolution: {integrity: sha512-MfcMpSUIaR/nNgeVS8AyvyDugXlADjN9AcV7e5rDfrF1wduIAGSkL4q2+wgrZgA3sHVAHLDO9FuauHhZYW2nBw==}
engines: {node: ^12 || >=14} engines: {node: ^12 || >=14}
postcss-pxtorem@6.1.0:
resolution: {integrity: sha512-ROODSNci9ADal3zUcPHOF/K83TiCgNSPXQFSbwyPHNV8ioHIE4SaC+FPOufd8jsr5jV2uIz29v1Uqy1c4ov42g==}
peerDependencies:
postcss: ^8.0.0
postcss-resolve-nested-selector@0.1.6: postcss-resolve-nested-selector@0.1.6:
resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==}
@ -9569,6 +9577,10 @@ snapshots:
postcss: 8.4.49 postcss: 8.4.49
postcss-safe-parser: 6.0.0(postcss@8.4.49) postcss-safe-parser: 6.0.0(postcss@8.4.49)
postcss-pxtorem@6.1.0(postcss@8.4.49):
dependencies:
postcss: 8.4.49
postcss-resolve-nested-selector@0.1.6: {} postcss-resolve-nested-selector@0.1.6: {}
postcss-safe-parser@6.0.0(postcss@8.4.49): postcss-safe-parser@6.0.0(postcss@8.4.49):

View File

@ -1,5 +1,15 @@
module.exports = { module.exports = {
plugins: { plugins: {
autoprefixer: {} autoprefixer: {},
'postcss-pxtorem': {
rootValue: 16, // 设计稿基准值1rem = 16px
unitPrecision: 5, // rem 的小数位数
propList: ['*'], // 需要转换的属性,* 表示所有属性
selectorBlackList: [], // 忽略的选择器,可以使用正则表达式
replace: true, // 是否替换而不是添加
mediaQuery: false, // 是否在媒体查询中转换 px
minPixelValue: 0, // 设置要替换的最小像素值0 表示所有值都转换
exclude: /node_modules/i // 排除 node_modules 目录
}
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1768528542812" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8272" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M838.4 473.6 505.6 134.4C499.2 128 480 134.4 480 147.2l0 256L217.6 134.4C211.2 128 192 134.4 192 147.2l0 729.6c0 12.8 12.8 19.2 25.6 12.8l262.4-262.4 0 256c0 12.8 12.8 19.2 25.6 12.8l339.2-339.2C864 531.2 864 492.8 838.4 473.6z" fill="#70B5C3" p-id="8273"></path></svg>

After

Width:  |  Height:  |  Size: 602 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1768544147360" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12017" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M11.54 290.28999998v461.35h690.625l0.596-461.35c0-11.547-9.301-20.907-20.748-20.907h-649.77c-11.443 0-20.705 9.361-20.704 20.907z" p-id="12018" fill="#E08F53"></path><path d="M391.146 903.10199998h304.473c-0.018-0.844-0.118-1.686-0.118-2.567 0-6.217 3.718-32.475 5.727-39.252 7.63-25.817 42-98.624 130.985-98.624 90.645 0 123.372 72.806 131.003 98.624 1.986 6.778 5.707 33.036 5.707 39.252 0 0.881-0.101 1.723-0.14 2.567h22.697c11.446 0 20.726-9.342 20.726-20.889v-107.402c0-5.312-2.083-10.146-5.302-13.834v-156.209c0-120.832-79.019-183.293-82.396-185.899-3.634-2.804-8.048-4.307-12.598-4.307h-166.716c-6.141 0-11.585 2.747-15.381 7.036v343.996c0 8.434-5.454 13.91-13.812 13.91h-396.032c66.527 25.733 71.141 93.803 71.181 123.598zM765.92 578.73199998v-122.356h138.316c13.791 12.769 51.567 53.721 59.599 122.356h-197.915z" p-id="12019" fill="#E08F53"></path><path d="M53.653 774.19099998c0 0.201-0.059 108.028-0.059 108.028 0 11.549 9.258 20.889 20.704 20.889h43.722c-8.245-64.458 49.987-123.258 82.07-128.915-26.156-0.001-146.437-0.001-146.437-0.001z" p-id="12020" fill="#E08F53"></path><path d="M832.215 790.54399998c60.763 0 110.021 49.26 110.021 110.022 0 60.761-49.259 110.02-110.021 110.02s-110.02-49.26-110.02-110.02c0-60.763 49.258-110.022 110.02-110.022zM781.382 900.56699998c0 28.073 22.759 50.83 50.833 50.83s50.831-22.758 50.831-50.83c0-28.075-22.758-50.832-50.831-50.832-28.075 0-50.833 22.757-50.833 50.832z" p-id="12021" fill="#E08F53"></path><path d="M255.55 790.54399998c60.763 0 110.02 49.26 110.02 110.022 0 60.761-49.258 110.02-110.02 110.02s-110.021-49.26-110.021-110.02c0-60.763 49.259-110.022 110.021-110.022zM204.716 900.56699998c0 28.073 22.758 50.83 50.832 50.83s50.832-22.758 50.832-50.83c0-28.075-22.758-50.832-50.832-50.832s-50.832 22.757-50.832 50.832z" p-id="12022" fill="#E08F53"></path></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1768544597038" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="25765" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M723.2 509.866667a298.496 298.496 0 0 0 79.274667-280.832l106.453333-45.653334a21.333333 21.333333 0 0 1 29.738667 19.626667V810.666667l-298.666667 128-256-128-268.928 115.242666a21.333333 21.333333 0 0 1-29.738667-19.626666V298.666667l133.504-57.216a298.368 298.368 0 0 0 81.962667 268.373333L512 721.066667l211.2-211.2z m-60.330667-60.373334L512 600.362667l-150.869333-150.869334a213.333333 213.333333 0 1 1 301.738666 0z" fill="#7CC897" p-id="25766"></path></svg>

After

Width:  |  Height:  |  Size: 800 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1768544331333" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14246" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M690.338 661.474c-41.498-18.374-103.937-65.868-195.163-82.197 23.332-24.985 40.564-63.996 58.672-110.231 10.528-26.786 8.425-49.62 8.425-82.185 0-24.002 4.507-62.528-1.502-83.71-20.073-71.577-71.002-91.29-130.507-91.29-59.583 0-110.473 19.828-130.557 91.452-5.848 21.24-1.355 59.627-1.355 83.548 0 32.623-1.764 55.56 8.787 82.359 18.247 46.477 35.683 85.444 58.865 110.347-90.428 16.525-148.928 63.73-190.16 82.047-85.32 38.11-83.219 79.828-83.219 79.828v70.698l685.644-0.128v-70.57c0-0.001-2.262-41.88-87.93-79.968zM659.56 227.805h271.187v59.352H659.56v-59.352zM659.56 357.765h165.782v60.375H659.56v-60.375z" fill="#8FEAFC" p-id="14247"></path><path d="M659.56 487.725h237.417V548.1H659.56v-60.375z" fill="#8FEAFC" p-id="14248"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1768544265577" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13983" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M328.4 292.6a182.9 182.9 0 1 0 365.8 0 182.9 182.9 0 1 0-365.8 0zM757.6 638.6L658.4 691c-15.6 8.3-15.6 30.7 0 38.9l99.2 52.4c6.4 3.4 14.1 3.4 20.6 0l99.2-52.4c15.6-8.3 15.6-30.7 0-38.9l-99.2-52.4c-6.5-3.4-14.2-3.4-20.6 0z" fill="#9D67D3" p-id="13984"></path><path d="M881.7 752.3c-1.1 0-3.3 1.1-5.1 2.2l-90 54.1c-11.7 6.9-26 6.9-37.7 0l-90.3-54.1c-1-0.7-3.5-2.2-5.1-2.2-4 0-7.3 3.3-7.3 7.3v18.3c0 4 2.6 7.3 5.9 8.8l96.9 58.5c11.7 6.9 26.3 6.9 37.7 0l96.9-58.5c3.3-1.5 5.5-4.8 5.5-8.8v-18.3c0-4-3.3-7.3-7.4-7.3z" fill="#9D67D3" p-id="13985"></path><path d="M881.7 816c-1.1 0-3.3 1.1-5.1 2.2l-90 54.1c-11.7 6.9-26 6.9-37.7 0l-90.3-54.1c-1-0.7-3.5-2.2-5.1-2.2-4 0-7.3 3.3-7.3 7.3v18.3c0 4 2.6 7.3 5.9 8.8l96.9 58.5c11.7 6.9 26.3 6.9 37.7 0l96.9-58.5c3.3-1.5 5.5-4.8 5.5-8.8v-18.3c0-4-3.3-7.3-7.4-7.3zM768 599.4c7.3 0 14.3 1.5 20.8 4 0.4 0 0.7 0.4 1.1 0.4 2.2 1.1 4.8 1.5 7.3 1.5 9.9 0 18.3-8 18.3-18.3 0-4-1.1-7.3-3.3-10.2-0.7-1.1-1.5-1.8-2.2-2.6-39.1-43.9-87.4-79.4-142.6-102.8-11.7-5.1-24.9-2.6-35.8 4.4-34.4 23-75.7 36.2-120.3 36.2-44.6 0-86.3-13.5-121.1-36.6-10.6-6.9-23.8-9.5-35.8-4.8-132.8 56-229.3 180.7-244.3 329.9-2.2 21.6 15 40.6 36.9 40.6h426.4c20.1 0 36.2-16.1 36.2-36.2 0.4-36.6 0.4-88.5 0.4-94.4 0-21.9 12.1-41.7 31.5-51.9l99.1-52.3c8.4-4.3 17.9-6.9 27.4-6.9z" fill="#9D67D3" p-id="13986"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1768526192243" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4670" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M313.991837 914.285714c-20.37551 0-40.228571-6.269388-56.946939-18.808163-30.302041-21.942857-44.930612-58.514286-38.661225-95.085714l24.032654-141.061225c3.134694-18.285714-3.134694-36.571429-16.195919-49.110204L123.297959 509.910204c-26.644898-26.122449-36.04898-64.261224-24.555102-99.787755 11.493878-35.526531 41.795918-61.126531 78.889796-66.35102l141.583674-20.375511c18.285714-2.612245 33.959184-14.106122 41.795918-30.30204l63.216326-128.522449C440.946939 130.612245 474.383673 109.714286 512 109.714286s71.053061 20.897959 87.24898 54.334694L662.987755 292.571429c8.359184 16.195918 24.032653 27.689796 41.795918 30.30204l141.583674 20.375511c37.093878 5.22449 67.395918 30.82449 78.889796 66.35102 11.493878 35.526531 2.089796 73.665306-24.555102 99.787755l-102.4 99.787755c-13.061224 12.538776-19.330612 31.346939-16.195919 49.110204l24.032654 141.061225c6.269388 37.093878-8.359184 73.142857-38.661225 95.085714-30.302041 21.942857-69.485714 24.555102-102.4 7.314286L538.122449 836.440816c-16.195918-8.359184-35.526531-8.359184-51.722449 0l-126.955102 66.87347c-14.628571 7.314286-30.302041 10.971429-45.453061 10.971428z m162.481632-96.653061z" fill="#DA5159" p-id="4671"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1768460125201" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4673" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512.0181274414062 140.7236328125c-125.93957519531247 0-228.48156738281247 102.50326538085938-228.48156738281247 228.48074340820312 0 125.90167236328126 102.46701049804688 228.44448852539062 228.48156738281247 228.44448852539062 125.97665405273438 0 228.517822265625-102.54199218749999 228.517822265625-228.44448852539062 0-125.97747802734375-102.54116821289061-228.48074340820312-228.517822265625-228.48074340820312z" fill="#ffffff" p-id="4674"></path><path d="M688.11962890625 592.4355773925781c-48.68206787109375 38.48208618164063-109.30682373046875 62.32461547851563-176.10150146484375 62.32461547851563-66.79550170898438 0-127.38153076171872-23.915863037109375-176.17565917968753-62.32461547851563C243.4666748046875 651.2484130859374 179.14804077148432 754.5303344726562 170.31338500976562 874.1483764648438c32.343475341796875 4.396728515625001 89.89892578124999 9.127990722656248 172.03436279296875 9.127990722656248h339.1916198730469c82.17333984375 0 139.7650451660156-4.731262207031249 172.14724731445312-9.127990722656248-8.762145996093748-119.69384765625-73.07995605468749-222.89996337890622-165.5669860839844-281.71279907226557z" fill="#ffffff" p-id="4675"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1768465043767" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9825" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M515 161.4c201.7 0 365.3 163.6 365.3 365.3S716.7 892 515 892 149.7 728.4 149.7 526.7 313.3 161.4 515 161.4z" fill="#7CC897" opacity=".7" p-id="9826"></path><path d="M514 526.7h448.2c0-246.9-200.2-447.1-447.1-447.1" fill="#7CC897" opacity=".7" p-id="9827"></path></svg>

After

Width:  |  Height:  |  Size: 601 B

View File

@ -25,6 +25,10 @@ import '@/styles/index.scss'
// 引入动画 // 引入动画
import '@/plugins/animate.css' import '@/plugins/animate.css'
// 初始化 rem 移动端适配
import { initRem } from '@/utils/rem'
initRem()
// 路由 // 路由
import router, { setupRouter } from '@/router' import router, { setupRouter } from '@/router'

View File

@ -53,13 +53,19 @@ const whiteList = [
'/auth-redirect', '/auth-redirect',
'/bind', '/bind',
'/register', '/register',
'/oauthLogin/gitee' '/oauthLogin/gitee',
'/dashboard' // Dashboard 页面
] ]
// 路由加载前 // 路由加载前
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
start() start()
loadStart() loadStart()
// 如果是主页路径或 dashboard 路径,直接放行(跳过权限验证)
if (to.path === '/dashboard') {
next()
return
}
if (getAccessToken()) { if (getAccessToken()) {
if (to.path === '/login') { if (to.path === '/login') {
next({ path: '/' }) next({ path: '/' })

View File

@ -174,6 +174,16 @@ const remainingRouter: AppRouteRecordRaw[] = [
} }
] ]
}, },
{
path: '/dashboard',
component: () => import('@/views/Dashboard/Index.vue'),
name: 'Dashboard',
meta: {
hidden: true,
title: 'Dashboard',
noTagsView: true
}
},
{ {
path: '/login', path: '/login',
component: () => import('@/views/Login/Login.vue'), component: () => import('@/views/Login/Login.vue'),

View File

@ -535,3 +535,6 @@ export const subString = (str: string, start: number, end: number) => {
} }
return str return str
} }
// rem 移动端适配
export { initRem, getRem } from './rem'

65
src/utils/rem.ts Normal file
View File

@ -0,0 +1,65 @@
/**
* rem
*
*/
// 设计稿基准宽度根据你的设计稿调整常见值375、750
const DESIGN_WIDTH = 375
// 基准字体大小(与 postcss.config.js 中的 rootValue 保持一致)
const BASE_FONT_SIZE = 16
/**
* rem
*/
function setRem() {
const docEl = document.documentElement
const width = docEl.clientWidth || window.innerWidth
// 计算根字体大小:屏幕宽度 / 设计稿宽度 * 基准字体大小
const rem = (width / DESIGN_WIDTH) * BASE_FONT_SIZE
// 设置根元素字体大小
docEl.style.fontSize = `${rem}px`
// 限制最大和最小字体大小(可选)
const maxRem = BASE_FONT_SIZE * 1.5 // 最大 24px
const minRem = BASE_FONT_SIZE * 0.8 // 最小 12.8px
if (rem > maxRem) {
docEl.style.fontSize = `${maxRem}px`
} else if (rem < minRem) {
docEl.style.fontSize = `${minRem}px`
}
}
/**
* rem
*/
export function initRem() {
// 初始设置
setRem()
// 监听窗口大小变化
window.addEventListener('resize', setRem)
// 监听屏幕方向变化(移动端横竖屏切换)
window.addEventListener('orientationchange', () => {
setTimeout(setRem, 100)
})
// 监听页面显示(处理浏览器标签页切换)
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
setRem()
}
})
}
/**
* rem
*/
export function getRem(): number {
const docEl = document.documentElement
const fontSize = window.getComputedStyle(docEl).fontSize
return parseFloat(fontSize)
}

View File

@ -0,0 +1,485 @@
<template>
<div class="dashboard-container">
<div class="prison-name">王某某</div>
<div class="current-time">{{ currentTime }}</div>
<div class="dashboard-content">
<div class="dashboard-content-top">
<div class="dashboard-content-top-left">
<InfoCard :basic-info="basicInfo" :interview-records="interviewRecords" />
</div>
<div class="dashboard-content-top-center">
<div class="gauge-container">
<div class="dashboard-content-top-center-data">
<!-- 左侧第一个卡片 -->
<div class="info-card-item">
<div class="card-number">{{ centerLeftData.top.value }}</div>
<div class="card-label">{{ centerLeftData.top.label }}</div>
</div>
<!-- 左侧第二个卡片 -->
<div class="card-row">
<div class="info-card-item">
<div class="card-number">{{ centerLeftData.middle.left.value }}</div>
<div class="card-label">{{ centerLeftData.middle.left.label }}</div>
</div>
<div class="info-card-item">
<div class="card-number">{{ centerLeftData.middle.right.value }}</div>
<div class="card-label">{{ centerLeftData.middle.right.label }}</div>
</div>
</div>
</div>
<div class="dashboard-content-top-center-center">
<GaugeChart :height="'200px'" :value="gaugeValue" :name="gaugeName" />
</div>
<div class="dashboard-content-top-center-data">
<!-- 右侧第一个卡片 -->
<div class="info-card-item">
<div class="card-number">{{ centerRightData.top.value }}</div>
<div class="card-label">{{ centerRightData.top.label }}</div>
</div>
<!-- 右侧第二个卡片 -->
<div class="card-row">
<div class="info-card-item">
<div class="card-number">{{ centerRightData.middle.left.value }}</div>
<div class="card-label">{{ centerRightData.middle.left.label }}</div>
</div>
<div class="info-card-item">
<div class="card-number">{{ centerRightData.middle.right.value }}</div>
<div class="card-label">{{ centerRightData.middle.right.label }}</div>
</div>
</div>
</div>
</div>
<div class="list-container">
<div class="list-card-item">
<div class="list-card-item-icon icon-location"></div>
<div class="list-card-item-value">108</div>
</div>
<div class="list-card-item">
<div class="list-card-item-icon icon-person"></div>
<div class="list-card-item-value">108</div>
</div>
<div class="list-card-item">
<div class="list-card-item-icon icon-person2"></div>
<div class="list-card-item-value">108</div>
</div>
<div class="list-card-item">
<div class="list-card-item-icon icon-car"></div>
<div class="list-card-item-value">108</div>
</div>
</div>
</div>
<div class="dashboard-content-top-right">
<ScoreAssessment />
</div>
</div>
<div class="dashboard-content-bottom">
<div class="dashboard-content-bottom-left">
<ConsumptionRecords />
</div>
<div class="dashboard-content-bottom-center">
<RecentRewardsPunishments />
</div>
<div class="dashboard-content-bottom-right">
<div class="dashboard-content-bottom-right-title">大帐统计</div>
<BarChart :height="'200px'" :data="barChartData" :card-data="barCardData" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// @ts-ignore
import GaugeChart from './components/GaugeChart.vue'
// @ts-ignore
import BarChart from './components/BarChart.vue'
// @ts-ignore
import InfoCard from './components/InfoCard/Index.vue'
// @ts-ignore
import ScoreAssessment from './components/ScoreAssessment/Index.vue'
// @ts-ignore
import RecentRewardsPunishments from './components/RecentRewardsPunishments/Index.vue'
// @ts-ignore
import ConsumptionRecords from './components/ConsumptionRecords/Index.vue'
import { ref, onMounted, onUnmounted } from 'vue'
defineOptions({ name: 'Dashboard' })
//
const gaugeValue = ref(92.23)
const gaugeName = ref('')
//
const centerLeftData = ref({
top: {
value: '1054',
label: 'XXXXXXXX'
},
middle: {
left: {
value: '124',
label: 'XXXXXXXX'
},
right: {
value: '78.24%',
label: 'XXXXXXXX'
}
},
bottom: {
value: '108位',
label: 'XXXXXXXXXX'
}
})
//
const centerRightData = ref({
top: {
value: '13',
label: 'XXXXXXXX'
},
middle: {
left: {
value: '15',
label: 'XXXXXXXX'
},
right: {
value: '34',
label: 'XXXXXXXX'
}
},
bottomLeft: {
value: '58位',
label: 'XXXXXXXXX'
},
bottomRight: {
value: '58辆',
label: 'XXXXXXXXX'
}
})
//
const barChartData = ref([
{ category: '1月', monthlyStandard: 24, perCapita: 17 },
{ category: '2月', monthlyStandard: 18, perCapita: 24 },
{ category: '3月', monthlyStandard: 15, perCapita: 30 },
{ category: '4月', monthlyStandard: 37, perCapita: 18 },
{ category: '5月', monthlyStandard: 29, perCapita: 15 },
{ category: '6月', monthlyStandard: 17, perCapita: 24 },
{ category: '7月', monthlyStandard: 15, perCapita: 30 },
{ category: '8月', monthlyStandard: 15, perCapita: 30 },
{ category: '9月', monthlyStandard: 15, perCapita: 30 },
{ category: '10月', monthlyStandard: 15, perCapita: 30 },
{ category: '11月', monthlyStandard: 15, perCapita: 30 },
{ category: '12月', monthlyStandard: 15, perCapita: 30 }
])
//
const barCardData = ref({
inProgress: 5,
toWarehouse: 5,
outWarehouse: 5
})
//
const basicInfo = ref({
district: '第1监区 十四分监区',
prisonNumber: 'ZF20230001',
sentenceStart: '2020/07/21',
sentenceEnd: '2020/07/21',
sentenceDays: 300,
age: 78,
hometown: '中国/江苏',
education: '大学',
maritalStatus: '未婚',
children: '无子女',
birthDate: '2025-02-02',
crimeType: '盗窃罪',
previousConvictions: '7次',
sentence: '有期徒刑1年6月'
})
// 访
const interviewRecords = ref([
{
date: '2023-10',
content: '情绪稳定,遵规守纪良好'
},
{
date: '2023-09',
content: '人际关系融洽,态度积极'
},
{
date: '2023-08',
content: '偶有焦虑情绪,需关注'
},
{
date: '2023-07',
content: '偶有焦虑情绪,需关注'
}
])
//
const currentTime = ref('')
//
const formatTime = () => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
const weekday = weekdays[now.getDay()]
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
const seconds = String(now.getSeconds()).padStart(2, '0')
return `${year}${month}${day}${weekday} ${hours}:${minutes}:${seconds}`
}
let timeInterval: NodeJS.Timeout | null = null
onMounted(() => {
//
currentTime.value = formatTime()
//
timeInterval = setInterval(() => {
currentTime.value = formatTime()
}, 1000)
//
// API barChartData.value
})
onUnmounted(() => {
//
if (timeInterval) {
clearInterval(timeInterval)
}
})
</script>
<style scoped lang="scss">
.dashboard-container {
width: 100%;
height: 100%;
background-image: url('@/assets/imgs/dashboard/dashboard-back.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
padding: 5% 20px;
}
.prison-name {
position: fixed;
top: 12px;
left: 50%;
transform: translateX(-48%);
color: white;
text-align: center;
width: 100%;
z-index: 1000;
font-size: 20px;
font-weight: bold;
}
.current-time {
position: fixed;
top: 12px;
right: 32px;
color: white;
font-size: 12px;
z-index: 1000;
font-weight: 500;
}
.dashboard-content {
margin: 0 auto;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 8px;
}
.dashboard-content-top {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
gap: 15px;
}
.dashboard-content-top-left {
width: 23%;
height: 100%;
}
.dashboard-content-top-center {
width: 54%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
border: 1px solid rgba(56, 102, 141, 0.5);
border-radius: 8px;
padding: 12px;
}
.gauge-container {
width: 100%;
height: 75%;
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 12px;
}
.list-container {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 24px;
height: 25%;
}
.list-card-item {
width: 25%;
height: 80%;
background: #2d3d5f;
border: 1px solid rgba(56, 102, 141, 0.5);
display: flex;
padding-left: 10px;
justify-content: start;
align-items: center;
}
.list-card-item-icon {
width: 18px;
height: 18px;
margin-right: 10px;
}
.icon-location {
background: url('@/assets/imgs/dashboard/icon-location.svg') no-repeat center center;
background-size: 100% 100%;
}
.icon-person {
background: url('@/assets/imgs/dashboard/icon-person.svg') no-repeat center center;
background-size: 100% 100%;
}
.icon-person2 {
background: url('@/assets/imgs/dashboard/icon-person2.svg') no-repeat center center;
background-size: 100% 100%;
}
.icon-car {
background: url('@/assets/imgs/dashboard/icon-card.svg') no-repeat center center;
background-size: 100% 100%;
}
.list-card-item-value {
font-size: 10px;
font-weight: bold;
color: #ffffff;
}
.dashboard-content-top-center-data {
width: 32%;
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px 0 10px;
justify-content: space-between;
}
.dashboard-content-top-center-center {
width: 36%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
padding: 8px 0;
}
//
.info-card-item {
border-radius: 2px;
padding: 5px 10px;
box-shadow: inset 0 0 15px 0 #2b4183;
border: 1px solid #2b4183;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
.card-row {
display: flex;
justify-content: space-between;
gap: 10px;
width: 100%;
}
.card-number {
font-size: 10px;
font-weight: bold;
color: #ffffff;
line-height: 1.3;
margin-bottom: 6px;
white-space: nowrap;
}
.card-label {
font-size: 7px;
color: rgba(255, 255, 255, 0.65);
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
margin-top: 2px;
}
.dashboard-content-top-right {
width: 23%;
height: 100%;
}
.dashboard-content-bottom {
flex: 1;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 15px;
}
.dashboard-content-bottom-left {
width: 38%;
height: 100%;
}
.dashboard-content-bottom-center {
width: 24%;
height: 100%;
}
.dashboard-content-bottom-right {
width: 38%;
height: 100%;
border: 1px solid rgba(56, 102, 141, 0.5);
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.dashboard-content-bottom-right-title {
font-size: 12px;
font-weight: bold;
color: #ffffff;
margin-bottom: 12px;
}
}
</style>

View File

@ -0,0 +1,250 @@
<template>
<div class="supply-chart-container">
<!-- 柱状图 -->
<EChart :options="barOption" :height="height" />
</div>
</template>
<script setup lang="ts">
import type { EChartsOption } from 'echarts'
// @ts-ignore
import EChart from '@/components/Echart/src/Echart.vue'
import { computed, watch } from 'vue'
defineOptions({ name: 'BarChart' })
interface ChartDataItem {
category: string
monthlyStandard: number
perCapita: number
}
interface CardData {
inProgress: number
toWarehouse: number
outWarehouse: number
}
const props = withDefaults(
defineProps<{
width?: number
height?: string
data?: ChartDataItem[]
cardData?: CardData
}>(),
{
width: 400,
height: '300px',
data: () => [],
cardData: () => ({
inProgress: 5,
toWarehouse: 5,
outWarehouse: 5
})
}
)
//
const createChartOption = (): EChartsOption => {
const categories = props.data.map((item) => item.category)
const monthlyStandardData = props.data.map((item) => item.monthlyStandard)
const perCapitaData = props.data.map((item) => item.perCapita)
// 50
const maxValue = 50
const monthlyStandardBgData = categories.map((_, index) => maxValue - monthlyStandardData[index])
const perCapitaBgData = categories.map((_, index) => maxValue - perCapitaData[index])
return {
backgroundColor: 'transparent',
grid: {
left: '10%',
right: '15%',
top: '20%',
bottom: '15%',
containLabel: false
},
xAxis: {
type: 'category',
data: categories,
axisLine: {
lineStyle: {
color: '#38668D50'
}
},
axisLabel: {
color: '#D8F0FF',
fontSize: 10,
interval: 0,
rotate: 0
},
axisTick: {
show: false
}
},
yAxis: {
type: 'value',
min: 0,
max: 50,
interval: 10,
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: '#D8F0FF',
fontSize: 10
},
splitLine: {
lineStyle: {
color: '#38668D20',
type: 'dashed'
}
}
},
legend: {
data: ['支出', '收入'],
top: '5%',
right: '10%',
textStyle: {
color: '#6D869A',
fontSize: 9
},
itemWidth: 9,
itemHeight: 9,
itemGap: 25
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: 'rgba(0, 167, 255, 0.5)',
textStyle: {
color: '#D8F0FF',
fontSize: 10
},
formatter: function (params: any) {
let result = params[0].name + '<br/>'
//
params.forEach((param: any) => {
if (param.seriesName === '支出' || param.seriesName === '收入') {
result += param.marker + param.seriesName + ': ' + param.value + '<br/>'
}
})
return result
}
},
series: [
// -
{
name: '支出',
type: 'bar',
stack: 'monthly',
data: monthlyStandardData,
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: '#10A0F2'
},
{
offset: 1,
color: 'rgba(0, 82, 184, 0)'
}
]
}
},
barWidth: '20%',
barGap: '20%'
},
// -
{
name: '支出底色',
type: 'bar',
stack: 'monthly',
data: monthlyStandardBgData,
itemStyle: {
color: '#38668D70'
},
barWidth: '20%',
barGap: '20%',
silent: true,
tooltip: {
show: false
}
},
//
{
name: '收入',
type: 'bar',
stack: 'perCapita',
data: perCapitaData,
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: '#FFA58D'
},
{
offset: 1,
color: 'rgba(87, 140, 205, 0)'
}
]
}
},
barWidth: '20%',
barGap: '80%'
},
{
name: '收入底色',
type: 'bar',
stack: 'perCapita',
data: perCapitaBgData,
itemStyle: {
color: '#38668D70'
},
barWidth: '20%',
barGap: '80%',
silent: true,
tooltip: {
show: false
}
}
]
}
}
//
const barOption = computed(() => createChartOption())
//
watch(
() => [props.data, props.cardData],
() => {
// computed
},
{ deep: true }
)
</script>
<style scoped lang="scss">
.supply-chart-container {
width: 100%;
}
</style>

View File

@ -0,0 +1,436 @@
<template>
<div class="consumption-records-container">
<div class="consumption-records-title">
<span class="title-text">费用明细</span>
</div>
<div class="consumption-records-relationship">
<div
v-for="(item, index) in relationshipData"
:key="index"
class="relationship-item"
:class="`relate-${item.color}`"
>
<div class="relationship-icon" :class="`icon-${item.color}`">
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor">
<path
d="M16 7c0-2.21-1.79-4-4-4S8 4.79 8 7c0 2.21 1.79 4 4 4s4-1.79 4-4zm-4 6c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
/>
<path
d="M20 7c0-2.21-1.79-4-4-4s-4 1.79-4 4c0 2.21 1.79 4 4 4s4-1.79 4-4zm-4 6c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
/>
</svg>
</div>
<div class="relationship-name">{{ item.name }}</div>
<div class="relationship-relate">{{ item.relate }}</div>
</div>
</div>
<!-- 标题栏 -->
<div class="consumption-records-content">
<div class="consumption-records">
<div class="records-header">
<span class="header-title">消费记录</span>
</div>
<!-- 记录列表 -->
<div class="records-list">
<div v-for="(item, index) in recordsData" :key="index" class="record-item">
<div class="record-date">{{ item.date }}</div>
<div class="record-name" :class="`name-${item.nameColor}`">
{{ item.name }}
</div>
<div class="record-category">{{ item.category }}</div>
<div class="record-amount">¥{{ item.amount }}</div>
</div>
</div>
</div>
<div class="consumption-records">
<div class="records-header">
<span class="header-title">近期汇款</span>
</div>
<!-- 记录列表 -->
<div class="records-list">
<div v-for="(item, index) in remittancesData" :key="index" class="record-item">
<div class="record-date">{{ item.date }}</div>
<div class="record-name" :class="`name-${item.nameColor}`">
{{ item.name }}
</div>
<div class="record-category">{{ item.category }}</div>
<div class="record-amount">¥{{ item.amount }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
defineOptions({ name: 'ConsumptionRecords' })
interface ConsumptionRecord {
date: string
name: string
nameColor: string
category: string
amount: number
}
interface RelationshipRecord {
name: string
relate: string
color: string
}
const relationshipData = ref<RelationshipRecord[]>([
{
name: '陈小美',
relate: '妻子',
color: 'orange'
},
{
name: '李三三',
relate: '儿子',
color: 'green'
},
{
name: '李为一',
relate: '父亲',
color: 'yellow'
},
{
name: '王大秀',
relate: '母亲',
color: 'purple'
}
])
//
const recordsData = ref<ConsumptionRecord[]>([
{
date: '2021-01-02',
name: '张伟',
nameColor: 'purple',
category: '餐饮',
amount: 1000
},
{
date: '2021-01-03',
name: '王强',
nameColor: 'green',
category: '购物',
amount: 500
},
{
date: '2021-01-04',
name: '陈丽',
nameColor: 'blue',
category: '交通',
amount: 200
},
{
date: '2021-01-05',
name: '赵敏',
nameColor: 'teal',
category: '餐饮',
amount: 800
},
{
date: '2021-01-06',
name: '周婷',
nameColor: 'pink',
category: '购物',
amount: 1200
},
{
date: '2021-01-07',
name: '吴磊',
nameColor: 'light-purple',
category: '交通',
amount: 300
},
{
date: '2021-01-08',
name: '张伟',
nameColor: 'purple',
category: '餐饮',
amount: 600
},
{
date: '2021-01-09',
name: '王强',
nameColor: 'green',
category: '购物',
amount: 900
}
])
//
const remittancesData = ref<ConsumptionRecord[]>([
{
date: '2021-01-02',
name: '张伟',
nameColor: 'purple',
category: '手机银行',
amount: 1000
},
{
date: '2021-01-03',
name: '王强',
nameColor: 'green',
category: '手机银行',
amount: 500
},
{
date: '2021-01-04',
name: '陈丽',
nameColor: 'blue',
category: '手机银行',
amount: 200
},
{
date: '2021-01-05',
name: '赵敏',
nameColor: 'teal',
category: '手机银行',
amount: 800
},
{
date: '2021-01-06',
name: '周婷',
nameColor: 'pink',
category: '手机银行',
amount: 1200
},
{
date: '2021-01-07',
name: '吴磊',
nameColor: 'light-purple',
category: '手机银行',
amount: 300
},
{
date: '2021-01-08',
name: '张伟一',
nameColor: 'purple',
category: '手机银行',
amount: 600
},
{
date: '2021-01-09',
name: '王强',
nameColor: 'green',
category: '手机银行',
amount: 900
}
])
// props
const props = withDefaults(
defineProps<{
data?: ConsumptionRecord[]
relatData?: RelationshipRecord[]
remitData?: ConsumptionRecord[]
}>(),
{
data: () => [],
relatData: () => [],
remitData: () => []
}
)
// 使
if (props.data && props.data.length > 0) {
recordsData.value = props.data
}
if (props.relatData && props.relatData.length > 0) {
relationshipData.value = props.relatData
}
if (props.remitData && props.remitData.length > 0) {
remittancesData.value = props.remitData
}
</script>
<style scoped lang="scss">
.consumption-records-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
gap: 10px;
overflow: hidden;
border-radius: 8px;
padding: 12px 8px;
border: 1px solid rgba(56, 102, 141, 0.3);
}
.consumption-records-title {
text-align: center;
font-size: 10px;
font-weight: bold;
color: #ffffff;
}
.consumption-records-content {
width: 100%;
flex: 1;
display: flex;
gap: 16px;
overflow: hidden;
}
.consumption-records {
width: 50%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
//
.records-header {
flex-shrink: 0;
padding: 8px;
background: #2c3d7e;
border-radius: 10px 10px 0 0;
display: flex;
align-items: center;
}
.header-title {
font-size: 9px;
font-weight: bold;
color: #ffffff;
}
//
.records-list {
flex: 1 1 0;
min-height: 0;
overflow-y: auto;
padding: 8px 0;
background: #22283a;
&::-webkit-scrollbar {
width: 0px;
}
border-radius: 0 0 10px 10px;
}
//
.record-item {
display: grid;
grid-template-columns: 1.4fr 0.8fr 1fr 0.8fr;
padding: 2px 8px;
border-bottom: 1px solid rgba(56, 102, 141, 0.15);
align-items: center;
&:last-child {
border-bottom: none;
}
}
.record-date {
font-size: 7px;
color: rgba(255, 255, 255, 0.9);
}
.record-name {
font-size: 7px;
font-weight: 500;
&.name-purple {
color: #a855f7;
}
&.name-green {
color: #10c896;
}
&.name-blue {
color: #3b82f6;
}
&.name-teal {
color: #14b8a6;
}
&.name-pink {
color: #ec4899;
}
&.name-light-purple {
color: #c084fc;
}
}
.record-category {
font-size: 7px;
color: rgba(255, 255, 255, 0.9);
}
.record-amount {
font-size: 7px;
font-weight: 500;
color: white;
text-align: right;
}
.consumption-records-relationship {
display: flex;
justify-content: space-around;
}
.relationship-item {
background: #422b1f;
padding: 4px 8px;
border-radius: 4px;
display: flex;
align-items: center;
gap: 2px;
&.relate-orange {
background: #422b1f;
color: #ffa500;
}
&.relate-green {
background: #1d3056;
color: #00ffff;
}
&.relate-yellow {
background: #453f26;
color: #ffff00;
}
&.relate-purple {
background: #3a2679;
color: #ef9efa;
}
}
.relationship-icon {
display: flex;
align-items: center;
flex-shrink: 0;
margin-top: 1.5px;
svg {
width: 8px;
height: 9px;
}
&.icon-orange {
color: #ffa500;
}
&.icon-green {
color: #00ffff;
}
&.icon-yellow {
color: #ffff00;
}
&.icon-purple {
color: #ef9efa;
}
}
.relationship-name {
font-size: 7px;
}
.relationship-relate {
font-size: 7px;
}
</style>

View File

@ -0,0 +1,147 @@
<template>
<div class="gauge-chart-wrapper">
<EChart :options="gaugeOption" :height="height" :width="width" />
<img :src="personIcon" class="center-icon" alt="person" />
</div>
</template>
<script setup lang="ts">
import type { EChartsOption } from 'echarts'
import * as echarts from 'echarts/core'
// @ts-ignore
import EChart from '@/components/Echart/src/Echart.vue'
import { computed, watch } from 'vue'
import personIcon from '@/assets/imgs/dashboard/person.svg?url'
defineOptions({ name: 'GaugeChart' })
const props = withDefaults(
defineProps<{
width?: string
height?: string
value?: number
name?: string
min?: number
max?: number
}>(),
{
width: '100%',
height: '300px',
value: 70,
name: '',
min: 0,
max: 100
}
)
//
const gaugeOption = computed(() => ({
series: [
{
type: 'gauge',
startAngle: 210,
endAngle: -30,
min: props.min,
max: props.max,
splitNumber: 10,
axisLine: {
lineStyle: {
width: 15,
color: [
[
0.7,
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{
offset: 0,
color: '#89C6C9'
},
{
offset: 0.2,
color: '#2BC6E6'
},
{
offset: 0.6,
color: '#E4BA86'
},
{
offset: 1,
color: '#D080A9'
}
])
],
[1, '#440F6580']
]
}
},
pointer: {
show: false
},
axisTick: {
length: 8,
distance: -40,
lineStyle: {
color: 'auto',
width: 1
}
},
splitLine: {
length: 14,
distance: -44,
lineStyle: {
color: 'auto',
width: 2
}
},
axisLabel: {
show: false
},
title: {
offsetCenter: [0, '-20%'],
fontSize: 20
},
detail: {
fontSize: 28,
offsetCenter: [0, '75%'],
valueAnimation: true,
formatter: function (value: number) {
return Math.round(value) + ''
},
color: '#1FBFF1'
},
data: [
{
value: props.value,
name: props.name
}
]
}
]
}))
//
watch(
() => [props.value, props.name, props.min, props.max],
() => {
// computed
}
)
</script>
<style scoped lang="scss">
.gauge-chart-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.center-icon {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -40%);
width: 50%;
height: 50%;
z-index: 100;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,294 @@
<template>
<div class="info-card-container">
<!-- 基本信息区域 -->
<div class="info-section">
<div class="section-header">
<span class="header-title">基本信息</span>
<div class="header-icon"></div>
</div>
<div class="info-content">
<div class="info-tag">{{ basicInfo.district }}</div>
<div class="info-tag">狱政编号: {{ basicInfo.prisonNumber }}</div>
<div class="info-tag">
刑期起/止日{{ basicInfo.sentenceStart }}---{{ basicInfo.sentenceEnd }} {{
basicInfo.sentenceDays
}}
</div>
</div>
</div>
<div class="info-detail">
<div class="info-list">
<div class="info-item">
<span class="info-label">年龄:</span>
<span class="info-value">{{ basicInfo.age }}</span>
</div>
<div class="info-item">
<span class="info-label">籍贯:</span>
<span class="info-value">{{ basicInfo.hometown }}</span>
</div>
<div class="info-item">
<span class="info-label">文化程度:</span>
<span class="info-value">{{ basicInfo.education }}</span>
</div>
<div class="info-item">
<span class="info-label">婚姻:</span>
<span class="info-value">{{ basicInfo.maritalStatus }}</span>
</div>
<div class="info-item">
<span class="info-label">生育:</span>
<span class="info-value">{{ basicInfo.children }}</span>
</div>
<div class="info-item">
<span class="info-label">出生日期:</span>
<span class="info-value">{{ basicInfo.birthDate }}</span>
</div>
<div class="info-item">
<span class="info-label">犯罪类型:</span>
<span class="info-value">{{ basicInfo.crimeType }}</span>
</div>
<div class="info-item">
<span class="info-label">前科次数:</span>
<span class="info-value">{{ basicInfo.previousConvictions }}</span>
</div>
<div class="info-item">
<span class="info-label">刑期:</span>
<span class="info-value">{{ basicInfo.sentence }}</span>
</div>
</div>
<div class="records-content">
<div class="records-content-title">心理访谈记录</div>
<div v-for="(record, index) in interviewRecords" :key="index" class="record-item">
<div class="record-bullet"></div>
<div class="record-content">
<div class="record-date">{{ record.date }}</div>
<div class="record-text">{{ record.content }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineOptions({ name: 'InfoCard' })
interface BasicInfo {
district: string
prisonNumber: string
sentenceStart: string
sentenceEnd: string
sentenceDays: number
age: number
hometown: string
education: string
maritalStatus: string
children: string
birthDate: string
crimeType: string
previousConvictions: string
sentence: string
}
interface InterviewRecord {
date: string
content: string
}
const props = withDefaults(
defineProps<{
basicInfo?: BasicInfo
interviewRecords?: InterviewRecord[]
}>(),
{
basicInfo: () => ({
district: '第1监区 十四分监区',
prisonNumber: 'ZF20230001',
sentenceStart: '2020/07/21',
sentenceEnd: '2020/07/21',
sentenceDays: 300,
age: 78,
hometown: '中国/江苏',
education: '大学',
maritalStatus: '未婚',
children: '无子女',
birthDate: '2025-02-02',
crimeType: '盗窃罪',
previousConvictions: '7次',
sentence: '有期徒刑1年6月'
}),
interviewRecords: () => [
{
date: '2023-10',
content: '情绪稳定,遵规守纪良好'
},
{
date: '2023-09',
content: '人际关系融洽,态度积极'
},
{
date: '2023-08',
content: '偶有焦虑情绪,需关注'
},
{
date: '2023-07',
content: '偶有焦虑情绪,需关注'
}
]
}
)
</script>
<style scoped lang="scss">
.info-card-container {
width: 100%;
height: 100%;
background: rgba(13, 30, 50, 0.9);
border-radius: 10px;
padding: 4px 8px;
gap: 24px;
border: 1px solid rgba(56, 102, 141, 0.3);
}
.info-section {
flex: 1;
display: flex;
flex-direction: column;
}
.interview-records {
padding-left: 24px;
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.header-title {
font-size: 8px;
margin-right: 2px;
color: white;
}
.header-icon {
width: 12px;
height: 12px;
background: url('@/assets/imgs/dashboard/pie.svg') no-repeat center center;
background-size: 100% 100%;
}
.info-content {
display: flex;
justify-content: center;
flex-direction: row;
flex-wrap: wrap;
gap: 6px;
}
.info-tag {
padding: 4px 6px;
background: #3f6973;
border: 1px solid rgba(56, 102, 141, 0.5);
border-radius: 2px;
font-size: 5px;
color: #d8f0ff;
white-space: nowrap;
}
.info-detail {
display: flex;
gap: 2px;
margin-top: 8px;
}
.records-content-title {
font-size: 6px;
color: #d8f0ff;
}
.info-list {
display: flex;
flex-direction: column;
gap: 2.5px;
}
.info-item {
display: flex;
align-items: center;
font-size: 6px;
color: white;
}
.info-label {
color: white;
margin-right: 4px;
min-width: 35px;
}
.info-value {
color: white;
flex: 1;
}
.records-content {
display: flex;
flex-direction: column;
gap: 2px;
height: 105px;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
}
.record-item {
display: flex;
gap: 3px;
position: relative;
}
.record-bullet {
width: 5px;
height: 5px;
border-radius: 50%;
background: #10a0f2;
flex-shrink: 0;
margin-top: 4px;
position: relative;
&::after {
content: '';
position: absolute;
top: 7px;
left: 50%;
transform: translateX(-50%);
width: 2px;
height: calc(100% + 4px);
background: rgba(56, 102, 141, 0.5);
}
}
.record-item:last-child .record-bullet::after {
display: none;
}
.record-content {
flex: 1;
padding-bottom: 3px;
}
.record-date {
font-size: 6px;
font-weight: 600;
color: white;
margin-bottom: 1px;
}
.record-text {
font-size: 6px;
color: #d8f0ff;
}
</style>

View File

@ -0,0 +1,250 @@
<template>
<div class="rewards-punishments-container">
<!-- 标题栏 -->
<div class="rewards-header">
<span class="header-title">近期奖惩</span>
<div class="filter-tabs">
<div
v-for="tab in filterTabs"
:key="tab.value"
class="filter-tab"
:class="{ active: activeFilter === tab.value }"
@click="activeFilter = tab.value"
>
{{ tab.label }}
</div>
</div>
</div>
<!-- 时间线列表 -->
<div class="timeline-container">
<div class="timeline-content">
<div class="timeline-line"></div>
<div class="timeline-items">
<div v-for="(item, index) in filteredList" :key="index" class="timeline-item">
<div class="timeline-dot" :class="item.type"></div>
<div class="timeline-card">
<div class="card-type" :class="item.type">{{ item.typeText }}</div>
<div class="card-description">{{ item.description }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
defineOptions({ name: 'RecentRewardsPunishments' })
interface RewardPunishmentItem {
type: 'reward' | 'punishment'
typeText: string
description: string
}
//
const filterTabs = [
{ label: '全部', value: 'all' },
{ label: '奖励记录', value: 'reward' },
{ label: '惩罚记录', value: 'punishment' }
]
const activeFilter = ref<string>('all')
//
const listData = ref<RewardPunishmentItem[]>([
{
type: 'reward',
typeText: '表扬奖励',
description: '因积极参加劳动改造,表现突出,获得监区表扬。'
},
{
type: 'reward',
typeText: '表扬奖励',
description: '因积极参加劳动改造,表现突出,获得监区表扬。'
},
{
type: 'punishment',
typeText: '警告',
description: '因积极参加劳动改造,表现突出,获得监区表扬。'
},
{
type: 'reward',
typeText: '表扬奖励',
description: '因积极参加劳动改造,表现突出,获得监区表扬。'
},
{
type: 'punishment',
typeText: '扣分',
description: '因积极参加劳动改造,表现突出,获得监区表扬。'
}
])
//
const filteredList = computed(() => {
if (activeFilter.value === 'all') {
return listData.value
} else if (activeFilter.value === 'reward') {
return listData.value.filter((item) => item.type === 'reward')
} else {
return listData.value.filter((item) => item.type === 'punishment')
}
})
// props
const props = withDefaults(
defineProps<{
data?: RewardPunishmentItem[]
}>(),
{
data: () => []
}
)
// 使
if (props.data && props.data.length > 0) {
listData.value = props.data
}
</script>
<style scoped lang="scss">
.rewards-punishments-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid rgba(56, 102, 141, 0.5);
border-radius: 8px;
padding: 12px;
}
//
.rewards-header {
flex-shrink: 0;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.header-title {
font-size: 10px;
font-weight: bold;
color: #ffffff;
}
.filter-tabs {
display: flex;
gap: 2px;
}
.filter-tab {
padding: 2px 4px;
font-size: 7px;
color: rgba(255, 255, 255, 0.85);
background: rgba(56, 102, 141, 0.2);
border-radius: 2px;
&.active {
background: #37599d;
color: #ffffff;
}
}
// 线
.timeline-container {
flex: 1 1 0;
min-height: 0;
overflow-y: auto;
padding-left: 6px;
&::-webkit-scrollbar {
width: 0px;
}
}
.timeline-content {
position: relative;
padding-bottom: 8px;
}
// 线
.timeline-line {
position: absolute;
left: -2px;
top: 0;
bottom: 0;
width: 1px;
background: #5e7fef;
}
.timeline-items {
position: relative;
}
// 线
.timeline-item {
position: relative;
display: flex;
align-items: flex-start;
margin-bottom: 4px;
}
// 线
.timeline-dot {
position: absolute;
left: -5px;
top: 50%;
transform: translateY(-50%);
width: 7px;
height: 7px;
border-radius: 50%;
border: 2px solid rgba(13, 30, 50, 0.8);
background: rgba(13, 30, 50, 0.8);
z-index: 1;
&.reward {
background: #10b981;
border-color: #10b981;
}
&.punishment {
background: #ff0000;
border-color: #ff0000;
}
}
//
.timeline-card {
flex: 1;
background: rgba(56, 102, 141, 0.15);
border: 1px solid rgba(56, 102, 141, 0.3);
border-radius: 2px;
padding: 4px 8px;
}
.card-type {
font-size: 7px;
font-weight: 500;
margin-bottom: 2px;
color: rgba(255, 255, 255, 0.9);
&.reward {
color: #10b981;
}
&.punishment {
color: #ff0000;
}
}
.card-description {
font-size: 5px;
color: rgba(255, 255, 255, 0.7);
line-height: 1.4;
}
</style>

View File

@ -0,0 +1,321 @@
<template>
<div class="score-assessment-container">
<!-- 标题栏 -->
<div class="score-header">
<span class="header-title">计分考核</span>
<div class="header-icon"></div>
</div>
<!-- 内容区域 -->
<div class="score-content">
<!-- 表头 -->
<div class="score-table-header">
<div class="header-cell">
<div class="required-star"></div>
<span>考核年月</span>
</div>
<div class="header-cell">/</div>
<div class="header-cell">最终分</div>
<div class="header-cell">等级</div>
</div>
<!-- 数据列表 -->
<div class="score-table-body">
<div v-for="(item, index) in scoreData" :key="index" class="score-table-row">
<div class="row-cell cell-date">
<div class="row-icon"></div>
<div>{{ item.date }}</div>
</div>
<div class="row-cell cell-score" :class="item.scoreType">
{{ item.score }}
</div>
<div class="row-cell cell-final">{{ item.finalScore }}</div>
<div class="row-cell cell-level">
<span class="level-badge" :class="`level-${item.level}`">
{{ item.levelText }}
</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
defineOptions({ name: 'ScoreAssessment' })
interface ScoreItem {
date: string
score: string
scoreType: 'positive' | 'negative'
finalScore: number
level: 'excellent' | 'good' | 'poor'
levelText: string
}
//
const scoreData = ref<ScoreItem[]>([
{
date: '2023-05',
score: '+5',
scoreType: 'positive',
finalScore: 88,
level: 'excellent',
levelText: '优秀'
},
{
date: '2023-05',
score: '-5',
scoreType: 'negative',
finalScore: 88,
level: 'good',
levelText: '良好'
},
{
date: '2023-05',
score: '+5',
scoreType: 'positive',
finalScore: 88,
level: 'excellent',
levelText: '优秀'
},
{
date: '2023-05',
score: '-5',
scoreType: 'negative',
finalScore: 88,
level: 'good',
levelText: '良好'
},
{
date: '2023-05',
score: '+5',
scoreType: 'positive',
finalScore: 88,
level: 'excellent',
levelText: '优秀'
},
{
date: '2023-05',
score: '-5',
scoreType: 'negative',
finalScore: 88,
level: 'poor',
levelText: '较差'
},
{
date: '2023-05',
score: '-5',
scoreType: 'negative',
finalScore: 88,
level: 'poor',
levelText: '较差'
},
{
date: '2023-05',
score: '-5',
scoreType: 'negative',
finalScore: 88,
level: 'poor',
levelText: '较差'
},
{
date: '2023-05',
score: '-5',
scoreType: 'negative',
finalScore: 88,
level: 'poor',
levelText: '较差'
},
{
date: '2023-05',
score: '-5',
scoreType: 'negative',
finalScore: 88,
level: 'poor',
levelText: '较差'
},
{
date: '2023-05',
score: '-5',
scoreType: 'negative',
finalScore: 88,
level: 'poor',
levelText: '较差'
},
{
date: '2023-05',
score: '+5',
scoreType: 'positive',
finalScore: 88,
level: 'excellent',
levelText: '优秀'
}
])
// props
const props = withDefaults(
defineProps<{
data?: ScoreItem[]
}>(),
{
data: () => []
}
)
// 使
if (props.data && props.data.length > 0) {
scoreData.value = props.data
}
</script>
<style scoped lang="scss">
.score-assessment-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 10px;
padding: 4px 8px 16px;
border: 1px solid rgba(56, 102, 141, 0.3);
}
//
.score-header {
flex-shrink: 0;
display: flex;
align-items: center;
margin-bottom: 12px;
}
.header-title {
font-size: 8px;
margin-right: 2px;
color: white;
}
.header-icon {
width: 12px;
height: 12px;
background: url('@/assets/imgs/dashboard/pie.svg') no-repeat center center;
background-size: 100% 100%;
}
//
.score-content {
flex: 1 1 0;
// min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
//
.score-table-header {
flex: 0 0 auto;
display: grid;
grid-template-columns: 1.5fr 1fr 1fr 1fr;
padding: 4px 2px;
}
.header-cell {
font-size: 6px;
color: rgba(255, 255, 255, 0.85);
font-weight: 500;
display: flex;
align-items: center;
}
.required-star {
background: url('@/assets/imgs/dashboard/icon-require.svg') no-repeat center center;
background-size: 100% 100%;
width: 7px;
height: 7px;
margin-right: 2px;
}
//
.score-table-body {
flex: 1 1 0;
// min-height: 0;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0px;
}
}
.score-table-row {
display: grid;
grid-template-columns: 1.5fr 1fr 1fr 1fr;
padding: 2px;
}
.row-icon {
background: url('@/assets/imgs/dashboard/icon-arrow.svg') no-repeat center center;
background-size: 100% 100%;
width: 7px;
height: 7px;
margin-right: 2px;
}
.row-cell {
font-size: 6px;
color: #ffffff;
display: flex;
align-items: center;
}
.cell-date {
color: #ffffff;
font-weight: 400;
}
.cell-score {
&.positive {
color: #10c896;
text-shadow: 0 0 4px rgba(16, 200, 150, 0.3);
}
&.negative {
color: #ff6b6b;
text-shadow: 0 0 4px rgba(255, 107, 107, 0.3);
}
}
.cell-final {
color: #ffd700;
text-shadow: 0 0 4px rgba(255, 215, 0, 0.3);
}
.cell-level {
display: flex;
align-items: center;
}
.level-badge {
display: inline-block;
padding: 1px 4px;
border-radius: 2px;
color: #ffffff;
white-space: nowrap;
&.level-excellent {
background: #51869290;
border: 1px solid #518692;
}
&.level-good {
background: #47363390;
border: 1px solid #473633;
}
&.level-poor {
background: #33131f90;
border: 1px solid #33131f;
}
}
</style>

2
types/env.d.ts vendored
View File

@ -1,7 +1,7 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare module '*.vue' { declare module '*.vue' {
import { DefineComponent } from 'vue' import type { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any> const component: DefineComponent<{}, {}, any>
export default component export default component