chart-compare.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. <template>
  2. <view class="page-compare">
  3. <!-- Loading state -->
  4. <view v-if="loading" class="loading-state">
  5. <view class="skeleton-chart" />
  6. <view class="skeleton-chart" />
  7. </view>
  8. <template v-else>
  9. <!-- Info header -->
  10. <view class="compare-header">
  11. <text class="compare-title">{{ relationName }} vs 我</text>
  12. <text class="compare-subtitle">{{ relationTypeLabel(relationType) }}关系能量对比</text>
  13. </view>
  14. <!-- Dual charts -->
  15. <view class="charts-row">
  16. <view class="chart-wrapper">
  17. <view class="chart-label">
  18. <text class="chart-label-name">{{ relationName }}</text>
  19. <text class="chart-label-date">{{ relationBirthDate }}</text>
  20. </view>
  21. <TriangleChart
  22. :chartData="relationChart"
  23. :birthYear="relationBirthYear"
  24. :birthMonth="relationBirthMonth"
  25. :birthDay="relationBirthDay"
  26. />
  27. </view>
  28. <view class="chart-wrapper">
  29. <view class="chart-label">
  30. <text class="chart-label-name">我</text>
  31. <text class="chart-label-date">{{ myBirthDate }}</text>
  32. </view>
  33. <TriangleChart
  34. :chartData="myChart"
  35. :birthYear="myBirthYear"
  36. :birthMonth="myBirthMonth"
  37. :birthDay="myBirthDay"
  38. />
  39. </view>
  40. </view>
  41. <!-- AI Interpretation button -->
  42. <view class="action-bar">
  43. <view class="btn-interpret" :class="{ interpreting: interpreting }" @click="handleInterpret">
  44. <text class="btn-interpret-text">
  45. {{ interpreting ? '解读中...' : interpretation ? '重新解读' : 'AI 关系能量解读' }}
  46. </text>
  47. </view>
  48. </view>
  49. <!-- Interpretation result -->
  50. <view v-if="interpretation" class="interpretation-card">
  51. <view class="interpretation-header">
  52. <text class="interpretation-title">🔗 关系能量解读</text>
  53. <text v-if="interpretDate" class="interpretation-date">{{ interpretDate }}</text>
  54. </view>
  55. <view class="interpretation-body">
  56. <text class="interpretation-content">{{ interpretation }}</text>
  57. </view>
  58. </view>
  59. </template>
  60. </view>
  61. </template>
  62. <script setup>
  63. import { ref } from 'vue'
  64. import { onLoad } from '@dcloudio/uni-app'
  65. import requestInstance from '@/utils/api'
  66. import TriangleChart from '@/components/TriangleChart.vue'
  67. const relationId = ref(null)
  68. const loading = ref(true)
  69. const relationName = ref('')
  70. const relationType = ref('')
  71. const relationBirthDate = ref('')
  72. const relationBirthYear = ref(0)
  73. const relationBirthMonth = ref(0)
  74. const relationBirthDay = ref(0)
  75. const relationChart = ref(null)
  76. const myChart = ref(null)
  77. const myBirthDate = ref('')
  78. const myBirthYear = ref(0)
  79. const myBirthMonth = ref(0)
  80. const myBirthDay = ref(0)
  81. const interpretation = ref('')
  82. const interpretDate = ref('')
  83. const interpreting = ref(false)
  84. const relationApi = {
  85. get: (id) => requestInstance.post('/relation/get', { id }),
  86. compare: (relationId) => requestInstance.post('/relation/compare', { relationId }),
  87. interpret: (relationId) => requestInstance.post('/relation/compare/interpret', { relationId })
  88. }
  89. const myApi = {
  90. profile: () => requestInstance.post('/profile/get', {})
  91. }
  92. const relationTypeMap = {
  93. spouse: '夫妻',
  94. parent_child: '亲子',
  95. colleague: '同事',
  96. friend: '朋友',
  97. superior: '上下级'
  98. }
  99. function relationTypeLabel(type) {
  100. return relationTypeMap[type] || type || '未知'
  101. }
  102. onLoad(async (query) => {
  103. relationId.value = query?.id || null
  104. if (!relationId.value) {
  105. uni.showToast({ title: '缺少关系人信息', icon: 'none' })
  106. setTimeout(() => uni.navigateBack(), 1500)
  107. return
  108. }
  109. await fetchData(relationId.value)
  110. })
  111. async function fetchData(relationId) {
  112. loading.value = true
  113. try {
  114. const [rel, profile] = await Promise.all([
  115. relationApi.get(relationId),
  116. myApi.profile()
  117. ])
  118. // Relation data
  119. relationName.value = rel.name || '未知'
  120. relationType.value = rel.relationType || ''
  121. relationBirthDate.value = rel.birthDate || ''
  122. relationChart.value = rel.chartData || null
  123. if (rel.birthDate) {
  124. const parts = rel.birthDate.split('-')
  125. relationBirthYear.value = parseInt(parts[0]) || 0
  126. relationBirthMonth.value = parseInt(parts[1]) || 0
  127. relationBirthDay.value = parseInt(parts[2]) || 0
  128. }
  129. // Current user data
  130. myChart.value = profile.chartData || null
  131. if (profile.birthDate) {
  132. myBirthDate.value = profile.birthDate
  133. const parts = profile.birthDate.split('-')
  134. myBirthYear.value = parseInt(parts[0]) || 0
  135. myBirthMonth.value = parseInt(parts[1]) || 0
  136. myBirthDay.value = parseInt(parts[2]) || 0
  137. }
  138. // Check if interpretation already exists
  139. try {
  140. const cmp = await relationApi.compare(relationId)
  141. if (cmp && cmp.interpretation) {
  142. interpretation.value = cmp.interpretation
  143. interpretDate.value = cmp.updatedAt || cmp.createdAt || ''
  144. }
  145. } catch (e) {
  146. // No existing comparison — user can trigger interpret
  147. }
  148. } catch (e) {
  149. uni.showToast({ title: '加载数据失败', icon: 'none' })
  150. } finally {
  151. loading.value = false
  152. }
  153. }
  154. async function handleInterpret() {
  155. if (interpreting.value) return
  156. interpreting.value = true
  157. try {
  158. const result = await relationApi.interpret(relationId.value)
  159. if (result && result.interpretation) {
  160. interpretation.value = result.interpretation
  161. interpretDate.value = result.updatedAt || result.createdAt || ''
  162. } else {
  163. uni.showToast({ title: '解读失败,请重试', icon: 'none' })
  164. }
  165. } catch (e) {
  166. uni.showToast({ title: '解读请求失败', icon: 'none' })
  167. } finally {
  168. interpreting.value = false
  169. }
  170. }
  171. </script>
  172. <style scoped lang="scss">
  173. .page-compare {
  174. min-height: 100vh;
  175. background: var(--color-bg);
  176. padding: 16px;
  177. }
  178. .loading-state {
  179. display: flex;
  180. flex-direction: column;
  181. gap: 16px;
  182. padding-top: 20px;
  183. }
  184. .skeleton-chart {
  185. height: 320px;
  186. background: linear-gradient(90deg, var(--color-bg-secondary) 25%, var(--color-bg) 50%, var(--color-bg-secondary) 75%);
  187. background-size: 400px 100%;
  188. animation: shimmer 1.5s ease-in-out infinite;
  189. border-radius: 16px;
  190. }
  191. @keyframes shimmer {
  192. 0% { background-position: -200px 0; }
  193. 100% { background-position: 200px 0; }
  194. }
  195. .compare-header {
  196. margin-bottom: 16px;
  197. }
  198. .compare-title {
  199. font-size: 20px;
  200. font-weight: 700;
  201. color: var(--color-text-primary);
  202. display: block;
  203. margin-bottom: 4px;
  204. }
  205. .compare-subtitle {
  206. font-size: 13px;
  207. color: var(--color-text-secondary);
  208. }
  209. .charts-row {
  210. display: flex;
  211. flex-direction: column;
  212. gap: 16px;
  213. }
  214. .chart-wrapper {
  215. width: 100%;
  216. }
  217. .chart-label {
  218. display: flex;
  219. justify-content: space-between;
  220. align-items: center;
  221. margin-bottom: 8px;
  222. padding: 0 4px;
  223. }
  224. .chart-label-name {
  225. font-size: 14px;
  226. font-weight: 600;
  227. color: var(--color-text-primary);
  228. }
  229. .chart-label-date {
  230. font-size: 12px;
  231. color: var(--color-text-secondary);
  232. }
  233. .action-bar {
  234. margin-top: 20px;
  235. margin-bottom: 16px;
  236. }
  237. .btn-interpret {
  238. width: 100%;
  239. height: 48px;
  240. background: var(--color-brand);
  241. border-radius: 12px;
  242. display: flex;
  243. align-items: center;
  244. justify-content: center;
  245. }
  246. .btn-interpret:active {
  247. transform: scale(0.97);
  248. opacity: 0.9;
  249. }
  250. .btn-interpret.interpreting {
  251. opacity: 0.7;
  252. pointer-events: none;
  253. }
  254. .btn-interpret-text {
  255. color: var(--color-bg-card);
  256. font-size: 16px;
  257. font-weight: 600;
  258. }
  259. .interpretation-card {
  260. background: var(--color-bg-card);
  261. border-radius: 16px;
  262. padding: 20px 16px;
  263. box-shadow: var(--shadow-card);
  264. }
  265. .interpretation-header {
  266. display: flex;
  267. justify-content: space-between;
  268. align-items: center;
  269. margin-bottom: 12px;
  270. }
  271. .interpretation-title {
  272. font-size: 16px;
  273. font-weight: 600;
  274. color: var(--color-text-primary);
  275. }
  276. .interpretation-date {
  277. font-size: 12px;
  278. color: var(--color-text-secondary);
  279. }
  280. .interpretation-body {
  281. background: var(--color-bg-secondary);
  282. border-radius: 12px;
  283. padding: 16px;
  284. }
  285. .interpretation-content {
  286. font-size: 14px;
  287. color: var(--color-text-secondary);
  288. line-height: 1.8;
  289. white-space: pre-wrap;
  290. }
  291. </style>