|
@@ -0,0 +1,518 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="family-relation-graph">
|
|
|
|
|
+ <!-- 空状态 -->
|
|
|
|
|
+ <view class="graph-empty" v-if="!members || members.length === 0">
|
|
|
|
|
+ <text class="empty-text">暂无家庭成员</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <!-- Canvas 画布 -->
|
|
|
|
|
+ <canvas
|
|
|
|
|
+ v-else
|
|
|
|
|
+ canvas-id="relationGraphCanvas"
|
|
|
|
|
+ id="relationGraphCanvas"
|
|
|
|
|
+ type="2d"
|
|
|
|
|
+ @tap="onCanvasTap"
|
|
|
|
|
+ :style="{
|
|
|
|
|
+ width: canvasWidth + 'px',
|
|
|
|
|
+ height: canvasHeight + 'px'
|
|
|
|
|
+ }" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'FamilyRelationGraph',
|
|
|
|
|
+ props: {
|
|
|
|
|
+ dimensionCode: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ validator: function(val) {
|
|
|
|
|
+ return ['body', 'mind', 'action'].indexOf(val) !== -1
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ selfId: {
|
|
|
|
|
+ type: [Number, String],
|
|
|
|
|
+ default: null
|
|
|
|
|
+ },
|
|
|
|
|
+ members: {
|
|
|
|
|
+ type: Array,
|
|
|
|
|
+ default: function() { return [] }
|
|
|
|
|
+ },
|
|
|
|
|
+ energyMap: {
|
|
|
|
|
+ type: Object,
|
|
|
|
|
+ default: function() { return {} }
|
|
|
|
|
+ },
|
|
|
|
|
+ intimacyMap: {
|
|
|
|
|
+ type: Object,
|
|
|
|
|
+ default: function() { return {} }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ data: function() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ canvasWidth: 320,
|
|
|
|
|
+ canvasHeight: 320,
|
|
|
|
|
+ dpr: 1,
|
|
|
|
|
+ positionedMembers: [],
|
|
|
|
|
+ selfPosition: { x: 0, y: 0 },
|
|
|
|
|
+ themeColors: {
|
|
|
|
|
+ body: '#8D6E63',
|
|
|
|
|
+ mind: '#FF6B35',
|
|
|
|
|
+ action: '#4CAF50'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ themeColor: function() {
|
|
|
|
|
+ return this.themeColors[this.dimensionCode] || '#8D6E63'
|
|
|
|
|
+ },
|
|
|
|
|
+ themeColorRgba: function() {
|
|
|
|
|
+ // 主题色15%透明度 for fill
|
|
|
|
|
+ var c = this.themeColor
|
|
|
|
|
+ return c + '26' // 15% alpha hex
|
|
|
|
|
+ },
|
|
|
|
|
+ themeColorStroke: function() {
|
|
|
|
|
+ var c = this.themeColor
|
|
|
|
|
+ return c + '66' // 40% alpha hex
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ watch: {
|
|
|
|
|
+ members: function() { this.draw() },
|
|
|
|
|
+ energyMap: function() { this.draw() },
|
|
|
|
|
+ intimacyMap: function() { this.draw() }
|
|
|
|
|
+ },
|
|
|
|
|
+ mounted: function() {
|
|
|
|
|
+ this.initCanvas()
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ initCanvas: function() {
|
|
|
|
|
+ var sysInfo = uni.getSystemInfoSync()
|
|
|
|
|
+ this.dpr = sysInfo.pixelRatio || 1
|
|
|
|
|
+ var size = uni.upx2px(690)
|
|
|
|
|
+ this.canvasWidth = size
|
|
|
|
|
+ this.canvasHeight = size
|
|
|
|
|
+ var self = this
|
|
|
|
|
+ this.$nextTick(function() {
|
|
|
|
|
+ if (self.members && self.members.length > 0) {
|
|
|
|
|
+ self.draw()
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 获取亲密度数据(有则用,无则返回默认值)
|
|
|
|
|
+ getIntimacy: function(memberId, key) {
|
|
|
|
|
+ var data = this.intimacyMap[memberId]
|
|
|
|
|
+ if (!data) {
|
|
|
|
|
+ var defaults = { closeness: 75, communication: 50, trust: 85 }
|
|
|
|
|
+ return defaults[key]
|
|
|
|
|
+ }
|
|
|
|
|
+ var val = data[key]
|
|
|
|
|
+ return (typeof val !== 'undefined' && val !== null) ? val : 75
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 获取能量值
|
|
|
|
|
+ getEnergy: function(memberId) {
|
|
|
|
|
+ var data = this.energyMap[memberId]
|
|
|
|
|
+ if (!data) return 0
|
|
|
|
|
+ var key = this.dimensionCode + 'Score' // bodyScore / mindScore / actionScore
|
|
|
|
|
+ var val = data[key]
|
|
|
|
|
+ return (typeof val !== 'undefined' && val !== null) ? val : 0
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 极坐标布局 — 计算每个成员的位置
|
|
|
|
|
+ polarLayout: function() {
|
|
|
|
|
+ var centerX = this.canvasWidth / 2
|
|
|
|
|
+ var centerY = this.canvasHeight / 2
|
|
|
|
|
+ this.selfPosition = { x: centerX, y: centerY }
|
|
|
|
|
+
|
|
|
|
|
+ var N = this.members.length
|
|
|
|
|
+ if (N === 0) {
|
|
|
|
|
+ this.positionedMembers = []
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var R_INNER = uni.upx2px(160) // ~74px
|
|
|
|
|
+ var R_OUTER = uni.upx2px(480) // ~222px
|
|
|
|
|
+ var positioned = []
|
|
|
|
|
+
|
|
|
|
|
+ for (var i = 0; i < N; i++) {
|
|
|
|
|
+ var member = this.members[i]
|
|
|
|
|
+ var intimacy = this.getIntimacy(member.id, 'closeness')
|
|
|
|
|
+ // 亲密度0→半径最大,100→半径最小
|
|
|
|
|
+ var radius = R_INNER + (100 - intimacy) / 100 * (R_OUTER - R_INNER)
|
|
|
|
|
+ // 从正上方(12点钟)开始均匀分布
|
|
|
|
|
+ var angle = (2 * Math.PI * i) / N - Math.PI / 2
|
|
|
|
|
+ var x = centerX + radius * Math.cos(angle)
|
|
|
|
|
+ var y = centerY + radius * Math.sin(angle)
|
|
|
|
|
+
|
|
|
|
|
+ var isSelf = (member.id === this.selfId)
|
|
|
|
|
+ var memberType = member.memberType || 'child'
|
|
|
|
|
+
|
|
|
|
|
+ positioned.push({
|
|
|
|
|
+ id: member.id,
|
|
|
|
|
+ nickname: member.nickname || member.name || '成员',
|
|
|
|
|
+ memberType: memberType,
|
|
|
|
|
+ canvasX: x,
|
|
|
|
|
+ canvasY: y,
|
|
|
|
|
+ radius: isSelf ? uni.upx2px(150) : uni.upx2px(120), // self 72px, others 56px -> divide by 2
|
|
|
|
|
+ hitRadius: isSelf ? uni.upx2px(180) : uni.upx2px(140),
|
|
|
|
|
+ isSelf: isSelf,
|
|
|
|
|
+ intimacy: this.getIntimacy(member.id, 'closeness'),
|
|
|
|
|
+ communication: this.getIntimacy(member.id, 'communication'),
|
|
|
|
|
+ trust: this.getIntimacy(member.id, 'trust')
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ this.positionedMembers = positioned
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 主绘制流程
|
|
|
|
|
+ draw: function() {
|
|
|
|
|
+ this.polarLayout()
|
|
|
|
|
+ if (this.positionedMembers.length === 0) return
|
|
|
|
|
+
|
|
|
|
|
+ var self = this
|
|
|
|
|
+ var query = uni.createSelectorQuery().in(this)
|
|
|
|
|
+
|
|
|
|
|
+ // 使用 type="2d" 的 node API
|
|
|
|
|
+ query.select('#relationGraphCanvas')
|
|
|
|
|
+ .fields({ node: true, size: true })
|
|
|
|
|
+ .exec(function(res) {
|
|
|
|
|
+ if (!res || !res[0]) {
|
|
|
|
|
+ // 降级方案:使用 CanvasContext
|
|
|
|
|
+ self.drawLegacy()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ var canvas = res[0].node
|
|
|
|
|
+ if (!canvas) {
|
|
|
|
|
+ self.drawLegacy()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ var ctx = canvas.getContext('2d')
|
|
|
|
|
+
|
|
|
|
|
+ // 设置 canvas 实际像素(适配高清屏)
|
|
|
|
|
+ canvas.width = self.canvasWidth * self.dpr
|
|
|
|
|
+ canvas.height = self.canvasHeight * self.dpr
|
|
|
|
|
+ ctx.scale(self.dpr, self.dpr)
|
|
|
|
|
+
|
|
|
|
|
+ // 清除画布
|
|
|
|
|
+ ctx.clearRect(0, 0, self.canvasWidth, self.canvasHeight)
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制连线(先画线,让形状盖在线上)
|
|
|
|
|
+ self.drawLines(ctx)
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制成员
|
|
|
|
|
+ for (var i = 0; i < self.positionedMembers.length; i++) {
|
|
|
|
|
+ self.drawMember(ctx, self.positionedMembers[i])
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 降级绘制(使用 CanvasContext 兼容旧版基础库)
|
|
|
|
|
+ drawLegacy: function() {
|
|
|
|
|
+ var ctx = uni.createCanvasContext('relationGraphCanvas', this)
|
|
|
|
|
+ var self = this
|
|
|
|
|
+
|
|
|
|
|
+ // 清除
|
|
|
|
|
+ ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
|
|
|
|
|
+
|
|
|
|
|
+ // 连线
|
|
|
|
|
+ self.drawLinesLegacy(ctx)
|
|
|
|
|
+
|
|
|
|
|
+ // 成员
|
|
|
|
|
+ for (var i = 0; i < self.positionedMembers.length; i++) {
|
|
|
|
|
+ self.drawMemberLegacy(ctx, self.positionedMembers[i])
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ctx.draw()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制连线(type=2d mode)
|
|
|
|
|
+ drawLines: function(ctx) {
|
|
|
|
|
+ var selfPos = this.selfPosition
|
|
|
|
|
+ var members = this.positionedMembers
|
|
|
|
|
+ for (var i = 0; i < members.length; i++) {
|
|
|
|
|
+ var m = members[i]
|
|
|
|
|
+ if (m.isSelf) continue
|
|
|
|
|
+
|
|
|
|
|
+ // 信任度 → 色相 (红0°→黄60°→绿120°)
|
|
|
|
|
+ var trust = Math.max(0, Math.min(100, m.trust || 0))
|
|
|
|
|
+ var hue = (120 * trust / 100)
|
|
|
|
|
+ ctx.strokeStyle = 'hsl(' + hue + ', 75%, 45%)'
|
|
|
|
|
+ ctx.globalAlpha = 0.8
|
|
|
|
|
+
|
|
|
|
|
+ // 沟通度 → 线宽 2~10px
|
|
|
|
|
+ var comm = Math.max(0, Math.min(100, m.communication || 0))
|
|
|
|
|
+ ctx.lineWidth = 2 + (comm / 100) * 8
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制从 self 到成员的线段
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ ctx.moveTo(selfPos.x, selfPos.y)
|
|
|
|
|
+ ctx.lineTo(m.canvasX, m.canvasY)
|
|
|
|
|
+ ctx.stroke()
|
|
|
|
|
+ }
|
|
|
|
|
+ ctx.globalAlpha = 1.0
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制连线(legacy mode)
|
|
|
|
|
+ drawLinesLegacy: function(ctx) {
|
|
|
|
|
+ var selfPos = this.selfPosition
|
|
|
|
|
+ var members = this.positionedMembers
|
|
|
|
|
+ for (var i = 0; i < members.length; i++) {
|
|
|
|
|
+ var m = members[i]
|
|
|
|
|
+ if (m.isSelf) continue
|
|
|
|
|
+
|
|
|
|
|
+ var trust = Math.max(0, Math.min(100, m.trust || 0))
|
|
|
|
|
+ var hue = (120 * trust / 100)
|
|
|
|
|
+ ctx.setStrokeStyle('hsl(' + hue + ', 75%, 45%)')
|
|
|
|
|
+ ctx.globalAlpha = 0.8
|
|
|
|
|
+
|
|
|
|
|
+ var comm = Math.max(0, Math.min(100, m.communication || 0))
|
|
|
|
|
+ ctx.setLineWidth(2 + (comm / 100) * 8)
|
|
|
|
|
+
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ ctx.moveTo(selfPos.x, selfPos.y)
|
|
|
|
|
+ ctx.lineTo(m.canvasX, m.canvasY)
|
|
|
|
|
+ ctx.stroke()
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 根据成员类型绘制形状 path
|
|
|
|
|
+ drawMemberShape: function(ctx, x, y, r, memberType) {
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ if (memberType === 'parent' || memberType === 'PARENT') {
|
|
|
|
|
+ // 圆角正方形
|
|
|
|
|
+ var cr = r * 0.25
|
|
|
|
|
+ var s = r * 0.8
|
|
|
|
|
+ ctx.moveTo(x - s + cr, y - s)
|
|
|
|
|
+ ctx.lineTo(x + s - cr, y - s)
|
|
|
|
|
+ ctx.arcTo(x + s, y - s, x + s, y - s + cr, cr)
|
|
|
|
|
+ ctx.lineTo(x + s, y + s - cr)
|
|
|
|
|
+ ctx.arcTo(x + s, y + s, x + s - cr, y + s, cr)
|
|
|
|
|
+ ctx.lineTo(x - s + cr, y + s)
|
|
|
|
|
+ ctx.arcTo(x - s, y + s, x - s, y + s - cr, cr)
|
|
|
|
|
+ ctx.lineTo(x - s, y - s + cr)
|
|
|
|
|
+ ctx.arcTo(x - s, y - s, x - s + cr, y - s, cr)
|
|
|
|
|
+ ctx.closePath()
|
|
|
|
|
+ } else if (memberType === 'child' || memberType === 'CHILD' || memberType === 'children') {
|
|
|
|
|
+ // 圆形
|
|
|
|
|
+ ctx.arc(x, y, r * 0.8, 0, 2 * Math.PI)
|
|
|
|
|
+ } else if (memberType === 'partner' || memberType === 'spouse' || memberType === 'PARTNER') {
|
|
|
|
|
+ // 简化心形
|
|
|
|
|
+ var hr = r * 0.48
|
|
|
|
|
+ // 左上弧(左心耳)
|
|
|
|
|
+ ctx.arc(x - hr * 0.5, y - hr * 0.2, hr, 0, Math.PI * 1.7, true)
|
|
|
|
|
+ // 右上弧(右心耳)
|
|
|
|
|
+ ctx.arc(x + hr * 0.5, y - hr * 0.2, hr, Math.PI * 1.3, Math.PI * 0.5, true)
|
|
|
|
|
+ ctx.closePath()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 默认圆形
|
|
|
|
|
+ ctx.arc(x, y, r * 0.8, 0, 2 * Math.PI)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制环形能量进度条
|
|
|
|
|
+ drawEnergyRing: function(ctx, x, y, radius, fillRatio, color) {
|
|
|
|
|
+ var lineWidth = 4
|
|
|
|
|
+ // 灰色底圈
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ ctx.arc(x, y, radius, 0, 2 * Math.PI)
|
|
|
|
|
+ ctx.strokeStyle = 'rgba(200, 200, 200, 0.35)'
|
|
|
|
|
+ ctx.lineWidth = lineWidth
|
|
|
|
|
+ ctx.stroke()
|
|
|
|
|
+
|
|
|
|
|
+ if (fillRatio <= 0) return
|
|
|
|
|
+
|
|
|
|
|
+ // 彩色进度圈
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ var startAngle = -Math.PI / 2
|
|
|
|
|
+ var endAngle = startAngle + fillRatio * 2 * Math.PI
|
|
|
|
|
+ ctx.arc(x, y, radius, startAngle, endAngle)
|
|
|
|
|
+ ctx.strokeStyle = color
|
|
|
|
|
+ ctx.lineWidth = lineWidth
|
|
|
|
|
+ ctx.lineCap = 'round'
|
|
|
|
|
+ ctx.stroke()
|
|
|
|
|
+ ctx.lineCap = 'butt' // reset
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制成员形状(type=2d mode)
|
|
|
|
|
+ drawMember: function(ctx, member) {
|
|
|
|
|
+ var themeColor = this.themeColor
|
|
|
|
|
+ var x = member.canvasX
|
|
|
|
|
+ var y = member.canvasY
|
|
|
|
|
+ var r = member.radius
|
|
|
|
|
+ var energy = this.getEnergy(member.id)
|
|
|
|
|
+ var fillRatio = Math.max(0, Math.min(1, energy / 100))
|
|
|
|
|
+
|
|
|
|
|
+ ctx.save()
|
|
|
|
|
+
|
|
|
|
|
+ if (member.isSelf) {
|
|
|
|
|
+ // self:外层发光描边
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ ctx.arc(x, y, r + 6, 0, 2 * Math.PI)
|
|
|
|
|
+ ctx.strokeStyle = themeColor
|
|
|
|
|
+ ctx.lineWidth = 3
|
|
|
|
|
+ ctx.globalAlpha = 0.4
|
|
|
|
|
+ ctx.stroke()
|
|
|
|
|
+ ctx.globalAlpha = 1.0
|
|
|
|
|
+
|
|
|
|
|
+ // 内圆填充
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ ctx.arc(x, y, r, 0, 2 * Math.PI)
|
|
|
|
|
+ ctx.fillStyle = '#FFFFFF'
|
|
|
|
|
+ ctx.fill()
|
|
|
|
|
+ ctx.strokeStyle = themeColor
|
|
|
|
|
+ ctx.lineWidth = 3
|
|
|
|
|
+ ctx.stroke()
|
|
|
|
|
+
|
|
|
|
|
+ // 中心文字(昵称首字)
|
|
|
|
|
+ ctx.fillStyle = themeColor
|
|
|
|
|
+ ctx.font = 'bold ' + Math.round(r * 1.0) + 'px sans-serif'
|
|
|
|
|
+ ctx.textAlign = 'center'
|
|
|
|
|
+ ctx.textBaseline = 'middle'
|
|
|
|
|
+ var displayName = (member.nickname && member.nickname.charAt(0)) || '我'
|
|
|
|
|
+ ctx.fillText(displayName, x, y)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 其他成员:根据 memberType 选择形状
|
|
|
|
|
+ this.drawMemberShape(ctx, x, y, r, member.memberType)
|
|
|
|
|
+
|
|
|
|
|
+ // 填充主题色(淡)
|
|
|
|
|
+ ctx.fillStyle = this.themeColorRgba
|
|
|
|
|
+ ctx.fill()
|
|
|
|
|
+ ctx.strokeStyle = this.themeColorStroke
|
|
|
|
|
+ ctx.lineWidth = 2
|
|
|
|
|
+ ctx.stroke()
|
|
|
|
|
+
|
|
|
|
|
+ // 环形能量进度条(在形状外围)
|
|
|
|
|
+ this.drawEnergyRing(ctx, x, y, r + 5, fillRatio, themeColor)
|
|
|
|
|
+
|
|
|
|
|
+ // 中心昵称首字
|
|
|
|
|
+ ctx.fillStyle = '#4A5568'
|
|
|
|
|
+ ctx.font = 'bold ' + Math.round(r * 0.7) + 'px sans-serif'
|
|
|
|
|
+ ctx.textAlign = 'center'
|
|
|
|
|
+ ctx.textBaseline = 'middle'
|
|
|
|
|
+ var displayName = (member.nickname && member.nickname.charAt(0)) || '?'
|
|
|
|
|
+ ctx.fillText(displayName, x, y)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ctx.restore()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制成员(legacy mode)
|
|
|
|
|
+ drawMemberLegacy: function(ctx, member) {
|
|
|
|
|
+ var themeColor = this.themeColor
|
|
|
|
|
+ var x = member.canvasX
|
|
|
|
|
+ var y = member.canvasY
|
|
|
|
|
+ var r = member.radius
|
|
|
|
|
+ var energy = this.getEnergy(member.id)
|
|
|
|
|
+ var fillRatio = Math.max(0, Math.min(1, energy / 100))
|
|
|
|
|
+
|
|
|
|
|
+ if (member.isSelf) {
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ ctx.arc(x, y, r + 6, 0, 2 * Math.PI)
|
|
|
|
|
+ ctx.setStrokeStyle(themeColor)
|
|
|
|
|
+ ctx.setLineWidth(3)
|
|
|
|
|
+ ctx.setGlobalAlpha(0.4)
|
|
|
|
|
+ ctx.stroke()
|
|
|
|
|
+ ctx.setGlobalAlpha(1.0)
|
|
|
|
|
+
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ ctx.arc(x, y, r, 0, 2 * Math.PI)
|
|
|
|
|
+ ctx.setFillStyle('#FFFFFF')
|
|
|
|
|
+ ctx.fill()
|
|
|
|
|
+ ctx.setStrokeStyle(themeColor)
|
|
|
|
|
+ ctx.setLineWidth(3)
|
|
|
|
|
+ ctx.stroke()
|
|
|
|
|
+
|
|
|
|
|
+ ctx.setFillStyle(themeColor)
|
|
|
|
|
+ ctx.font = 'bold ' + Math.round(r * 1.0) + 'px sans-serif'
|
|
|
|
|
+ ctx.setTextAlign('center')
|
|
|
|
|
+ ctx.setTextBaseline('middle')
|
|
|
|
|
+ var name = (member.nickname && member.nickname.charAt(0)) || '我'
|
|
|
|
|
+ ctx.fillText(name, x, y)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.drawMemberShape(ctx, x, y, r, member.memberType)
|
|
|
|
|
+ ctx.setFillStyle(this.themeColorRgba)
|
|
|
|
|
+ ctx.fill()
|
|
|
|
|
+ ctx.setStrokeStyle(this.themeColorStroke)
|
|
|
|
|
+ ctx.setLineWidth(2)
|
|
|
|
|
+ ctx.stroke()
|
|
|
|
|
+
|
|
|
|
|
+ // 环形进度条
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ ctx.arc(x, y, r + 5, 0, 2 * Math.PI)
|
|
|
|
|
+ ctx.setStrokeStyle('rgba(200, 200, 200, 0.35)')
|
|
|
|
|
+ ctx.setLineWidth(4)
|
|
|
|
|
+ ctx.stroke()
|
|
|
|
|
+
|
|
|
|
|
+ if (fillRatio > 0) {
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ var startAngle = -Math.PI / 2
|
|
|
|
|
+ var endAngle = startAngle + fillRatio * 2 * Math.PI
|
|
|
|
|
+ ctx.arc(x, y, r + 5, startAngle, endAngle)
|
|
|
|
|
+ ctx.setStrokeStyle(themeColor)
|
|
|
|
|
+ ctx.setLineWidth(4)
|
|
|
|
|
+ ctx.setLineCap('round')
|
|
|
|
|
+ ctx.stroke()
|
|
|
|
|
+ ctx.setLineCap('butt')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ctx.setFillStyle('#4A5568')
|
|
|
|
|
+ ctx.font = 'bold ' + Math.round(r * 0.7) + 'px sans-serif'
|
|
|
|
|
+ ctx.setTextAlign('center')
|
|
|
|
|
+ ctx.setTextBaseline('middle')
|
|
|
|
|
+ var name = (member.nickname && member.nickname.charAt(0)) || '?'
|
|
|
|
|
+ ctx.fillText(name, x, y)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // Canvas 点击命中检测
|
|
|
|
|
+ onCanvasTap: function(e) {
|
|
|
|
|
+ if (!e || !e.detail || typeof e.detail.x === 'undefined') return
|
|
|
|
|
+
|
|
|
|
|
+ var self = this
|
|
|
|
|
+ var query = uni.createSelectorQuery().in(this)
|
|
|
|
|
+ query.select('#relationGraphCanvas').boundingClientRect(function(rect) {
|
|
|
|
|
+ if (!rect) return
|
|
|
|
|
+ var tapX = e.detail.x - (rect.left || 0)
|
|
|
|
|
+ var tapY = e.detail.y - (rect.top || 0)
|
|
|
|
|
+
|
|
|
|
|
+ for (var i = 0; i < self.positionedMembers.length; i++) {
|
|
|
|
|
+ var m = self.positionedMembers[i]
|
|
|
|
|
+ var dx = tapX - m.canvasX
|
|
|
|
|
+ var dy = tapY - m.canvasY
|
|
|
|
|
+ var dist = Math.sqrt(dx * dx + dy * dy)
|
|
|
|
|
+ if (dist <= m.hitRadius) {
|
|
|
|
|
+ self.$emit('memberTap', {
|
|
|
|
|
+ memberId: m.id,
|
|
|
|
|
+ memberType: m.memberType,
|
|
|
|
|
+ nickname: m.nickname
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }).exec()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.family-relation-graph {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 20rpx 0;
|
|
|
|
|
+ margin: 10rpx 0;
|
|
|
|
|
+ background: #FFFFFF;
|
|
|
|
|
+ border-radius: 24rpx;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+}
|
|
|
|
|
+.graph-empty {
|
|
|
|
|
+ padding: 60rpx 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+.empty-text {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ color: #94A3B8;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|