Commit 4f4750130ff3f959133574da475f80d5d99d791a

Authored by 刘淇
1 parent 03b006dc

k线图

api/user.js
... ... @@ -49,3 +49,26 @@ export const getSimpleDictDataList = () => {
49 49 export const refreshToken = (params) => {
50 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 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 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 381 \ No newline at end of file
... ...
pages-sub/data/tree-archive/editTree.vue
... ... @@ -235,15 +235,15 @@
235 235 <script setup>
236 236 import { ref, reactive, nextTick } from 'vue'
237 237 import { onReady, onLoad, onShow, onUnload } from '@dcloudio/uni-app';
238   -// 接口修改:删除addTree,新增 treeDetailReq + updateTree
  238 +
239 239 import { treeRoadReq, treeLogReq, treeDetailReq, updateTree } from "@/api/tree-archive/tree-archive.js";
240 240 import { timeFormat } from '@/uni_modules/uview-plus';
241 241 import { useUploadImgs } from '@/common/utils/useUploadImgs'
242 242 import { useUserStore } from '@/pinia/user';
243 243  
244   -// ========== 常量配置区 - 完全对标参考页写法 集中管理 ==========
  244 +// ========== 常量配置区
245 245 const CONST = {
246   - // 上传配置 统一抽离,和参考页一致
  246 + // 上传配置 统一抽离
247 247 UPLOAD_CONFIG: { maxCount: 3, uploadText: '选择图片', sizeType: ['compressed'] }
248 248 }
249 249  
... ... @@ -275,13 +275,13 @@ const treeLevelData = ref([])
275 275 const showActionSheet = ref(false);
276 276 const currentActionSheetData = reactive({type: '', list: [], title: ''});
277 277  
278   -// ========== 核心修复:图片上传配置 - 1:1对标工单页面 无任何冗余 ==========
  278 +// ========== 核心修复:图片上传配置 ==========
279 279 const treeImgList = useUploadImgs({
280 280 ...CONST.UPLOAD_CONFIG,
281 281 formRef: formRef,
282 282 fieldName: 'treeImgList'
283 283 })
284   -// 初始化图片数组为纯净空数组,杜绝残留数据
  284 +// 初始化图片数组为纯净空数组,杜绝残留数据
285 285 treeImgList.imgList.value = []
286 286 treeImgList.rawImgList.value = []
287 287  
... ... @@ -440,7 +440,6 @@ onLoad((options) =&gt; {
440 440 onShow(async () => {
441 441 treeLevelData.value = uni.$dict.transformLabelValueToNameValue(uni.$dict.getDictSimpleList('tree_level'))
442 442 treeOwnershipData.value = uni.$dict.transformLabelValueToNameValue(uni.$dict.getDictSimpleList('tree_ownership'))
443   - // ✅ 核心修复:只执行一次回显加载,彻底杜绝重复加载
444 443 if (activeTab.value === 0 && treeId.value && !isInit.value) {
445 444 await getTreeDetail()
446 445 isInit.value = true // 加载完成后标记为true,永不重复执行
... ... @@ -467,7 +466,7 @@ const treeRoadQuery = async () =&gt; {
467 466 rows.value = res.list
468 467 }
469 468  
470   -// ========== ✅ 终极修复:图片回显逻辑 - 彻底解决重复渲染4张图的问题 ==========
  469 +// ==========
471 470 const getTreeDetail = async () => {
472 471 const res = await treeDetailReq({id: treeId.value})
473 472 Object.assign(formData, res)
... ... @@ -475,7 +474,6 @@ const getTreeDetail = async () =&gt; {
475 474 formData.oldtreeownershipText = uni.$dict.getDictLabel('tree_ownership', res.oldtreeownership)
476 475 formData.treeleveltext = uni.$dict.getDictLabel('tree_level', formData.treelevel)
477 476  
478   - // ✅ 第一步:强制清空为纯净空数组,杜绝任何残留
479 477 treeImgList.imgList.value = []
480 478 treeImgList.rawImgList.value = []
481 479 formData.treephotoone = ''
... ... @@ -485,7 +483,6 @@ const getTreeDetail = async () =&gt; {
485 483 formData.treephotofour = ''
486 484 formData.treephotofive = ''
487 485  
488   - // ✅ 第二步:接口返回几张就渲染几张,绝对不会重复
489 486 if (Array.isArray(res.treeImgList) && res.treeImgList.length > 0) {
490 487 const imgList = res.treeImgList.map((url, idx) => ({
491 488 url,
... ... @@ -563,7 +560,7 @@ const submit = async () =&gt; {
563 560 // 表单校验
564 561 const valid = await formRef.value.validate()
565 562 if (!valid) return
566   - // 对标参考页:图片取值 统一调用封装方法
  563 + // 图片取值 统一调用封装方法
567 564 const uploadImgUrls = treeImgList.getSuccessImgUrls()
568 565 formData.maintainunit = userStore.userInfo.user.companyId
569 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 64 \ No newline at end of file
... ...
pages-sub/problem/work-order-manage/index.vue
... ... @@ -138,7 +138,7 @@
138 138 </view>
139 139 <view class="u-body-item u-flex">
140 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 142 </view>
143 143 <view class="u-body-item u-flex">
144 144 <view class="u-body-item-title">工单名称:</view>
... ...
pages.json
... ... @@ -218,9 +218,15 @@
218 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 3 <!-- 用户信息栏 -->
4 4 <view class="user-info-bar">
5 5 <view class="user-info">
6   - <image class="avatar" src="../../static/imgs/default-avatar.png" mode="widthFix"></image>
7 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 9 </view>
11 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 16 </view>
16 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 48 </view>
32 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 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 92 </view>
73 93 </view>
74 94 </view>
75   -
76   -
77 95 </view>
78 96 </template>
79 97  
80 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 265 </script>
180 266  
181 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 283 .home-page {
183   - background-color: #f5f7fa;
  284 + background-color: $bg-color;
184 285 min-height: 100vh;
185 286 }
186 287  
187   -/* 用户信息栏 */
188 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 292 display: flex;
192 293 justify-content: space-between;
193 294 align-items: center;
194 295 color: #fff;
195   - z-index: 1;
196 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 310 .msg-icon {
221 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 320 .msg-badge {
224 321 position: absolute;
225 322 top: -10rpx;
226 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 332 position: relative;
249 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 351 .card-header {
258 352 display: flex;
259 353 justify-content: space-between;
260   - font-size: 24rpx;
261   - color: #999;
262   - margin-bottom: 10rpx;
263 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 371 width: 100%;
283 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 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 385 .tab-item {
296 386 flex: 1;
297 387 text-align: center;
298   - padding: 20rpx 0;
  388 + padding: $spacing-md 0;
299 389 font-size: 28rpx;
300   - color: #666;
  390 + color: $text-color-light;
301 391 position: relative;
  392 + transition: color 0.2s;
  393 +
  394 + &--hover {
  395 + background-color: $bg-color;
  396 + }
302 397  
303 398 &.active {
304   - color: #1677ff;
  399 + color: $primary-color;
305 400 font-weight: 500;
306 401 }
307 402  
... ... @@ -311,22 +406,26 @@ watch(currentTab, (newVal) =&gt; {
311 406 left: 0;
312 407 width: 100%;
313 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 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 430 &:last-child {
332 431 border-bottom: none;
... ... @@ -334,22 +433,39 @@ watch(currentTab, (newVal) =&gt; {
334 433  
335 434 .task-name {
336 435 font-size: 28rpx;
337   - color: #333;
338   - margin-bottom: 10rpx;
339   - // ✅ 修复超长文字溢出:单行显示+超出省略
  436 + color: $text-color;
  437 + margin-bottom: $spacing-sm;
340 438 white-space: nowrap;
341 439 overflow: hidden;
342 440 text-overflow: ellipsis;
343 441 }
344 442  
345   - .task-desc {
  443 + .task-meta {
346 444 display: flex;
347 445 justify-content: space-between;
  446 + align-items: center;
348 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 467 .task-time {
352   - color: #999;
  468 + color: $text-color-placeholder;
353 469 }
354 470 }
355 471 }
... ...