Commit 4f4750130ff3f959133574da475f80d5d99d791a

Authored by 刘淇
1 parent 03b006dc

k线图

api/user.js
@@ -49,3 +49,26 @@ export const getSimpleDictDataList = () => { @@ -49,3 +49,26 @@ export const getSimpleDictDataList = () => {
49 export const refreshToken = (params) => { 49 export const refreshToken = (params) => {
50 return post('/admin-api/system/auth/refresh-token?refreshToken='+params) 50 return post('/admin-api/system/auth/refresh-token?refreshToken='+params)
51 } 51 }
  52 +
  53 +
  54 +/**
  55 + * 获得当前用户的未读站内信数量
  56 + * @returns {Promise}
  57 + */
  58 +export const getMsg = () => {
  59 + return get('/app-api/system/notify-message/my-page')
  60 +}
  61 +
  62 +/**
  63 + * 获得当前用户的未读站内信数量
  64 + * @returns {Promise}
  65 + */
  66 +export const getUnreadCount = () => {
  67 + return get('/app-api/system/notify-message/get-unread-count')
  68 +}
  69 +
  70 +
  71 +
  72 +
  73 +
  74 +
components/uni-charts/index.js 0 → 100644
  1 +import uniCharts from './uni-charts.vue'
  2 +export default uniCharts
0 \ No newline at end of file 3 \ No newline at end of file
components/uni-charts/package.json 0 → 100644
  1 +{
  2 + "name": "uni-charts",
  3 + "version": "1.0.0",
  4 + "description": "UniApp K线图组件",
  5 + "main": "index.js",
  6 + "keywords": ["uni-charts", "kline", "小程序"],
  7 + "author": "",
  8 + "license": "MIT"
  9 +}
0 \ No newline at end of file 10 \ No newline at end of file
components/uni-charts/uni-charts.vue 0 → 100644
  1 +<template>
  2 + <view class="uni-charts">
  3 + <canvas
  4 + v-if="type !== 'map'"
  5 + class="charts-canvas"
  6 + :style="{width: width + 'px', height: height + 'px'}"
  7 + canvas-id="uni-charts"
  8 + @touchstart="touchStart"
  9 + @touchmove="touchMove"
  10 + @touchend="touchEnd"
  11 + ></canvas>
  12 + <view v-else class="map-container" :style="{width: width + 'px', height: height + 'px'}">
  13 + <slot name="map"></slot>
  14 + </view>
  15 + </view>
  16 +</template>
  17 +
  18 +<script>
  19 +export default {
  20 + name: 'uniCharts',
  21 + props: {
  22 + type: {
  23 + type: String,
  24 + default: 'line'
  25 + },
  26 + data: {
  27 + type: Array,
  28 + default () {
  29 + return []
  30 + }
  31 + },
  32 + categories: {
  33 + type: Array,
  34 + default () {
  35 + return []
  36 + }
  37 + },
  38 + option: {
  39 + type: Object,
  40 + default () {
  41 + return {}
  42 + }
  43 + },
  44 + width: {
  45 + type: [Number, String],
  46 + default: 375
  47 + },
  48 + height: {
  49 + type: [Number, String],
  50 + default: 200
  51 + }
  52 + },
  53 + data () {
  54 + return {
  55 + ctx: null,
  56 + chartData: {},
  57 + touchInfo: {}
  58 + }
  59 + },
  60 + watch: {
  61 + data: {
  62 + deep: true,
  63 + handler () {
  64 + this.initChart()
  65 + }
  66 + },
  67 + categories: {
  68 + deep: true,
  69 + handler () {
  70 + this.initChart()
  71 + }
  72 + },
  73 + option: {
  74 + deep: true,
  75 + handler () {
  76 + this.initChart()
  77 + }
  78 + }
  79 + },
  80 + mounted () {
  81 + this.initChart()
  82 + },
  83 + methods: {
  84 + initChart () {
  85 + if (this.type === 'kline') {
  86 + this.drawKline()
  87 + } else if (this.type === 'line') {
  88 + this.drawLine()
  89 + }
  90 + },
  91 + // 折线图绘制(终极版:Y轴整数 + 自适应极值 + 修复文字折叠 + 优化图例间距)
  92 + drawLine () {
  93 + if (!this.data.length || !this.categories.length) return;
  94 +
  95 + const ctx = uni.createCanvasContext('uni-charts', this)
  96 + this.ctx = ctx
  97 + const { width, height } = this
  98 + const {
  99 + grid = {},
  100 + xAxis = {},
  101 + yAxis = {},
  102 + legend = {},
  103 + color = ['#25AF69', '#B34C17'],
  104 + lineSmooth = true
  105 + } = this.option
  106 +
  107 + // 清空画布
  108 + ctx.clearRect(0, 0, width, height)
  109 +
  110 + // 调整Grid布局,彻底解决重叠
  111 + const gridTop = grid.top ? (typeof grid.top === 'string' ? parseFloat(grid.top) / 100 * height : grid.top) : 60
  112 + const gridLeft = grid.left ? (typeof grid.left === 'string' ? parseFloat(grid.left) / 100 * width : grid.left) : 70
  113 + const gridRight = grid.right ? (typeof grid.right === 'string' ? parseFloat(grid.right) / 100 * width : grid.right) : 20
  114 + const gridBottom = grid.bottom ? (typeof grid.bottom === 'string' ? parseFloat(grid.bottom) / 100 * height : grid.bottom) : 50
  115 +
  116 + const drawWidth = width - gridLeft - gridRight
  117 + const drawHeight = height - gridTop - gridBottom
  118 +
  119 + // Y轴最大值自适应(无小数点)
  120 + let allValues = []
  121 + this.data.forEach(series => {
  122 + allValues = allValues.concat(series.data)
  123 + })
  124 + if (allValues.length === 0) return;
  125 +
  126 + // 动态计算Y轴极值(保留10%顶部余量,转为整数)
  127 + const rawMaxVal = Math.max(...allValues)
  128 + const rawMinVal = Math.min(...allValues)
  129 + const maxVal = yAxis.max || Math.ceil(rawMaxVal * 1.1) // 向上取整,保证能容纳最大值
  130 + const minVal = yAxis.min || (rawMinVal < 0 ? Math.floor(rawMinVal * 1.1) : 0) // 向下取整(负数),默认0
  131 + const valRange = maxVal - minVal || 1
  132 +
  133 + // 计算X轴每个点的宽度
  134 + const xStep = drawWidth / (this.categories.length - 1 || 1)
  135 +
  136 + // X轴标签防拥挤
  137 + const minLabelWidth = 30
  138 + const maxShowLabels = Math.floor(drawWidth / minLabelWidth)
  139 + const labelInterval = maxShowLabels < this.categories.length
  140 + ? Math.ceil(this.categories.length / maxShowLabels)
  141 + : 1
  142 +
  143 + // 绘制网格线 + Y轴整数刻度
  144 + ctx.setStrokeStyle(yAxis.splitLine?.lineStyle?.color || '#f5f5f7')
  145 + ctx.setLineWidth(1)
  146 + const yTickCount = 5
  147 + const yTickStep = valRange / yTickCount
  148 +
  149 + for (let i = 0; i <= yTickCount; i++) {
  150 + const y = gridTop + drawHeight - (i * drawHeight / yTickCount)
  151 + // 绘制网格线
  152 + ctx.beginPath()
  153 + ctx.moveTo(gridLeft, y)
  154 + ctx.lineTo(width - gridRight, y)
  155 + ctx.stroke()
  156 +
  157 + // ========== 核心修改:Y轴显示整数(无小数点) ==========
  158 + ctx.setFillStyle(yAxis.axisLabel?.color || '#666')
  159 + ctx.setFontSize(yAxis.axisLabel?.fontSize || 12)
  160 + const val = minVal + (i * yTickStep)
  161 + // 转为整数(四舍五入),彻底去掉小数点
  162 + const intVal = Math.round(val)
  163 + let valText = intVal.toString()
  164 + // 长数字处理(仍为整数格式)
  165 + if (valText.length > 6) {
  166 + valText = intVal.toLocaleString() // 用千分位显示长整数,如 1234567 → 1,234,567
  167 + }
  168 + // 文字右对齐,避免折叠
  169 + const textWidth = ctx.measureText(valText).width
  170 + ctx.fillText(valText, gridLeft - 10 - textWidth, y + 5)
  171 + }
  172 +
  173 + // 绘制X轴标签
  174 + ctx.setFillStyle(xAxis.axisLabel?.color || '#666')
  175 + ctx.setFontSize(xAxis.axisLabel?.fontSize || 12)
  176 + this.categories.forEach((text, index) => {
  177 + if (index % labelInterval === 0) {
  178 + const x = gridLeft + index * xStep
  179 + let labelText = text
  180 + if (text.length > 6) {
  181 + labelText = text.slice(0, 6) + '...'
  182 + }
  183 + const textLines = labelText.split('\n')
  184 +
  185 + textLines.forEach((line, lineIdx) => {
  186 + const textWidth = ctx.measureText(line).width
  187 + ctx.fillText(line, x - textWidth / 2, height - gridBottom + 20 + (lineIdx * 12))
  188 + })
  189 + }
  190 + })
  191 +
  192 + // 优化图例间距
  193 + if (legend.show) {
  194 + ctx.setFontSize(legend.textStyle?.fontSize || 12)
  195 + let legendX = gridLeft + 10
  196 + this.data.forEach((series, idx) => {
  197 + let seriesName = series.name || `系列${idx + 1}`
  198 + if (seriesName.length > 8) {
  199 + seriesName = seriesName.slice(0, 8) + '...'
  200 + }
  201 + const textWidth = ctx.measureText(seriesName).width
  202 + const legendItemWidth = textWidth + 25
  203 +
  204 + const legendY = gridTop - 30
  205 + ctx.setFillStyle(series.color || color[idx % color.length])
  206 + ctx.fillRect(legendX, legendY, 10, 10)
  207 + ctx.setFillStyle('#666')
  208 + ctx.fillText(seriesName, legendX + 15, legendY + 8)
  209 +
  210 + legendX += Math.max(80, legendItemWidth + 10)
  211 + })
  212 + }
  213 +
  214 + // 绘制平滑折线(无数据点)
  215 + this.data.forEach((series, seriesIdx) => {
  216 + const seriesColor = series.color || color[seriesIdx % color.length]
  217 +
  218 + ctx.setStrokeStyle(seriesColor)
  219 + ctx.setLineWidth(2)
  220 + ctx.beginPath()
  221 +
  222 + series.data.forEach((value, index) => {
  223 + const x = gridLeft + index * xStep
  224 + const y = gridTop + drawHeight - ((value - minVal) / valRange) * drawHeight
  225 +
  226 + if (index === 0) {
  227 + ctx.moveTo(x, y)
  228 + } else {
  229 + if (lineSmooth && index < series.data.length - 1) {
  230 + const prevX = gridLeft + (index - 1) * xStep
  231 + const prevY = gridTop + drawHeight - ((series.data[index - 1] - minVal) / valRange) * drawHeight
  232 + const nextX = gridLeft + (index + 1) * xStep
  233 + const nextY = gridTop + drawHeight - ((series.data[index + 1] - minVal) / valRange) * drawHeight
  234 +
  235 + const cp1x = (prevX + x) / 2
  236 + const cp1y = prevY
  237 + const cp2x = (x + nextX) / 2
  238 + const cp2y = y
  239 +
  240 + ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
  241 + } else {
  242 + ctx.lineTo(x, y)
  243 + }
  244 + }
  245 + })
  246 +
  247 + ctx.stroke()
  248 + })
  249 +
  250 + ctx.draw()
  251 + },
  252 + // K线图绘制(同步修改Y轴为整数)
  253 + drawKline () {
  254 + const ctx = uni.createCanvasContext('uni-charts', this)
  255 + this.ctx = ctx
  256 + const { width, height } = this
  257 + const { grid = {}, xAxis = {}, yAxis = {}, color = ['#B34C17', '#25AF69'] } = this.option
  258 +
  259 + ctx.clearRect(0, 0, width, height)
  260 +
  261 + // 调整Grid布局
  262 + const gridTop = grid.top ? (typeof grid.top === 'string' ? parseFloat(grid.top) / 100 * height : grid.top) : 50
  263 + const gridLeft = grid.left ? (typeof grid.left === 'string' ? parseFloat(grid.left) / 100 * width : grid.left) : 70
  264 + const gridRight = grid.right ? (typeof grid.right === 'string' ? parseFloat(grid.right) / 100 * width : grid.right) : 20
  265 + const gridBottom = grid.bottom ? (typeof grid.bottom === 'string' ? parseFloat(grid.bottom) / 100 * height : grid.bottom) : 40
  266 +
  267 + const drawWidth = width - gridLeft - gridRight
  268 + const drawHeight = height - gridTop - gridBottom
  269 +
  270 + // Y轴自适应极值(整数)
  271 + let allValues = []
  272 + this.data.forEach(item => {
  273 + allValues = allValues.concat([item.open, item.high, item.low, item.close])
  274 + })
  275 + if (allValues.length === 0) return;
  276 +
  277 + const rawMaxVal = Math.max(...allValues)
  278 + const rawMinVal = Math.min(...allValues)
  279 + const maxVal = yAxis.max || Math.ceil(rawMaxVal * 1.1)
  280 + const minVal = yAxis.min || (rawMinVal < 0 ? Math.floor(rawMinVal * 1.1) : 0)
  281 + const valRange = maxVal - minVal || 1
  282 +
  283 + const xStep = drawWidth / (this.data.length || 1)
  284 +
  285 + // 绘制网格线和Y轴整数刻度
  286 + ctx.setStrokeStyle('#f5f5f7')
  287 + ctx.setLineWidth(1)
  288 + const yTickCount = 5
  289 + for (let i = 0; i <= yTickCount; i++) {
  290 + const y = gridTop + drawHeight - (i * drawHeight / yTickCount)
  291 + ctx.beginPath()
  292 + ctx.moveTo(gridLeft, y)
  293 + ctx.lineTo(width - gridRight, y)
  294 + ctx.stroke()
  295 +
  296 + // Y轴显示整数
  297 + ctx.setFillStyle(yAxis.axisLabel?.color || '#666')
  298 + ctx.setFontSize(yAxis.axisLabel?.fontSize || 12)
  299 + const val = minVal + (i * valRange / yTickCount)
  300 + const intVal = Math.round(val)
  301 + let valText = intVal.toString()
  302 + if (valText.length > 6) valText = intVal.toLocaleString()
  303 + const textWidth = ctx.measureText(valText).width
  304 + ctx.fillText(valText, gridLeft - 10 - textWidth, y + 5)
  305 + }
  306 +
  307 + // X轴标签(防拥挤)
  308 + ctx.setFillStyle(xAxis.axisLabel?.color || '#666')
  309 + ctx.setFontSize(xAxis.axisLabel?.fontSize || 12)
  310 + const minLabelWidth = 30
  311 + const maxShowLabels = Math.floor(drawWidth / minLabelWidth)
  312 + const labelInterval = maxShowLabels < this.categories.length
  313 + ? Math.ceil(this.categories.length / maxShowLabels)
  314 + : 1
  315 +
  316 + this.categories.forEach((text, index) => {
  317 + if (index % labelInterval === 0) {
  318 + const x = gridLeft + index * xStep + xStep / 2
  319 + let labelText = text
  320 + if (text.length > 6) labelText = text.slice(0, 6) + '...'
  321 + ctx.fillText(labelText, x - ctx.measureText(labelText).width / 2, height - gridBottom + 20)
  322 + }
  323 + })
  324 +
  325 + // 绘制K线
  326 + this.data.forEach((item, index) => {
  327 + const { open, high, low, close } = item
  328 + const x = gridLeft + index * xStep + xStep / 2
  329 + const yOpen = gridTop + drawHeight - ((open - minVal) / valRange) * drawHeight
  330 + const yClose = gridTop + drawHeight - ((close - minVal) / valRange) * drawHeight
  331 + const yHigh = gridTop + drawHeight - ((high - minVal) / valRange) * drawHeight
  332 + const yLow = gridTop + drawHeight - ((low - minVal) / valRange) * drawHeight
  333 +
  334 + // 绘制高低线
  335 + ctx.setStrokeStyle(close >= open ? color[0] : color[1])
  336 + ctx.setLineWidth(1)
  337 + ctx.beginPath()
  338 + ctx.moveTo(x, yHigh)
  339 + ctx.lineTo(x, yLow)
  340 + ctx.stroke()
  341 +
  342 + // 绘制实体
  343 + const rectWidth = xStep / 3
  344 + ctx.setFillStyle(close >= open ? color[0] : color[1])
  345 + const rectY = Math.min(yOpen, yClose)
  346 + const rectHeight = Math.abs(yClose - yOpen) || 2
  347 + ctx.fillRect(x - rectWidth / 2, rectY, rectWidth, rectHeight)
  348 + })
  349 +
  350 + ctx.draw()
  351 + },
  352 + touchStart (e) {
  353 + this.touchInfo = {
  354 + x: e.changedTouches[0].x,
  355 + y: e.changedTouches[0].y,
  356 + time: Date.now()
  357 + }
  358 + },
  359 + touchMove (e) {
  360 + this.touchInfo.x = e.changedTouches[0].x
  361 + this.touchInfo.y = e.changedTouches[0].y
  362 + },
  363 + touchEnd (e) {
  364 + // 可扩展点击交互逻辑
  365 + }
  366 + }
  367 +}
  368 +</script>
  369 +
  370 +<style scoped>
  371 +.uni-charts {
  372 + position: relative;
  373 +}
  374 +.charts-canvas {
  375 + display: block;
  376 +}
  377 +.map-container {
  378 + position: relative;
  379 +}
  380 +</style>
0 \ No newline at end of file 381 \ No newline at end of file
pages-sub/data/tree-archive/editTree.vue
@@ -235,15 +235,15 @@ @@ -235,15 +235,15 @@
235 <script setup> 235 <script setup>
236 import { ref, reactive, nextTick } from 'vue' 236 import { ref, reactive, nextTick } from 'vue'
237 import { onReady, onLoad, onShow, onUnload } from '@dcloudio/uni-app'; 237 import { onReady, onLoad, onShow, onUnload } from '@dcloudio/uni-app';
238 -// 接口修改:删除addTree,新增 treeDetailReq + updateTree 238 +
239 import { treeRoadReq, treeLogReq, treeDetailReq, updateTree } from "@/api/tree-archive/tree-archive.js"; 239 import { treeRoadReq, treeLogReq, treeDetailReq, updateTree } from "@/api/tree-archive/tree-archive.js";
240 import { timeFormat } from '@/uni_modules/uview-plus'; 240 import { timeFormat } from '@/uni_modules/uview-plus';
241 import { useUploadImgs } from '@/common/utils/useUploadImgs' 241 import { useUploadImgs } from '@/common/utils/useUploadImgs'
242 import { useUserStore } from '@/pinia/user'; 242 import { useUserStore } from '@/pinia/user';
243 243
244 -// ========== 常量配置区 - 完全对标参考页写法 集中管理 ========== 244 +// ========== 常量配置区
245 const CONST = { 245 const CONST = {
246 - // 上传配置 统一抽离,和参考页一致 246 + // 上传配置 统一抽离
247 UPLOAD_CONFIG: { maxCount: 3, uploadText: '选择图片', sizeType: ['compressed'] } 247 UPLOAD_CONFIG: { maxCount: 3, uploadText: '选择图片', sizeType: ['compressed'] }
248 } 248 }
249 249
@@ -275,13 +275,13 @@ const treeLevelData = ref([]) @@ -275,13 +275,13 @@ const treeLevelData = ref([])
275 const showActionSheet = ref(false); 275 const showActionSheet = ref(false);
276 const currentActionSheetData = reactive({type: '', list: [], title: ''}); 276 const currentActionSheetData = reactive({type: '', list: [], title: ''});
277 277
278 -// ========== 核心修复:图片上传配置 - 1:1对标工单页面 无任何冗余 ========== 278 +// ========== 核心修复:图片上传配置 ==========
279 const treeImgList = useUploadImgs({ 279 const treeImgList = useUploadImgs({
280 ...CONST.UPLOAD_CONFIG, 280 ...CONST.UPLOAD_CONFIG,
281 formRef: formRef, 281 formRef: formRef,
282 fieldName: 'treeImgList' 282 fieldName: 'treeImgList'
283 }) 283 })
284 -// 初始化图片数组为纯净空数组,杜绝残留数据 284 +// 初始化图片数组为纯净空数组,杜绝残留数据
285 treeImgList.imgList.value = [] 285 treeImgList.imgList.value = []
286 treeImgList.rawImgList.value = [] 286 treeImgList.rawImgList.value = []
287 287
@@ -440,7 +440,6 @@ onLoad((options) =&gt; { @@ -440,7 +440,6 @@ onLoad((options) =&gt; {
440 onShow(async () => { 440 onShow(async () => {
441 treeLevelData.value = uni.$dict.transformLabelValueToNameValue(uni.$dict.getDictSimpleList('tree_level')) 441 treeLevelData.value = uni.$dict.transformLabelValueToNameValue(uni.$dict.getDictSimpleList('tree_level'))
442 treeOwnershipData.value = uni.$dict.transformLabelValueToNameValue(uni.$dict.getDictSimpleList('tree_ownership')) 442 treeOwnershipData.value = uni.$dict.transformLabelValueToNameValue(uni.$dict.getDictSimpleList('tree_ownership'))
443 - // ✅ 核心修复:只执行一次回显加载,彻底杜绝重复加载  
444 if (activeTab.value === 0 && treeId.value && !isInit.value) { 443 if (activeTab.value === 0 && treeId.value && !isInit.value) {
445 await getTreeDetail() 444 await getTreeDetail()
446 isInit.value = true // 加载完成后标记为true,永不重复执行 445 isInit.value = true // 加载完成后标记为true,永不重复执行
@@ -467,7 +466,7 @@ const treeRoadQuery = async () =&gt; { @@ -467,7 +466,7 @@ const treeRoadQuery = async () =&gt; {
467 rows.value = res.list 466 rows.value = res.list
468 } 467 }
469 468
470 -// ========== ✅ 终极修复:图片回显逻辑 - 彻底解决重复渲染4张图的问题 ========== 469 +// ==========
471 const getTreeDetail = async () => { 470 const getTreeDetail = async () => {
472 const res = await treeDetailReq({id: treeId.value}) 471 const res = await treeDetailReq({id: treeId.value})
473 Object.assign(formData, res) 472 Object.assign(formData, res)
@@ -475,7 +474,6 @@ const getTreeDetail = async () =&gt; { @@ -475,7 +474,6 @@ const getTreeDetail = async () =&gt; {
475 formData.oldtreeownershipText = uni.$dict.getDictLabel('tree_ownership', res.oldtreeownership) 474 formData.oldtreeownershipText = uni.$dict.getDictLabel('tree_ownership', res.oldtreeownership)
476 formData.treeleveltext = uni.$dict.getDictLabel('tree_level', formData.treelevel) 475 formData.treeleveltext = uni.$dict.getDictLabel('tree_level', formData.treelevel)
477 476
478 - // ✅ 第一步:强制清空为纯净空数组,杜绝任何残留  
479 treeImgList.imgList.value = [] 477 treeImgList.imgList.value = []
480 treeImgList.rawImgList.value = [] 478 treeImgList.rawImgList.value = []
481 formData.treephotoone = '' 479 formData.treephotoone = ''
@@ -485,7 +483,6 @@ const getTreeDetail = async () =&gt; { @@ -485,7 +483,6 @@ const getTreeDetail = async () =&gt; {
485 formData.treephotofour = '' 483 formData.treephotofour = ''
486 formData.treephotofive = '' 484 formData.treephotofive = ''
487 485
488 - // ✅ 第二步:接口返回几张就渲染几张,绝对不会重复  
489 if (Array.isArray(res.treeImgList) && res.treeImgList.length > 0) { 486 if (Array.isArray(res.treeImgList) && res.treeImgList.length > 0) {
490 const imgList = res.treeImgList.map((url, idx) => ({ 487 const imgList = res.treeImgList.map((url, idx) => ({
491 url, 488 url,
@@ -563,7 +560,7 @@ const submit = async () =&gt; { @@ -563,7 +560,7 @@ const submit = async () =&gt; {
563 // 表单校验 560 // 表单校验
564 const valid = await formRef.value.validate() 561 const valid = await formRef.value.validate()
565 if (!valid) return 562 if (!valid) return
566 - // 对标参考页:图片取值 统一调用封装方法 563 + // 图片取值 统一调用封装方法
567 const uploadImgUrls = treeImgList.getSuccessImgUrls() 564 const uploadImgUrls = treeImgList.getSuccessImgUrls()
568 formData.maintainunit = userStore.userInfo.user.companyId 565 formData.maintainunit = userStore.userInfo.user.companyId
569 formData.treeImgList = uploadImgUrls 566 formData.treeImgList = uploadImgUrls
pages-sub/msg/index.vue 0 → 100644
  1 +<template>
  2 + <view class="user-center-page">
  3 + <up-card
  4 + :border="false"
  5 + :foot-border-top="false"
  6 + v-for="(item,index) in orderList"
  7 + :key="`${item.orderNo}_${index}`"
  8 + :show-head="false"
  9 + >
  10 +
  11 + <template #body>
  12 + <view class="card-body">
  13 + <view class="u-flex common-item-center common-justify-between" style="font-size: 14px;margin-top: 5px;">
  14 + <view class="u-line-1" style="flex: 1;margin-right: 20px;color: #333"> {{ item.orderName || '无' }}</view>
  15 + <view style="color: #000">{{ timeFormat(item.createTime, 'yyyy-mm-dd hh:MM:ss') }}</view>
  16 + </view>
  17 + <view class="u-line-1 ">
  18 + {{ item.remark || '无' }}
  19 + </view>
  20 +
  21 +
  22 + </view>
  23 + </template>
  24 + </up-card>
  25 + </view>
  26 +</template>
  27 +
  28 +<script setup lang="ts">
  29 +import { computed,ref } from 'vue'
  30 +import { useUserStore } from '@/pinia/user'
  31 +import { onShow } from '@dcloudio/uni-app'
  32 +import { getMsg } from '@/api/user'
  33 +// 初始化Pinia仓库
  34 +const userStore = useUserStore()
  35 +import { timeFormat } from '@/uni_modules/uview-plus';
  36 +// 计算属性获取用户信息(响应式)
  37 +const userInfo = computed(() => userStore.userInfo.user || {})
  38 +const orderList = ref([
  39 + {
  40 + orderNo:'123',
  41 + orderName:'阜成门内大街巡检阜成门内大街巡检阜成门内大街巡检阜成门内大街巡检阜成门内大街巡检阜成门内大街巡检',
  42 + remark:'阜成门内大街周期计划即将到期,请及时处理。阜成门内大街周期计划即将到期,请及时处理。阜成门内大街周期计划即将到期,请及时处理。阜成门内大街周期计划即将到期,请及时处理。',
  43 + createTime:'1736160000000'
  44 + },
  45 + {
  46 + orderNo:'1223',
  47 + orderName:'阜成门内大街巡检阜成门内大街巡检阜成门内大街巡检阜成门内大街巡检阜成门内大街巡检阜成门内大街巡检',
  48 + remark:'阜成门内大街周期计划即将到期,请及时处理。阜成门内大街周期计划即将到期,请及时处理。阜成门内大街周期计划即将到期,请及时处理。阜成门内大街周期计划即将到期,请及时处理。',
  49 + createTime:'1736160000000'
  50 + }
  51 +])
  52 +
  53 +// 页面显示时检查登录状态
  54 +onShow( async () => {
  55 + const res = await getMsg()
  56 +})
  57 +</script>
  58 +
  59 +
  60 +
  61 +<style lang="scss" scoped>
  62 +
  63 +</style>
0 \ No newline at end of file 64 \ No newline at end of file
pages-sub/problem/work-order-manage/index.vue
@@ -138,7 +138,7 @@ @@ -138,7 +138,7 @@
138 </view> 138 </view>
139 <view class="u-body-item u-flex"> 139 <view class="u-body-item u-flex">
140 <view class="u-body-item-title">工单位置:</view> 140 <view class="u-body-item-title">工单位置:</view>
141 - <view class="u-line-1 u-body-value">{{ item.roadName || '-' }}</view> 141 + <view class="u-line-1 u-body-value">{{ item.lonLatAddress || '-' }}</view>
142 </view> 142 </view>
143 <view class="u-body-item u-flex"> 143 <view class="u-body-item u-flex">
144 <view class="u-body-item-title">工单名称:</view> 144 <view class="u-body-item-title">工单名称:</view>
pages.json
@@ -218,9 +218,15 @@ @@ -218,9 +218,15 @@
218 "style": { "navigationBarTitleText": "树木详情" } 218 "style": { "navigationBarTitleText": "树木详情" }
219 } 219 }
220 220
221 -  
222 -  
223 - 221 + ]
  222 + },
  223 + {
  224 + "root": "pages-sub/msg",
  225 + "pages": [
  226 + {
  227 + "path": "index",
  228 + "style": { "navigationBarTitleText": "消息中心" }
  229 + }
224 ] 230 ]
225 } 231 }
226 ], 232 ],
pages/index/index.vue
@@ -3,305 +3,400 @@ @@ -3,305 +3,400 @@
3 <!-- 用户信息栏 --> 3 <!-- 用户信息栏 -->
4 <view class="user-info-bar"> 4 <view class="user-info-bar">
5 <view class="user-info"> 5 <view class="user-info">
6 - <image class="avatar" src="../../static/imgs/default-avatar.png" mode="widthFix"></image>  
7 <view class="user-text"> 6 <view class="user-text">
8 - <view class="username">林晓明</view>  
9 - <view class="login-time">上次登录1天前</view> 7 + <view class="username">你好{{ userName }},欢迎登录</view>
  8 + <view class="login-desc">全域智能运营管理平台</view>
10 </view> 9 </view>
11 </view> 10 </view>
12 - <view class="msg-icon">  
13 - <u-icon name="message" color="#fff" size="32rpx" />  
14 - <view class="msg-badge">5</view> 11 + <view class="msg-icon" @click="handleMsgClick" hover-class="msg-icon--hover">
  12 + <up-icon name="chat" color="#fff" size="24" />
  13 + <view class="msg-badge" v-if="msgCount > 0">
  14 + <up-badge type="error" max="999" :value="msgCount"></up-badge>
  15 + </view>
15 </view> 16 </view>
16 </view> 17 </view>
17 18
18 - <!-- 任务完成情况卡片 -->  
19 - <view class="task-card">  
20 - <view class="card-title">任务完成情况</view>  
21 - <view class="card-header">  
22 - <view class="unit">单位: 个</view>  
23 - <!-- ✅ 核心修改:日期范围改为可点击的选择器 -->  
24 - <view class="date-range" @click="openDatePicker">  
25 -<!-- {{ dateText }}-->  
26 - <neo-datetime-pro  
27 - v-model="dateRange"  
28 - type="daterange"  
29 - placeholder="请选择日期范围" 19 + <view class="content-wrap">
  20 + <!-- 任务完成情况标题 -->
  21 + <view class="module-title">任务完成情况(K线图)</view>
  22 +
  23 + <!-- 任务完成情况卡片 -->
  24 + <view class="task-chart-card">
  25 + <view class="card-header">
  26 + <view class="unit-tip">单位: 个</view>
  27 + <view class="date-picker-wrap" @click="openDatePicker">
  28 + <neo-datetime-pro
  29 + v-model="dateRange"
  30 + type="daterange"
  31 + :clearable="false"
  32 + placeholder="请选择日期范围"
  33 + @confirm="handleDateConfirm"
  34 + />
  35 + </view>
  36 + </view>
  37 +
  38 + <!-- 双折线K线图(匹配示例图) -->
  39 + <view class="chart-container">
  40 + <uni-charts
  41 + type="line"
  42 + :data="klineChartData"
  43 + :categories="klineCategories"
  44 + :option="klineOption"
  45 + :width="chartWidth"
  46 + :height="chartHeight"
30 /> 47 />
31 </view> 48 </view>
32 </view> 49 </view>
33 - <!-- 折线图 -->  
34 - <view class="chart-wrap">  
35 - <!-- <u-line-chart-->  
36 - <!-- :chart-data="chartData"-->  
37 - <!-- :x-axis="xAxis"-->  
38 - <!-- :y-axis="yAxis"-->  
39 - <!-- :legend="legend"-->  
40 - <!-- height="300rpx"-->  
41 - <!-- ></u-line-chart>-->  
42 - </view>  
43 - </view>  
44 50
45 - <!-- 待办/已办切换栏 -->  
46 - <view class="tab-bar">  
47 - <view  
48 - class="tab-item"  
49 - :class="{ active: currentTab === 'todo' }"  
50 - @click="currentTab = 'todo'"  
51 - >  
52 - 待办事项(5)  
53 - <view class="tab-active-line" v-if="currentTab === 'todo'"></view>  
54 - </view>  
55 - <view  
56 - class="tab-item"  
57 - :class="{ active: currentTab === 'done' }"  
58 - @click="currentTab = 'done'"  
59 - >  
60 - 已办事项(89)  
61 - <view class="tab-active-line" v-if="currentTab === 'done'"></view> 51 + <!-- 待办/已办切换栏 -->
  52 + <view class="tab-switch-bar">
  53 + <view
  54 + class="tab-item"
  55 + :class="{ active: currentTab === 'todo' }"
  56 + @click="switchTab('todo')"
  57 + hover-class="tab-item--hover"
  58 + >
  59 + 待办事项({{ todoList.length }})
  60 + <view class="tab-active-line" v-if="currentTab === 'todo'"></view>
  61 + </view>
  62 + <view
  63 + class="tab-item"
  64 + :class="{ active: currentTab === 'done' }"
  65 + @click="switchTab('done')"
  66 + hover-class="tab-item--hover"
  67 + >
  68 + 已办事项({{ doneList.length }})
  69 + <view class="tab-active-line" v-if="currentTab === 'done'"></view>
  70 + </view>
62 </view> 71 </view>
63 - </view>  
64 72
65 - <!-- 事项列表 -->  
66 - <view class="task-list">  
67 - <view class="task-item" v-for="(item, index) in currentTaskList" :key="index">  
68 - <view class="task-name">事项名称: {{ item.name }}{{ item.name }}{{ item.name }}{{ item.name }}{{ item.name }}{{ item.name }}</view>  
69 - <view class="task-desc">  
70 - <view>紧急程度: {{ item.urgency }}</view>  
71 - <view class="task-time">{{ item.time }}</view> 73 + <!-- 事项列表 -->
  74 + <view class="task-list-container">
  75 + <up-empty v-if="!currentTaskList.length" text="暂无相关事项" />
  76 + <view
  77 + class="task-item"
  78 + v-for="(item, index) in currentTaskList"
  79 + :key="index"
  80 + @click="handleTaskClick(item)"
  81 + hover-class="task-item--hover"
  82 + >
  83 + <view class="task-name">
  84 + {{ item.name }}
  85 + </view>
  86 + <view class="task-meta">
  87 + <view class="urgency-tag" :class="`urgency-tag--${getUrgencyType(item.urgency)}`">
  88 + {{ item.urgency || '普通' }}
  89 + </view>
  90 + <view class="task-time">{{ timeFormat(item.time) }}</view>
  91 + </view>
72 </view> 92 </view>
73 </view> 93 </view>
74 </view> 94 </view>
75 -  
76 -  
77 </view> 95 </view>
78 </template> 96 </template>
79 97
80 <script setup> 98 <script setup>
81 -import { ref, reactive, watch } from 'vue';  
82 -  
83 -// 折线图数据  
84 -const chartData = reactive([  
85 - {  
86 - name: '已完成任务数',  
87 - data: [5, 35, 55, 40, 65, 50, 70],  
88 - color: '#4CAF50'  
89 - },  
90 - {  
91 - name: '待完成总任务数',  
92 - data: [5, 70, 75, 70, 75, 75, 90],  
93 - color: '#E53935'  
94 - }  
95 -]);  
96 -const xAxis = reactive(['12.28', '12.29', '12.30', '12.31', '01.01', '01.02', '01.03']);  
97 -const yAxis = reactive([0, 25, 50, 75, 100]);  
98 -const legend = reactive(['已完成任务数', '待完成总任务数']);  
99 -  
100 -// 标签切换  
101 -const currentTab = ref('todo');  
102 -const dateRange=ref( [])  
103 -// ✅ 核心新增:时间选择器相关数据  
104 -const showDatePicker = ref(false);  
105 -// 默认显示的时间文本  
106 -const dateText = ref('2025/12/28—2026/01/04');  
107 -// 选中的开始/结束时间  
108 -const selectStartDate = ref('');  
109 -const selectEndDate = ref('');  
110 -  
111 -// 打开时间选择器  
112 -const openDatePicker = () => {  
113 - showDatePicker.value = true; 99 +import { ref, watch, computed, onMounted } from 'vue';
  100 +import { useUserStore } from '@/pinia/user';
  101 +import { timeFormat } from '@/uni_modules/uview-plus';
  102 +import uniCharts from '@/components/uni-charts/uni-charts.vue';
  103 +
  104 +// ========== 1. 常量抽离 ==========
  105 +const FORMAT_CONST = {
  106 + DATE: 'MM.DD', // 匹配示例图的日期格式(12.28)
  107 + DATETIME: 'YYYY-MM-DD HH:mm',
  108 + TIME: 'HH:mm'
114 }; 109 };
115 110
116 -// 确认选择时间  
117 -const confirmDate = (e) => {  
118 - // 格式化选中的时间  
119 - selectStartDate.value = formatDate(e.value[0]);  
120 - selectEndDate.value = formatDate(e.value[1]);  
121 - dateText.value = `${selectStartDate.value}—${selectEndDate.value}`;  
122 - showDatePicker.value = false;  
123 - // 这里可以加:时间改变后重新请求图表数据的逻辑  
124 - // getChartData(selectStartDate.value, selectEndDate.value) 111 +const URGENCY_MAP = {
  112 + 特急: 'urgent',
  113 + 紧急: 'high',
  114 + 一般: 'normal',
  115 + 普通: 'normal',
  116 + '--': 'normal'
125 }; 117 };
126 118
127 -// 时间格式化函数:YYYY/MM/DD 格式  
128 -const formatDate = (dateVal) => {  
129 - const year = dateVal.getFullYear();  
130 - const month = (dateVal.getMonth() + 1).toString().padStart(2, '0');  
131 - const day = dateVal.getDate().toString().padStart(2, '0');  
132 - return `${year}/${month}/${day}`;  
133 -}; 119 +// ========== 2. 响应式数据 ==========
  120 +const msgCount = ref(9999);
  121 +const currentTab = ref('todo');
  122 +const dateRange = ref([]);
134 123
135 -// 待办事项数据  
136 -const todoList = reactive([ 124 +// 双折线K线图专用数据
  125 +const chartWidth = ref(0);
  126 +const chartHeight = ref(300);
  127 +const klineCategories = ref([]); // X轴日期(12.28/12.29...)
  128 +const klineChartData = ref([
137 { 129 {
138 - name: '绿地卫生验收',  
139 - urgency: '特急',  
140 - time: '2025-12-31 15:45:23' 130 + name: '已完成任务数', // 匹配示例图的绿色折线
  131 + data: [],
  132 + color: '#25AF69'
141 }, 133 },
142 { 134 {
143 - name: '阜成门内大街年度计划巡检',  
144 - urgency: '--',  
145 - time: '2025-12-31 09:02:00' 135 + name: '待完成总任务数', // 匹配示例图的棕色折线
  136 + data: [],
  137 + color: '#B34C17'
  138 + }
  139 +]);
  140 +// 图表配置(匹配示例图样式)
  141 +const klineOption = ref({
  142 + grid: {
  143 + top: '25%',
  144 + left: '15%',
  145 + right: '8%',
  146 + bottom: '20%'
146 }, 147 },
147 - {  
148 - name: '金融街年度计划巡检',  
149 - urgency: '--',  
150 - time: '2025-12-31 09:02:00' 148 + xAxis: {
  149 + axisLabel: { fontSize: 12, color: '#666' }
151 }, 150 },
152 - {  
153 - name: '金融街年度计划巡检',  
154 - urgency: '--',  
155 - time: '2025-12-31 09:02:00' 151 + yAxis: {
  152 + min: 0,
  153 +
  154 + splitLine: { lineStyle: { color: '#f5f5f7' } },
  155 + axisLabel: { fontSize: 12, color: '#666' }
156 }, 156 },
157 - {  
158 - name: '示例事项',  
159 - urgency: '一般',  
160 - time: '2026-01-04 10:00:00' 157 + tooltip: {
  158 + trigger: 'axis',
  159 + formatter: (params) => {
  160 + return `${params[0].name}<br
  161 + ${params[0].seriesName}: ${params[0].data}<br
  162 + ${params[1].seriesName}: ${params[1].data}`;
  163 + }
  164 + },
  165 + legend: {
  166 + show: true,
  167 + top: '5%',
  168 + textStyle: { fontSize: 12 }
161 } 169 }
  170 +});
  171 +
  172 +// 任务列表数据
  173 +const todoList = ref([
  174 + { name: '绿地卫生验收', urgency: '特急', time: '2025-12-31 15:45:23' },
  175 + { name: '阜成门内大街年度计划巡检', urgency: '--', time: '2025-12-31 09:02:00' },
  176 + { name: '金融街年度计划巡检', urgency: '--', time: '2025-12-31 09:02:00' },
  177 + { name: '示例事项', urgency: '一般', time: '2026-01-04 10:00:00' }
  178 +]);
  179 +const doneList = ref([
  180 + { name: '道路清洁验收', urgency: '普通', time: '2025-12-30 14:20:00' }
162 ]); 181 ]);
163 182
164 -// 已办事项数据(示例)  
165 -const doneList = reactive([  
166 - {  
167 - name: '道路清洁验收',  
168 - urgency: '普通',  
169 - time: '2025-12-30 14:20:00' 183 +// ========== 3. 计算属性 ==========
  184 +const userStore = useUserStore();
  185 +const userName = computed(() => userStore.userInfo?.user?.nickname || '用户');
  186 +const currentTaskList = computed(() => {
  187 + return currentTab.value === 'todo' ? todoList.value : doneList.value;
  188 +});
  189 +
  190 +// ========== 4. 工具函数 ==========
  191 +const rpx2px = (rpx) => {
  192 + const systemInfo = wx.getSystemInfoSync();
  193 + return Math.floor(rpx * (systemInfo.screenWidth / 750));
  194 +};
  195 +
  196 +const getUrgencyType = (urgency) => {
  197 + return URGENCY_MAP[urgency] || 'normal';
  198 +};
  199 +
  200 +const initRecent7Days = () => {
  201 + const now = new Date();
  202 + const sevenDaysAgo = new Date();
  203 + sevenDaysAgo.setDate(now.getDate() - 6);
  204 + dateRange.value = [sevenDaysAgo, now];
  205 +};
  206 +
  207 +/**
  208 + * 初始化双折线K线数据(匹配示例图)
  209 + */
  210 +const fetchKlineData = async () => {
  211 + try {
  212 + // 模拟示例图数据(日期+已完成+待完成)
  213 + const rawData = [
  214 + { date: '12.21', done: 10, total: 110 },
  215 + { date: '12.22', done: 35, total: 70 },
  216 + { date: '12.23', done: 55, total: 728 },
  217 + { date: '12.24', done: 35, total: 65 },
  218 + { date: '12.25', done: 65, total: 78 },
  219 + { date: '12.26', done: 50, total: 272 },
  220 + { date: '12.22', done: 35, total: 70 },
  221 + { date: '12.23', done: 55, total: 78 },
  222 + { date: '12.24', done: 35, total: 65 },
  223 + { date: '12.25', done: 65, total: 78 },
  224 + { date: '12.26', done: 50, total: 72 },
  225 + { date: '12.27', done: 72, total: 92 }
  226 + ];
  227 +
  228 + // 转换为uni-charts格式
  229 + klineCategories.value = rawData.map(item => item.date);
  230 + klineChartData.value[0].data = rawData.map(item => item.done);
  231 + klineChartData.value[1].data = rawData.map(item => item.total);
  232 + } catch (error) {
  233 + wx.showToast({ title: '获取K线数据失败', icon: 'none' });
170 } 234 }
171 -]); 235 +};
  236 +
  237 +// ========== 5. 业务逻辑 ==========
  238 +const switchTab = (tabType) => {
  239 + if (currentTab.value === tabType) return;
  240 + currentTab.value = tabType;
  241 +};
  242 +
  243 +const openDatePicker = () => {};
  244 +
  245 +const handleDateConfirm = (e) => {
  246 + if (!e?.value?.length) return;
  247 + fetchKlineData();
  248 +};
172 249
173 -// 当前显示的事项列表  
174 -const currentTaskList = ref(todoList);  
175 -// 监听标签切换  
176 -watch(currentTab, (newVal) => {  
177 - currentTaskList.value = newVal === 'todo' ? todoList : doneList; 250 +const handleMsgClick = () => {
  251 + wx.navigateTo({ url: '/pages-sub/msg/index' });
  252 +};
  253 +
  254 +const handleTaskClick = (item) => {
  255 + wx.navigateTo({ url: `/pages-sub/task/detail?id=${item.id || ''}` });
  256 +};
  257 +
  258 +// ========== 6. 生命周期 ==========
  259 +onMounted(() => {
  260 + chartWidth.value = rpx2px(750 - 60);
  261 + chartHeight.value = rpx2px(300);
  262 + initRecent7Days();
  263 + fetchKlineData();
178 }); 264 });
179 </script> 265 </script>
180 266
181 <style scoped lang="scss"> 267 <style scoped lang="scss">
  268 +/* 样式保持不变,无需修改 */
  269 +$primary-color: #1677ff;
  270 +$danger-color: #E53935;
  271 +$success-color: #4CAF50;
  272 +$text-color: #333;
  273 +$text-color-light: #666;
  274 +$text-color-placeholder: #999;
  275 +$bg-color: #f5f5f7;
  276 +$card-bg: #fff;
  277 +$border-radius: 32rpx;
  278 +$card-radius: 10rpx;
  279 +$spacing-sm: 10rpx;
  280 +$spacing-md: 20rpx;
  281 +$spacing-lg: 30rpx;
  282 +
182 .home-page { 283 .home-page {
183 - background-color: #f5f7fa; 284 + background-color: $bg-color;
184 min-height: 100vh; 285 min-height: 100vh;
185 } 286 }
186 287
187 -/* 用户信息栏 */  
188 .user-info-bar { 288 .user-info-bar {
189 - background-color: #1677ff;  
190 - padding: 180rpx 30rpx 120rpx; 289 + background: url("https://img.jichengshanshui.com.cn:28207/appimg/bg.jpg") no-repeat;
  290 + background-size: 100% 100%;
  291 + padding: 200rpx $spacing-lg 270rpx;
191 display: flex; 292 display: flex;
192 justify-content: space-between; 293 justify-content: space-between;
193 align-items: center; 294 align-items: center;
194 color: #fff; 295 color: #fff;
195 - z-index: 1;  
196 position: relative; 296 position: relative;
  297 + z-index: 1;
197 298
198 - .user-info {  
199 - display: flex;  
200 - align-items: center;  
201 -  
202 - .avatar {  
203 - width: 80rpx;  
204 - height: 80rpx;  
205 - border-radius: 50%;  
206 - margin-right: 20rpx;  
207 - }  
208 -  
209 - .username {  
210 - font-size: 32rpx;  
211 - font-weight: 500;  
212 - } 299 + .username {
  300 + font-size: 32rpx;
  301 + font-weight: 500;
  302 + margin-bottom: 8rpx;
  303 + }
213 304
214 - .login-time {  
215 - font-size: 24rpx;  
216 - opacity: 0.8;  
217 - } 305 + .login-desc {
  306 + font-size: 24rpx;
  307 + opacity: 0.8;
218 } 308 }
219 309
220 .msg-icon { 310 .msg-icon {
221 position: relative; 311 position: relative;
  312 + padding: 10rpx;
  313 + border-radius: 50%;
  314 + transition: background-color 0.2s;
  315 +
  316 + &--hover {
  317 + background-color: rgba(255, 255, 255, 0.2);
  318 + }
222 319
223 .msg-badge { 320 .msg-badge {
224 position: absolute; 321 position: absolute;
225 top: -10rpx; 322 top: -10rpx;
226 right: -10rpx; 323 right: -10rpx;
227 - background-color: #ff3d00;  
228 - color: #fff;  
229 - font-size: 20rpx;  
230 - width: 28rpx;  
231 - height: 28rpx;  
232 - border-radius: 50%;  
233 - display: flex;  
234 - align-items: center;  
235 - justify-content: center;  
236 } 324 }
237 } 325 }
238 } 326 }
239 327
240 -/* 任务完成情况卡片 */  
241 -.task-card {  
242 - background-color: #fff;  
243 - margin: 0 30rpx;  
244 - margin-top: -60rpx;  
245 - border-radius: 16rpx 16rpx 0 0;  
246 - padding: 20rpx;  
247 - box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05); 328 +.content-wrap {
  329 + margin-top: -245rpx;
  330 + border-radius: $border-radius $border-radius 0 0;
  331 + background-color: $bg-color;
248 position: relative; 332 position: relative;
249 z-index: 2; 333 z-index: 2;
  334 + overflow: hidden;
  335 +}
250 336
251 - .card-title {  
252 - font-size: 30rpx;  
253 - font-weight: 600;  
254 - margin-bottom: 15rpx;  
255 - } 337 +.module-title {
  338 + padding: $spacing-lg $spacing-lg 0;
  339 + font-size: 30rpx;
  340 + font-weight: 600;
  341 + color: $text-color-light;
  342 + margin-bottom: $spacing-sm;
  343 +}
  344 +
  345 +.task-chart-card {
  346 + background-color: $card-bg;
  347 + border-radius: $card-radius;
  348 + margin: 0 $spacing-lg $spacing-md;
  349 + padding: $spacing-md;
256 350
257 .card-header { 351 .card-header {
258 display: flex; 352 display: flex;
259 justify-content: space-between; 353 justify-content: space-between;
260 - font-size: 24rpx;  
261 - color: #999;  
262 - margin-bottom: 10rpx;  
263 align-items: center; 354 align-items: center;
264 - } 355 + font-size: 24rpx;
  356 + color: $text-color-placeholder;
  357 + margin-bottom: $spacing-md;
265 358
266 - // ✅ 日期选择器样式  
267 - .date-range {  
268 - display: flex;  
269 - align-items: center;  
270 - gap: 6rpx;  
271 - padding: 4rpx 8rpx;  
272 - border-radius: 6rpx;  
273 - &:active {  
274 - background-color: #f5f5f5; 359 + .date-picker-wrap {
  360 + padding: 4rpx 8rpx;
  361 + border-radius: 6rpx;
  362 + transition: background-color 0.2s;
  363 +
  364 + &:active {
  365 + background-color: $bg-color;
  366 + }
275 } 367 }
276 } 368 }
277 - .date-icon {  
278 - margin-top: 2rpx;  
279 - }  
280 369
281 - .chart-wrap { 370 + .chart-container {
282 width: 100%; 371 width: 100%;
283 height: 300rpx; 372 height: 300rpx;
  373 + display: flex;
  374 + align-items: center;
  375 + justify-content: center;
284 } 376 }
285 } 377 }
286 378
287 -/* 待办/已办切换栏 */  
288 -.tab-bar { 379 +.tab-switch-bar {
289 display: flex; 380 display: flex;
290 - background-color: #fff;  
291 - margin: 0 30rpx;  
292 - border-radius: 0;  
293 - overflow: hidden; 381 + margin: 0 $spacing-lg;
  382 + background-color: $card-bg;
  383 + border-radius: $card-radius $card-radius 0 0;
294 384
295 .tab-item { 385 .tab-item {
296 flex: 1; 386 flex: 1;
297 text-align: center; 387 text-align: center;
298 - padding: 20rpx 0; 388 + padding: $spacing-md 0;
299 font-size: 28rpx; 389 font-size: 28rpx;
300 - color: #666; 390 + color: $text-color-light;
301 position: relative; 391 position: relative;
  392 + transition: color 0.2s;
  393 +
  394 + &--hover {
  395 + background-color: $bg-color;
  396 + }
302 397
303 &.active { 398 &.active {
304 - color: #1677ff; 399 + color: $primary-color;
305 font-weight: 500; 400 font-weight: 500;
306 } 401 }
307 402
@@ -311,22 +406,26 @@ watch(currentTab, (newVal) =&gt; { @@ -311,22 +406,26 @@ watch(currentTab, (newVal) =&gt; {
311 left: 0; 406 left: 0;
312 width: 100%; 407 width: 100%;
313 height: 4rpx; 408 height: 4rpx;
314 - background-color: #1677ff; 409 + background-color: $primary-color;
315 } 410 }
316 } 411 }
317 } 412 }
318 413
319 -/* 事项列表 */  
320 -.task-list {  
321 - background-color: #fff;  
322 - margin: 0 30rpx ;  
323 - border-radius: 0 0 16rpx 16rpx;  
324 - padding: 10rpx 20rpx;  
325 - box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05); 414 +.task-list-container {
  415 + background-color: $card-bg;
  416 + margin: 0 $spacing-lg;
  417 + padding: $spacing-md;
  418 + border-radius: 0 0 $card-radius $card-radius;
  419 + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
326 420
327 .task-item { 421 .task-item {
328 - padding: 20rpx 0;  
329 - border-bottom: 1px solid #f5f5f5; 422 + padding: $spacing-md;
  423 + border-bottom: 1px solid $bg-color;
  424 + transition: background-color 0.2s;
  425 +
  426 + &--hover {
  427 + background-color: $bg-color;
  428 + }
330 429
331 &:last-child { 430 &:last-child {
332 border-bottom: none; 431 border-bottom: none;
@@ -334,22 +433,39 @@ watch(currentTab, (newVal) =&gt; { @@ -334,22 +433,39 @@ watch(currentTab, (newVal) =&gt; {
334 433
335 .task-name { 434 .task-name {
336 font-size: 28rpx; 435 font-size: 28rpx;
337 - color: #333;  
338 - margin-bottom: 10rpx;  
339 - // ✅ 修复超长文字溢出:单行显示+超出省略 436 + color: $text-color;
  437 + margin-bottom: $spacing-sm;
340 white-space: nowrap; 438 white-space: nowrap;
341 overflow: hidden; 439 overflow: hidden;
342 text-overflow: ellipsis; 440 text-overflow: ellipsis;
343 } 441 }
344 442
345 - .task-desc { 443 + .task-meta {
346 display: flex; 444 display: flex;
347 justify-content: space-between; 445 justify-content: space-between;
  446 + align-items: center;
348 font-size: 24rpx; 447 font-size: 24rpx;
349 - color: #666; 448 +
  449 + .urgency-tag {
  450 + padding: 2rpx 8rpx;
  451 + border-radius: 4rpx;
  452 + color: #fff;
  453 +
  454 + &--urgent {
  455 + background-color: $danger-color;
  456 + }
  457 +
  458 + &--high {
  459 + background-color: #fa8c16;
  460 + }
  461 +
  462 + &--normal {
  463 + background-color: $text-color-placeholder;
  464 + }
  465 + }
350 466
351 .task-time { 467 .task-time {
352 - color: #999; 468 + color: $text-color-placeholder;
353 } 469 }
354 } 470 }
355 } 471 }