chart-compare.vue 8.2 KB

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