Commit 4f4750130ff3f959133574da475f80d5d99d791a
1 parent
03b006dc
k线图
Showing
9 changed files
with
835 additions
and
239 deletions
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
components/uni-charts/package.json
0 → 100644
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) => { | @@ -440,7 +440,6 @@ onLoad((options) => { | ||
| 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 () => { | @@ -467,7 +466,7 @@ const treeRoadQuery = async () => { | ||
| 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 () => { | @@ -475,7 +474,6 @@ const getTreeDetail = async () => { | ||
| 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 () => { | @@ -485,7 +483,6 @@ const getTreeDetail = async () => { | ||
| 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 () => { | @@ -563,7 +560,7 @@ const submit = async () => { | ||
| 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) => { | @@ -311,22 +406,26 @@ watch(currentTab, (newVal) => { | ||
| 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) => { | @@ -334,22 +433,39 @@ watch(currentTab, (newVal) => { | ||
| 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 | } |