|
|
@@ -0,0 +1,368 @@
|
|
|
+# 行首页 OctopusGraph → 圈子区块 实现计划
|
|
|
+
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
+
|
|
|
+**Goal:** Replace OctopusGraph component with "我的圈子 + 发现推荐" circle section on the action dimension homepage
|
|
|
+
|
|
|
+**Architecture:** Add 2 API wrappers to api.js, then modify action/index.vue to inline the circle section replacing OctopusGraph import + template + data + methods + CSS. Reuse existing CircleCard.vue and CircleDetail.vue components.
|
|
|
+
|
|
|
+**Tech Stack:** uni-app Vue 2, WeChat mini-program
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: Add circle API functions to api.js
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-frontend/utils/api.js` (insert after existing circle functions around line 1710)
|
|
|
+
|
|
|
+- [ ] **Step 1: Add API functions**
|
|
|
+
|
|
|
+Find the existing circle functions block around line 1704-1710:
|
|
|
+```js
|
|
|
+export const getCircleChallengeHistory = (data) => request('/api/health/circle/challenge/history', 'POST', data)
|
|
|
+```
|
|
|
+
|
|
|
+After that line, add:
|
|
|
+```js
|
|
|
+export const getMyCircles = (memberId, memberType) => {
|
|
|
+ return request('/api/circle/my-circles', 'POST', { memberId: memberId, memberType: memberType || 'child' })
|
|
|
+}
|
|
|
+
|
|
|
+export const discoverCircles = (childId) => {
|
|
|
+ return request('/api/circle/discover', 'POST', { childId: childId })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Verify**
|
|
|
+
|
|
|
+Read the surrounding context to confirm insertion point is correct and no syntax errors.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: Modify action/index.vue — remove OctopusGraph, add circle section
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `cfc-frontend/pages/action/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: Remove OctopusGraph import and component registration**
|
|
|
+
|
|
|
+In the `<script>` section:
|
|
|
+
|
|
|
+Remove import line (line 132):
|
|
|
+```js
|
|
|
+import OctopusGraph from '../../components/OctopusGraph.vue'
|
|
|
+```
|
|
|
+
|
|
|
+Remove from components registration (line 139):
|
|
|
+```js
|
|
|
+OctopusGraph,
|
|
|
+```
|
|
|
+
|
|
|
+Remove from the import line the `getContactsList` and `getHealthAlerts` if they were only used by OctopusGraph. Actually, check: `loadContacts` is called separately in `loadChildren`, and `loadRelationshipHealth` is called in `onShow`. These methods call `getContactList` and `getHealthAlerts` respectively. The OctopusGraph component just receives the data. So the data loading methods should stay — they may be needed for other parts.
|
|
|
+
|
|
|
+Actually, let me look at the actual usage: `contactList` and `healthAlerts` data are passed to `<OctopusGraph>` via props. If we remove OctopusGraph, the data loading and storage are still there but unused unless we remove them too. Remove `loadContacts()` call, `loadRelationshipHealth()` call, and their data variables to clean up dead code.
|
|
|
+
|
|
|
+Remove from `loadChildren` method (after `self.loadDimensionData()`):
|
|
|
+```js
|
|
|
+self.loadContacts()
|
|
|
+```
|
|
|
+
|
|
|
+Remove from `onShow` method (after `this.loadFamilyMembersVisible()`):
|
|
|
+```js
|
|
|
+this.loadRelationshipHealth()
|
|
|
+```
|
|
|
+
|
|
|
+Remove data properties that were only used by OctopusGraph (around lines 159-161):
|
|
|
+```js
|
|
|
+contactList: [],
|
|
|
+showImportModal: false,
|
|
|
+healthAlerts: [],
|
|
|
+```
|
|
|
+
|
|
|
+Remove computed/methods that were only used by OctopusGraph:
|
|
|
+- `onContactClick` method
|
|
|
+- `onShowImport` method
|
|
|
+- `onCloseImport` method
|
|
|
+- `onImportSuccess` method
|
|
|
+- `loadContacts` method
|
|
|
+- `loadRelationshipHealth` method
|
|
|
+- `goInteractionLog` method
|
|
|
+- `goRelationshipQuestionnaire` method
|
|
|
+
|
|
|
+Remove ContactCard and ContactImport imports (lines 130-131):
|
|
|
+```js
|
|
|
+import ContactCard from '../../components/ContactCard.vue'
|
|
|
+import ContactImport from '../../components/ContactImport.vue'
|
|
|
+```
|
|
|
+
|
|
|
+Update import of API functions — remove `getContactList`, `getHealthAlerts` if they were only used by the removed methods.
|
|
|
+
|
|
|
+Actually, rather than doing this deletion-by-deletion which is error prone, let me take a surgical approach: just remove the OctopusGraph component usage and its data that's ONLY props for it, but keep the imports and methods since removing them is a bigger refactor than needed.
|
|
|
+
|
|
|
+Let me be more precise — the cleanest approach:
|
|
|
+
|
|
|
+1. Remove `<OctopusGraph ...>` from template
|
|
|
+2. Remove `import OctopusGraph` from script
|
|
|
+3. Remove `OctopusGraph,` from components
|
|
|
+4. Add circle section template + data + methods + CSS
|
|
|
+
|
|
|
+Do NOT remove ContactCard, ContactImport, loadContacts, loadRelationshipHealth, etc. unless they're confirmed unused elsewhere. This is safer and follows "surgical changes" principle.
|
|
|
+
|
|
|
+Actually wait, let me re-read the current code more carefully. The `onContactClick`, `onShowImport`, `onCloseImport`, `onImportSuccess` methods and `ContactImport` component, `ContactCard` component, `showImportModal` data, `contactList` data are all related to the OctopusGraph / contact section. But they might also be used elsewhere on the page.
|
|
|
+
|
|
|
+Looking at the template:
|
|
|
+- Line 111-114: `<ContactImport :visible="showImportModal" @close="onCloseImport" @success="onImportSuccess" />`
|
|
|
+
|
|
|
+This ContactImport is still in the template! So it's page-level, not just OctopusGraph. If we remove OctopusGraph, the ContactImport modal stays if the user wants to import contacts elsewhere.
|
|
|
+
|
|
|
+Actually, ContactImport is a standalone component in the template, not inside OctopusGraph. So removing OctopusGraph component doesn't automatically remove ContactImport. The ContactImport modal is shown from the OctopusGraph component's "import" button via event emission.
|
|
|
+
|
|
|
+Given the user wants to replace the entire OctopusGraph with circles, I should:
|
|
|
+1. Remove `<OctopusGraph>` template tag
|
|
|
+2. Add circle section template
|
|
|
+3. Remove `OctopusGraph` import and component registration
|
|
|
+4. Keep everything else — the ContactImport and related methods are page-level and don't hurt to keep. They might be reused later. The important thing is OctopusGraph is gone and circles are in.
|
|
|
+
|
|
|
+Let me revise the plan to be more surgical.
|
|
|
+
|
|
|
+- [ ] **Step 2: Remove OctopusGraph from template**
|
|
|
+
|
|
|
+Remove lines 92-96:
|
|
|
+```html
|
|
|
+ <!-- ===== 重要关系维护 ===== -->
|
|
|
+ <OctopusGraph
|
|
|
+ :contactList="contactList"
|
|
|
+ :healthAlerts="healthAlerts"
|
|
|
+ :isLoggedIn="isLoggedIn" />
|
|
|
+```
|
|
|
+
|
|
|
+Replace with circle section template (see Step 3).
|
|
|
+
|
|
|
+- [ ] **Step 3: Remove OctopusGraph import and component**
|
|
|
+
|
|
|
+Remove line 132:
|
|
|
+```js
|
|
|
+import OctopusGraph from '../../components/OctopusGraph.vue'
|
|
|
+```
|
|
|
+
|
|
|
+Remove from line 139:
|
|
|
+```js
|
|
|
+OctopusGraph,
|
|
|
+```
|
|
|
+
|
|
|
+Remove from components list (line 139):
|
|
|
+```js
|
|
|
+'OctopusGraph': OctopusGraph,
|
|
|
+```
|
|
|
+(may be just `OctopusGraph` depending on how it's written)
|
|
|
+
|
|
|
+- [ ] **Step 4: Add circle data properties**
|
|
|
+
|
|
|
+In `data()` add after healthAlerts or similar:
|
|
|
+```js
|
|
|
+myCircles: [],
|
|
|
+discoverCirclesList: [],
|
|
|
+circleDetailVisible: false,
|
|
|
+selectedCircle: null,
|
|
|
+selectedCircleIsMember: false,
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: Add circle loading methods**
|
|
|
+
|
|
|
+In `methods`, add after `loadRelationshipHealth` or end of methods:
|
|
|
+```js
|
|
|
+loadCircleData: function() {
|
|
|
+ var self = this
|
|
|
+ if (!this.currentChildId) return
|
|
|
+ getMyCircles(this.currentChildId, 'child').then(function(res) {
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ self.myCircles = Array.isArray(res.data) ? res.data : []
|
|
|
+ }
|
|
|
+ }).catch(function(e) {
|
|
|
+ console.log('获取我的圈子失败', e)
|
|
|
+ })
|
|
|
+ discoverCircles(this.currentChildId).then(function(res) {
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ self.discoverCirclesList = Array.isArray(res.data) ? res.data.slice(0, 3) : []
|
|
|
+ }
|
|
|
+ }).catch(function(e) {
|
|
|
+ console.log('获取推荐圈子失败', e)
|
|
|
+ })
|
|
|
+},
|
|
|
+onCircleClick: function(circle) {
|
|
|
+ var isMember = this.myCircles.some(function(c) {
|
|
|
+ return (c.id || c.circleId) === (circle.id || circle.circleId)
|
|
|
+ })
|
|
|
+ this.selectedCircle = circle
|
|
|
+ this.selectedCircleIsMember = isMember
|
|
|
+ this.circleDetailVisible = true
|
|
|
+},
|
|
|
+onCircleJoin: function(circleId) {
|
|
|
+ var self = this
|
|
|
+ joinCircle({ circleId: circleId, memberId: this.currentChildId, memberType: 'child' }).then(function(res) {
|
|
|
+ if (res.code === 200) {
|
|
|
+ uni.showToast({ title: '加入成功', icon: 'success' })
|
|
|
+ self.circleDetailVisible = false
|
|
|
+ self.loadCircleData()
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: res.message || '加入失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ }).catch(function(e) {
|
|
|
+ console.log('加入圈子失败', e)
|
|
|
+ })
|
|
|
+},
|
|
|
+onCircleLeave: function(circleId) {
|
|
|
+ var self = this
|
|
|
+ leaveCircle({ circleId: circleId, memberId: this.currentChildId, memberType: 'child' }).then(function(res) {
|
|
|
+ if (res.code === 200) {
|
|
|
+ uni.showToast({ title: '已退出', icon: 'success' })
|
|
|
+ self.circleDetailVisible = false
|
|
|
+ self.loadCircleData()
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: res.message || '退出失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ }).catch(function(e) {
|
|
|
+ console.log('退出圈子失败', e)
|
|
|
+ })
|
|
|
+},
|
|
|
+goDiscoverCircles: function() {
|
|
|
+ uni.navigateTo({ url: '/pages/discover/circles' })
|
|
|
+},
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 6: Add import for circle API functions**
|
|
|
+
|
|
|
+In the import line at the top of `<script>`, add to the existing imports from `../../utils/api.js`:
|
|
|
+```js
|
|
|
+getMyCircles, discoverCircles, joinCircle, leaveCircle
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 7: Call loadCircleData**
|
|
|
+
|
|
|
+In `loadDimensionData` method (line 274), add after `this.loadDimensionProducts()`:
|
|
|
+```js
|
|
|
+this.loadCircleData()
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 8: Add circle section template**
|
|
|
+
|
|
|
+Add this HTML template in place of the removed `<OctopusGraph>`:
|
|
|
+```html
|
|
|
+ <!-- ===== 我的圈子 ===== -->
|
|
|
+ <view class="circle-section" v-if="isLoggedIn">
|
|
|
+ <view class="circle-header">
|
|
|
+ <text class="circle-section-title">🤝 我的圈子</text>
|
|
|
+ <text class="circle-header-link" @click="goDiscoverCircles">发现更多 ›</text>
|
|
|
+ </view>
|
|
|
+ <scroll-view class="circle-scroll" scroll-x enable-flex v-if="myCircles.length > 0">
|
|
|
+ <view class="circle-scroll-inner">
|
|
|
+ <view class="circle-card-wrap" v-for="item in myCircles" :key="item.id || item.circleId" @click="onCircleClick(item)">
|
|
|
+ <CircleCard :circle="item" compact />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+ <view class="circle-empty" v-else>
|
|
|
+ <text class="circle-empty-text">暂未加入圈子,去发现看看</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- ===== 发现推荐 ===== -->
|
|
|
+ <view class="circle-discover" v-if="discoverCirclesList.length > 0">
|
|
|
+ <view class="circle-header">
|
|
|
+ <text class="circle-section-title">🔍 发现推荐</text>
|
|
|
+ <text class="circle-header-link" @click="goDiscoverCircles">查看更多 ›</text>
|
|
|
+ </view>
|
|
|
+ <view class="discover-list">
|
|
|
+ <view class="discover-item" v-for="item in discoverCirclesList" :key="item.id || item.circleId" @click="onCircleClick(item)">
|
|
|
+ <CircleCard :circle="item" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 圈子详情弹窗 -->
|
|
|
+ <CircleDetail
|
|
|
+ :visible="circleDetailVisible"
|
|
|
+ :circle="selectedCircle || {}"
|
|
|
+ :isMember="selectedCircleIsMember"
|
|
|
+ @close="circleDetailVisible = false"
|
|
|
+ @join="onCircleJoin"
|
|
|
+ @leave="onCircleLeave" />
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 9: Add component registration**
|
|
|
+
|
|
|
+In `components` (line 139), add:
|
|
|
+```js
|
|
|
+CircleCard,
|
|
|
+CircleDetail,
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 10: Add component imports**
|
|
|
+
|
|
|
+Add in the import section:
|
|
|
+```js
|
|
|
+import CircleCard from '../../components/CircleCard.vue'
|
|
|
+import CircleDetail from '../../components/CircleDetail.vue'
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 11: Add CSS styles**
|
|
|
+
|
|
|
+Add to `<style scoped>`:
|
|
|
+```css
|
|
|
+/* ===== 圈子区块 ===== */
|
|
|
+.circle-section {
|
|
|
+ margin: 20rpx 20rpx 0;
|
|
|
+}
|
|
|
+.circle-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+.circle-section-title {
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+.circle-header-link {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #F97316;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+.circle-scroll {
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.circle-scroll-inner {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ gap: 16rpx;
|
|
|
+ padding: 8rpx 0 16rpx;
|
|
|
+}
|
|
|
+.circle-card-wrap {
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 220rpx;
|
|
|
+}
|
|
|
+.circle-card-wrap:active {
|
|
|
+ opacity: 0.8;
|
|
|
+}
|
|
|
+.circle-empty {
|
|
|
+ padding: 40rpx 0;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.circle-empty-text {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #ccc;
|
|
|
+}
|
|
|
+.circle-discover {
|
|
|
+ margin-top: 8rpx;
|
|
|
+}
|
|
|
+.discover-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12rpx;
|
|
|
+}
|
|
|
+.discover-item:active {
|
|
|
+ opacity: 0.8;
|
|
|
+}
|
|
|
+```
|