index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. <template>
  2. <view class="container">
  3. <view class="header">
  4. <text class="title">训练方案</text>
  5. <view class="header-actions">
  6. <view class="btn-secondary" @click="showChildPicker">
  7. <text class="btn-secondary-text">筛选孩子</text>
  8. </view>
  9. <view class="btn-primary" @click="goToCreate">
  10. <text class="btn-primary-text">+ 新建方案</text>
  11. </view>
  12. </view>
  13. </view>
  14. <view class="child-filter-bar" v-if="selectedChildId">
  15. <text class="filter-label">当前: {{ selectedChildName || '孩子' }}</text>
  16. <view class="filter-clear" @click="clearChildFilter">
  17. <text class="filter-clear-text">清除</text>
  18. </view>
  19. </view>
  20. <view class="plan-list" v-if="plans.length > 0">
  21. <view class="plan-card" v-for="plan in plans" :key="plan.id" @click="goToDetail(plan.id)">
  22. <view class="plan-header">
  23. <text class="plan-title">{{ plan.title || '未命名方案' }}</text>
  24. <text class="plan-status" :class="'status-' + plan.status">{{ statusText(plan.status) }}</text>
  25. </view>
  26. <view class="plan-meta" v-if="plan.childId">
  27. <text class="meta-text">孩子ID: {{ plan.childId }}</text>
  28. <text class="meta-date">{{ formatDate(plan.createdAt) }}</text>
  29. </view>
  30. <view class="plan-desc" v-if="plan.description">{{ plan.description }}</view>
  31. <view class="plan-actions" @click.stop>
  32. <view class="action-btn outline" v-if="plan.status === 'draft'" @click="goToDetail(plan.id)">
  33. <text>编辑</text>
  34. </view>
  35. <view class="action-btn outline" v-if="plan.status === 'published'" @click="goToBoard(plan.id)">
  36. <text>执行看板</text>
  37. </view>
  38. <view class="action-btn danger" v-if="plan.status === 'draft'" @click="confirmDelete(plan.id)">
  39. <text>删除</text>
  40. </view>
  41. <view class="action-btn primary" v-if="plan.status === 'draft'" @click="handleDeploy(plan.id)">
  42. <text>下发</text>
  43. </view>
  44. </view>
  45. </view>
  46. </view>
  47. <view class="empty" v-else>
  48. <text class="empty-icon">&#x1F3EB;</text>
  49. <text class="empty-title">暂无训练方案</text>
  50. <text class="empty-desc">为绑定家庭创建认知训练方案,或从推荐结果自动生成</text>
  51. <view class="empty-btns">
  52. <view class="btn-secondary" @click="showGenerateDialog">
  53. <text class="btn-secondary-text">从推荐生成</text>
  54. </view>
  55. <view class="btn-primary" @click="goToCreate">
  56. <text class="btn-primary-text">新建方案</text>
  57. </view>
  58. </view>
  59. </view>
  60. <!-- 生成方案弹窗 -->
  61. <view class="modal-mask" v-if="showGenerate" @click="showGenerate = false">
  62. <view class="modal-card" @click.stop>
  63. <text class="modal-title">从推荐结果生成方案</text>
  64. <text class="modal-desc">将根据该孩子最新的认知评估结果,自动生成针对性训练方案</text>
  65. <view class="modal-child-list">
  66. <view class="modal-child-item" v-for="child in bindedChildren" :key="child.id"
  67. @click="generateFromRec(child.id)">
  68. <text class="modal-child-name">{{ child.name || '孩子' + child.id }}</text>
  69. <text class="modal-child-id">ID: {{ child.id }}</text>
  70. </view>
  71. <view class="empty-small" v-if="bindedChildren.length === 0">
  72. <text class="empty-small-text">暂无可用孩子,请先绑定家庭</text>
  73. </view>
  74. </view>
  75. <view class="modal-footer">
  76. <view class="btn-cancel" @click="showGenerate = false">
  77. <text>取消</text>
  78. </view>
  79. </view>
  80. </view>
  81. </view>
  82. <!-- 孩子选择器弹窗 -->
  83. <view class="modal-mask" v-if="showPicker" @click="showPicker = false">
  84. <view class="modal-card" @click.stop>
  85. <text class="modal-title">选择孩子</text>
  86. <view class="modal-child-list">
  87. <view class="modal-child-item" v-for="child in bindedChildren" :key="child.id"
  88. @click="selectChild(child.id, child.name)">
  89. <text class="modal-child-name">{{ child.name || '孩子' + child.id }}</text>
  90. <text class="modal-child-id">ID: {{ child.id }}</text>
  91. </view>
  92. <view class="modal-child-item" @click="selectChild(null, '全部')">
  93. <text class="modal-child-name">全部</text>
  94. </view>
  95. </view>
  96. <view class="modal-footer">
  97. <view class="btn-cancel" @click="showPicker = false">
  98. <text>取消</text>
  99. </view>
  100. </view>
  101. </view>
  102. </view>
  103. <view class="loading" v-if="loading">
  104. <text class="loading-text">加载中...</text>
  105. </view>
  106. </view>
  107. </template>
  108. <script>
  109. import { getTrainingPlanList, generatePlanFromRecommendations, deployTrainingPlan, deleteTrainingPlan } from '../../../utils/api.js'
  110. import { getGuideBindedChildren } from '../../../utils/api.js'
  111. export default {
  112. data() {
  113. return {
  114. plans: [],
  115. bindedChildren: [],
  116. selectedChildId: null,
  117. selectedChildName: null,
  118. showGenerate: false,
  119. showPicker: false,
  120. loading: false
  121. }
  122. },
  123. onShow() {
  124. this.loadPlans()
  125. this.loadBindedChildren()
  126. },
  127. methods: {
  128. loadPlans() {
  129. var self = this
  130. self.loading = true
  131. getTrainingPlanList(self.selectedChildId).then(function(res) {
  132. self.loading = false
  133. if (res.code === 200) {
  134. self.plans = res.data || []
  135. }
  136. }).catch(function() {
  137. self.loading = false
  138. })
  139. },
  140. loadBindedChildren() {
  141. var self = this
  142. getGuideBindedChildren().then(function(res) {
  143. if (res.code === 200) {
  144. self.bindedChildren = res.data || []
  145. }
  146. }).catch(function() {})
  147. },
  148. goToDetail(planId) {
  149. uni.navigateTo({ url: '/pages/guide/training-plans/detail?planId=' + planId })
  150. },
  151. goToBoard(planId) {
  152. uni.navigateTo({ url: '/pages/guide/training-plans/detail?planId=' + planId + '&mode=board' })
  153. },
  154. goToCreate() {
  155. uni.navigateTo({ url: '/pages/guide/training-plans/detail?planId=0' })
  156. },
  157. showChildPicker() {
  158. this.showPicker = true
  159. },
  160. clearChildFilter() {
  161. this.selectedChildId = null
  162. this.selectedChildName = null
  163. this.loadPlans()
  164. },
  165. selectChild(childId, childName) {
  166. this.selectedChildId = childId
  167. this.selectedChildName = childName
  168. this.showPicker = false
  169. this.loadPlans()
  170. },
  171. showGenerateDialog() {
  172. this.showGenerate = true
  173. },
  174. generateFromRec(childId) {
  175. var self = this
  176. uni.showModal({
  177. title: '确认生成',
  178. content: '将根据该孩子的评估结果自动生成训练方案,是否继续?',
  179. success: function(res) {
  180. if (res.confirm) {
  181. self.showGenerate = false
  182. uni.showLoading({ title: '生成中...' })
  183. generatePlanFromRecommendations(childId).then(function(res) {
  184. uni.hideLoading()
  185. if (res.code === 200 && res.data) {
  186. uni.showToast({ title: '生成成功', icon: 'success' })
  187. self.loadPlans()
  188. setTimeout(function() {
  189. self.goToDetail(res.data)
  190. }, 1000)
  191. } else {
  192. uni.showToast({ title: res.message || '生成失败', icon: 'none' })
  193. }
  194. }).catch(function() {
  195. uni.hideLoading()
  196. uni.showToast({ title: '网络异常', icon: 'none' })
  197. })
  198. }
  199. }
  200. })
  201. },
  202. handleDeploy(planId) {
  203. var self = this
  204. uni.showModal({
  205. title: '确认下发',
  206. content: '确定下发此方案?将一次性创建所有训练任务。',
  207. success: function(res) {
  208. if (res.confirm) {
  209. uni.showLoading({ title: '下发中...' })
  210. deployTrainingPlan(planId).then(function(res) {
  211. uni.hideLoading()
  212. if (res.code === 200) {
  213. uni.showToast({ title: '下发成功', icon: 'success' })
  214. self.loadPlans()
  215. } else {
  216. uni.showToast({ title: res.message || '下发失败', icon: 'none' })
  217. }
  218. }).catch(function() {
  219. uni.hideLoading()
  220. uni.showToast({ title: '网络异常', icon: 'none' })
  221. })
  222. }
  223. }
  224. })
  225. },
  226. confirmDelete(planId) {
  227. var self = this
  228. uni.showModal({
  229. title: '确认删除',
  230. content: '删除后无法恢复,是否确定删除?',
  231. success: function(res) {
  232. if (res.confirm) {
  233. deleteTrainingPlan(planId).then(function(res) {
  234. if (res.code === 200) {
  235. uni.showToast({ title: '删除成功', icon: 'success' })
  236. self.loadPlans()
  237. } else {
  238. uni.showToast({ title: res.message || '删除失败', icon: 'none' })
  239. }
  240. }).catch(function() {
  241. uni.showToast({ title: '网络异常', icon: 'none' })
  242. })
  243. }
  244. }
  245. })
  246. },
  247. statusText(status) {
  248. var map = { draft: '草稿', published: '已下发', completed: '已完成' }
  249. return map[status] || status || '草稿'
  250. },
  251. formatDate(dateStr) {
  252. if (!dateStr) return ''
  253. var d = new Date(dateStr)
  254. var y = d.getFullYear()
  255. var m = ('0' + (d.getMonth() + 1)).slice(-2)
  256. var day = ('0' + d.getDate()).slice(-2)
  257. return y + '-' + m + '-' + day
  258. }
  259. }
  260. }
  261. </script>
  262. <style scoped>
  263. .container {
  264. min-height: 100vh;
  265. background: #f5f7fa;
  266. padding-bottom: 40rpx;
  267. }
  268. .header {
  269. display: flex;
  270. flex-direction: row;
  271. justify-content: space-between;
  272. align-items: center;
  273. padding: 30rpx;
  274. background: #fff;
  275. }
  276. .title {
  277. font-size: 36rpx;
  278. font-weight: bold;
  279. color: #333;
  280. }
  281. .header-actions {
  282. display: flex;
  283. flex-direction: row;
  284. gap: 16rpx;
  285. }
  286. .btn-primary {
  287. background: linear-gradient(135deg, #667eea, #764ba2);
  288. padding: 12rpx 28rpx;
  289. border-radius: 32rpx;
  290. }
  291. .btn-primary:active { opacity: 0.8; }
  292. .btn-primary-text {
  293. color: #fff;
  294. font-size: 26rpx;
  295. font-weight: 500;
  296. }
  297. .btn-secondary {
  298. background: #f0f0f0;
  299. padding: 12rpx 28rpx;
  300. border-radius: 32rpx;
  301. }
  302. .btn-secondary:active { opacity: 0.8; }
  303. .btn-secondary-text {
  304. color: #666;
  305. font-size: 26rpx;
  306. }
  307. .child-filter-bar {
  308. display: flex;
  309. flex-direction: row;
  310. justify-content: space-between;
  311. align-items: center;
  312. padding: 16rpx 30rpx;
  313. background: #eef2ff;
  314. }
  315. .filter-label {
  316. font-size: 26rpx;
  317. color: #667eea;
  318. }
  319. .filter-clear {
  320. padding: 4rpx 16rpx;
  321. background: #fff;
  322. border-radius: 16rpx;
  323. }
  324. .filter-clear-text {
  325. font-size: 24rpx;
  326. color: #999;
  327. }
  328. .plan-list {
  329. padding: 20rpx 30rpx;
  330. display: flex;
  331. flex-direction: column;
  332. gap: 20rpx;
  333. }
  334. .plan-card {
  335. background: #fff;
  336. border-radius: 20rpx;
  337. padding: 28rpx;
  338. box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.05);
  339. }
  340. .plan-header {
  341. display: flex;
  342. flex-direction: row;
  343. justify-content: space-between;
  344. align-items: flex-start;
  345. margin-bottom: 12rpx;
  346. }
  347. .plan-title {
  348. font-size: 30rpx;
  349. font-weight: bold;
  350. color: #333;
  351. flex: 1;
  352. }
  353. .plan-status {
  354. font-size: 22rpx;
  355. padding: 4rpx 16rpx;
  356. border-radius: 16rpx;
  357. margin-left: 16rpx;
  358. }
  359. .status-draft {
  360. background: #f3f4f6;
  361. color: #666;
  362. }
  363. .status-published {
  364. background: #dbeafe;
  365. color: #2563eb;
  366. }
  367. .status-completed {
  368. background: #d1fae5;
  369. color: #059669;
  370. }
  371. .plan-meta {
  372. display: flex;
  373. flex-direction: row;
  374. justify-content: space-between;
  375. margin-bottom: 8rpx;
  376. }
  377. .meta-text, .meta-date {
  378. font-size: 24rpx;
  379. color: #999;
  380. }
  381. .plan-desc {
  382. font-size: 26rpx;
  383. color: #666;
  384. line-height: 1.5;
  385. margin-bottom: 16rpx;
  386. }
  387. .plan-actions {
  388. display: flex;
  389. flex-direction: row;
  390. flex-wrap: wrap;
  391. gap: 12rpx;
  392. margin-top: 16rpx;
  393. border-top: 1rpx solid #f0f0f0;
  394. padding-top: 16rpx;
  395. }
  396. .action-btn {
  397. padding: 10rpx 24rpx;
  398. border-radius: 24rpx;
  399. font-size: 24rpx;
  400. }
  401. .action-btn:active { opacity: 0.7; }
  402. .action-btn.outline {
  403. background: #f9fafb;
  404. color: #374151;
  405. border: 1rpx solid #e5e7eb;
  406. }
  407. .action-btn.primary {
  408. background: #667eea;
  409. color: #fff;
  410. }
  411. .action-btn.danger {
  412. background: #fee2e2;
  413. color: #dc2626;
  414. }
  415. .empty {
  416. display: flex;
  417. flex-direction: column;
  418. align-items: center;
  419. padding: 120rpx 40rpx 80rpx;
  420. }
  421. .empty-icon {
  422. font-size: 100rpx;
  423. margin-bottom: 20rpx;
  424. }
  425. .empty-title {
  426. font-size: 32rpx;
  427. color: #333;
  428. font-weight: bold;
  429. margin-bottom: 12rpx;
  430. }
  431. .empty-desc {
  432. font-size: 26rpx;
  433. color: #999;
  434. text-align: center;
  435. line-height: 1.6;
  436. margin-bottom: 40rpx;
  437. }
  438. .empty-btns {
  439. display: flex;
  440. flex-direction: row;
  441. gap: 16rpx;
  442. }
  443. .loading {
  444. display: flex;
  445. align-items: center;
  446. justify-content: center;
  447. padding: 80rpx;
  448. }
  449. .loading-text {
  450. font-size: 28rpx;
  451. color: #999;
  452. }
  453. /* 弹窗 */
  454. .modal-mask {
  455. position: fixed;
  456. top: 0;
  457. left: 0;
  458. right: 0;
  459. bottom: 0;
  460. background: rgba(0,0,0,0.5);
  461. z-index: 100;
  462. display: flex;
  463. align-items: center;
  464. justify-content: center;
  465. }
  466. .modal-card {
  467. background: #fff;
  468. border-radius: 24rpx;
  469. padding: 40rpx;
  470. width: 600rpx;
  471. max-height: 70vh;
  472. overflow-y: auto;
  473. }
  474. .modal-title {
  475. font-size: 32rpx;
  476. font-weight: bold;
  477. color: #333;
  478. display: block;
  479. margin-bottom: 12rpx;
  480. }
  481. .modal-desc {
  482. font-size: 26rpx;
  483. color: #888;
  484. display: block;
  485. margin-bottom: 24rpx;
  486. line-height: 1.5;
  487. }
  488. .modal-child-list {
  489. display: flex;
  490. flex-direction: column;
  491. gap: 12rpx;
  492. margin-bottom: 24rpx;
  493. }
  494. .modal-child-item {
  495. display: flex;
  496. flex-direction: row;
  497. justify-content: space-between;
  498. align-items: center;
  499. padding: 20rpx;
  500. background: #f9fafb;
  501. border-radius: 12rpx;
  502. }
  503. .modal-child-item:active {
  504. background: #eef2ff;
  505. }
  506. .modal-child-name {
  507. font-size: 28rpx;
  508. color: #333;
  509. font-weight: 500;
  510. }
  511. .modal-child-id {
  512. font-size: 24rpx;
  513. color: #999;
  514. }
  515. .empty-small {
  516. padding: 24rpx 0;
  517. text-align: center;
  518. }
  519. .empty-small-text {
  520. font-size: 26rpx;
  521. color: #999;
  522. }
  523. .modal-footer {
  524. display: flex;
  525. flex-direction: row;
  526. justify-content: center;
  527. }
  528. .btn-cancel {
  529. padding: 16rpx 48rpx;
  530. background: #f3f4f6;
  531. border-radius: 32rpx;
  532. }
  533. .btn-cancel:active { opacity: 0.7; }
  534. .btn-cancel text {
  535. font-size: 28rpx;
  536. color: #666;
  537. }
  538. </style>