|
@@ -0,0 +1,270 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="it-anchor">
|
|
|
|
|
+ <!-- 触发区:有箭头才渲染;点击弹出曲线浮层 -->
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-if="arrowText"
|
|
|
|
|
+ class="indicator-trend-arrow"
|
|
|
|
|
+ @tap.stop="openPopup"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text :class="arrowCssClass">{{ arrowText }}</text>
|
|
|
|
|
+ <text v-if="showLatest && effectiveLatestText" class="it-a-latest">{{ effectiveLatestText }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 曲线弹层 -->
|
|
|
|
|
+ <view v-if="popupVisible" class="it-popup-mask" @tap.stop="closePopup">
|
|
|
|
|
+ <view class="it-popup-card" @tap.stop>
|
|
|
|
|
+ <view class="it-popup-head">
|
|
|
|
|
+ <text class="it-popup-title">{{ titleText }}</text>
|
|
|
|
|
+ <text v-if="effectiveLatestText" class="it-popup-latest">最新:{{ effectiveLatestText }}</text>
|
|
|
|
|
+ <text class="it-popup-close" @tap.stop="closePopup">✕</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="it-popup-body">
|
|
|
|
|
+ <view v-if="!hasTrend" class="it-empty">
|
|
|
|
|
+ <text v-if="!item" class="it-empty-note">暂无趋势数据</text>
|
|
|
|
|
+ <text v-else class="it-empty-note">仅一次记录,暂无趋势</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <TrendChart
|
|
|
|
|
+ v-else
|
|
|
|
|
+ :series="series"
|
|
|
|
|
+ mode="line"
|
|
|
|
|
+ :ref-low="item.refLow"
|
|
|
|
|
+ :ref-high="item.refHigh"
|
|
|
|
|
+ :all-period-dates="allPeriodDates"
|
|
|
|
|
+ :show-values="false"
|
|
|
|
|
+ />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import { getIndicatorTrend } from '@/utils/api'
|
|
|
|
|
+import { resolveChange } from '@/utils/indicator-change'
|
|
|
|
|
+import TrendChart from '@/pages/health/components/TrendChart.vue'
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'IndicatorTrendArrow',
|
|
|
|
|
+ components: { TrendChart },
|
|
|
|
|
+ props: {
|
|
|
|
|
+ /** 用户 id(必传) */
|
|
|
|
|
+ subjectId: { type: [Number, String], required: true },
|
|
|
|
|
+ /** 指标定义 id(必传) */
|
|
|
|
|
+ definitionId: { type: [Number, String], required: true },
|
|
|
|
|
+ /** 报告来源类型(可选,透传给后端) */
|
|
|
|
|
+ sourceType: { type: String, default: '' },
|
|
|
|
|
+ /** 外部已加载的 trend 数据,传入则零请求 */
|
|
|
|
|
+ trend: { type: Object, default: null },
|
|
|
|
|
+ /** 最新值文本(可选,供弹层标题/内联展示;缺省时回退 item.latest) */
|
|
|
|
|
+ latestText: { type: String, default: '' },
|
|
|
|
|
+ /** 是否在内联触发区同时展示最新值(默认 true;宿主已展示值时传 false 避免重复) */
|
|
|
|
|
+ showLatest: { type: Boolean, default: true }
|
|
|
|
|
+ },
|
|
|
|
|
+ data: function() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ item: this.trend,
|
|
|
|
|
+ popupVisible: false,
|
|
|
|
|
+ loading: false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ /** 方向箭头:movement up→↑ down→↓ flat→→;不可用时返回 ''
|
|
|
|
|
+ * 注:箭头判定只依赖 change.movement(quicklook 行 item 无 points,
|
|
|
|
|
+ * 不能因 points.length<2 而拦截箭头);openPopup 时若无 points,
|
|
|
|
|
+ * 弹窗内已降级为「暂无趋势数据」,不会崩。 */
|
|
|
|
|
+ arrowText: function() {
|
|
|
|
|
+ if (!this.item || !this.item.change) return ''
|
|
|
|
|
+ var ch = this.item.change
|
|
|
|
|
+ if (ch.comparable === false) return ''
|
|
|
|
|
+ var mv = ch.movement
|
|
|
|
|
+ if (mv === 'up') return '↑'
|
|
|
|
|
+ if (mv === 'down') return '↓'
|
|
|
|
|
+ if (mv === 'flat') return '→'
|
|
|
|
|
+ return ''
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 箭头颜色类(消费后端 trendCode,前端不自判改善/恶化) */
|
|
|
|
|
+ arrowCssClass: function() {
|
|
|
|
|
+ if (!this.item || !this.item.change) return 'gray-text'
|
|
|
|
|
+ var r = resolveChange(this.item.change.trendCode, this.item.change.movement, this.item.change.trendLabel)
|
|
|
|
|
+ return r.colorClass
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 是否有可画的 trend(>=1 期) */
|
|
|
|
|
+ hasTrend: function() {
|
|
|
|
|
+ return !!(this.item && this.item.points && this.item.points.length >= 1)
|
|
|
|
|
+ },
|
|
|
|
|
+ /** series 构造:与 indicator-timeline.vue series 口径一致 */
|
|
|
|
|
+ series: function() {
|
|
|
|
|
+ if (!this.item || !Array.isArray(this.item.points)) return []
|
|
|
|
|
+ return [{
|
|
|
|
|
+ name: this.item.name || '',
|
|
|
|
|
+ color: '#F97316',
|
|
|
|
|
+ points: this.item.points.map(function(p) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ reportDate: String(p.reportDate).slice(0, 10),
|
|
|
|
|
+ value: p.value,
|
|
|
|
|
+ numericValue: p.numericValue,
|
|
|
|
|
+ unit: p.unit,
|
|
|
|
|
+ dateSource: p.dateSource
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }]
|
|
|
|
|
+ },
|
|
|
|
|
+ /** X 轴日期全集(points 日期 + missingPeriods 日期合并去重升序) */
|
|
|
|
|
+ allPeriodDates: function() {
|
|
|
|
|
+ if (!this.item) return []
|
|
|
|
|
+ var set = {}
|
|
|
|
|
+ var pts = this.item.points || []
|
|
|
|
|
+ for (var i = 0; i < pts.length; i++) {
|
|
|
|
|
+ var d = pts[i] && pts[i].reportDate
|
|
|
|
|
+ if (d) set[String(d).slice(0, 10)] = true
|
|
|
|
|
+ }
|
|
|
|
|
+ var misses = this.item.missingPeriods || []
|
|
|
|
|
+ for (var j = 0; j < misses.length; j++) {
|
|
|
|
|
+ var md = misses[j] && misses[j].reportDate
|
|
|
|
|
+ if (md) set[String(md).slice(0, 10)] = true
|
|
|
|
|
+ }
|
|
|
|
|
+ var keys = Object.keys(set)
|
|
|
|
|
+ keys.sort(function(a, b) { return a < b ? -1 : (a > b ? 1 : 0) })
|
|
|
|
|
+ return keys
|
|
|
|
|
+ },
|
|
|
|
|
+ titleText: function() {
|
|
|
|
|
+ if (!this.item) return ''
|
|
|
|
|
+ var n = this.item.name || ''
|
|
|
|
|
+ var u = this.item.unit ? (' (' + this.item.unit + ')') : ''
|
|
|
|
|
+ return n + u
|
|
|
|
|
+ },
|
|
|
|
|
+ /** 最新值展示文本:优先 prop,缺省回退 item.latest(弹层标题/内联共用) */
|
|
|
|
|
+ effectiveLatestText: function() {
|
|
|
|
|
+ if (this.latestText) return this.latestText
|
|
|
|
|
+ if (!this.item || !this.item.latest) return ''
|
|
|
|
|
+ var v = this.item.latest
|
|
|
|
|
+ if (v.numericValue !== undefined && v.numericValue !== null) return String(v.numericValue)
|
|
|
|
|
+ return v.value || ''
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ watch: {
|
|
|
|
|
+ trend: function(val) { this.item = val }
|
|
|
|
|
+ },
|
|
|
|
|
+ mounted: function() {
|
|
|
|
|
+ var self = this
|
|
|
|
|
+ var hasPoints = !!(this.item && this.item.points && this.item.points.length > 0)
|
|
|
|
|
+ // 已有 trend prop 且含 points:零请求直接消费
|
|
|
|
|
+ if (this.item && hasPoints) return
|
|
|
|
|
+ var sid = Number(this.subjectId)
|
|
|
|
|
+ var did = Number(this.definitionId)
|
|
|
|
|
+ if (isNaN(sid) || isNaN(did)) return
|
|
|
|
|
+ // 无 trend 或 trend 缺 points(quicklook 行 item):自拉补全,供箭头/弹窗曲线使用
|
|
|
|
|
+ this.loading = true
|
|
|
|
|
+ getIndicatorTrend({
|
|
|
|
|
+ subjectId: sid,
|
|
|
|
|
+ definitionIds: [did],
|
|
|
|
|
+ sourceType: this.sourceType || undefined
|
|
|
|
|
+ }).then(function(res) {
|
|
|
|
|
+ self.loading = false
|
|
|
|
|
+ var full = (res.data && res.data.items && res.data.items[0]) || null
|
|
|
|
|
+ // 已拿到完整 trend(含 points)时整体替换;仅当请求失败(full 为空)才保留行 item 的 change 供箭头展示
|
|
|
|
|
+ if (full) { self.item = full }
|
|
|
|
|
+ }).catch(function() {
|
|
|
|
|
+ self.loading = false
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ openPopup: function() {
|
|
|
|
|
+ this.popupVisible = true
|
|
|
|
|
+ },
|
|
|
|
|
+ closePopup: function() {
|
|
|
|
|
+ this.popupVisible = false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+/* 锚点容器:默认行内,不打断宿主布局 */
|
|
|
|
|
+.it-anchor {
|
|
|
|
|
+ display: inline;
|
|
|
|
|
+}
|
|
|
|
|
+/* 箭头触发区:30rpx 字号,行内对齐指标值展示 */
|
|
|
|
|
+.indicator-trend-arrow {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-left: 6rpx;
|
|
|
|
|
+ vertical-align: baseline;
|
|
|
|
|
+}
|
|
|
|
|
+.indicator-trend-arrow text {
|
|
|
|
|
+ font-size: 30rpx;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
|
|
+.it-a-latest {
|
|
|
|
|
+ font-size: calc(20rpx * var(--font-scale, 1));
|
|
|
|
|
+ color: $facade-text-body;
|
|
|
|
|
+ margin-left: 6rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 遮罩:固定全屏,深色半透明 */
|
|
|
|
|
+.it-popup-mask {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ z-index: 999;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.55);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+/* 卡片:居中,圆角,内边距对齐指标页风格 */
|
|
|
|
|
+.it-popup-card {
|
|
|
|
|
+ width: 85vw;
|
|
|
|
|
+ max-height: 80vh;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ padding: 24rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.18);
|
|
|
|
|
+}
|
|
|
|
|
+.it-popup-head {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: row;
|
|
|
|
|
+ align-items: baseline;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin-bottom: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.it-popup-title {
|
|
|
|
|
+ font-size: calc(28rpx * var(--font-scale, 1));
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #111827;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+}
|
|
|
|
|
+.it-popup-latest {
|
|
|
|
|
+ font-size: calc(22rpx * var(--font-scale, 1));
|
|
|
|
|
+ color: $facade-text-body;
|
|
|
|
|
+ margin-right: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.it-popup-close {
|
|
|
|
|
+ font-size: calc(28rpx * var(--font-scale, 1));
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ padding: 4rpx 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.it-popup-body {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.it-empty {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ min-height: 200rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.it-empty-note {
|
|
|
|
|
+ font-size: calc(24rpx * var(--font-scale, 1));
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 颜色类:全局未定义时兜底;scoped 下不影响其他组件 */
|
|
|
|
|
+.green-text { color: #2BA24C; }
|
|
|
|
|
+.red-text { color: #E64340; }
|
|
|
|
|
+.gray-text { color: #999; }
|
|
|
|
|
+</style>
|