detail.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968
  1. <template>
  2. <view class="container">
  3. <!-- ===== 看板模式 ===== -->
  4. <view class="board-section" v-if="mode === 'board' && boardData">
  5. <view class="board-header">
  6. <text class="board-title">{{ boardData.title }}</text>
  7. <text class="board-progress">{{ boardData.progress }}%</text>
  8. </view>
  9. <view class="progress-bar-wrap">
  10. <view class="progress-bar-fill" :style="{ width: boardData.progress + '%' }"></view>
  11. </view>
  12. <view class="board-summary">
  13. <view class="summary-item">
  14. <text class="summary-value">{{ boardData.completedItems }}</text>
  15. <text class="summary-label">已完成</text>
  16. </view>
  17. <view class="summary-item">
  18. <text class="summary-value">{{ boardData.totalItems }}</text>
  19. <text class="summary-label">总任务</text>
  20. </view>
  21. <view class="summary-item">
  22. <text class="summary-value">{{ boardData.totalItems - boardData.completedItems }}</text>
  23. <text class="summary-label">进行中</text>
  24. </view>
  25. </view>
  26. <view class="board-items" v-if="boardData.itemStatus">
  27. <view class="board-item" v-for="item in boardData.itemStatus" :key="item.itemId">
  28. <view class="bi-left">
  29. <view class="bi-check" :class="item.status === 'completed' ? 'done' : ''">
  30. <text v-if="item.status === 'completed'">&#x2714;</text>
  31. <text v-else>&#x25CB;</text>
  32. </view>
  33. <view class="bi-info">
  34. <text class="bi-dim">{{ dimensionLabel(item.dimensionKey) }}</text>
  35. <text class="bi-title">{{ item.taskTitle }}</text>
  36. </view>
  37. </view>
  38. <view class="bi-right">
  39. <text class="bi-status" :class="'bi-' + item.status">{{ itemStatusText(item.status) }}</text>
  40. </view>
  41. </view>
  42. </view>
  43. </view>
  44. <!-- ===== 编辑模式 ===== -->
  45. <view class="edit-section" v-else>
  46. <!-- 基本信息 -->
  47. <view class="section-card">
  48. <view class="section-title">方案信息</view>
  49. <view class="form-item">
  50. <text class="form-label">方案标题</text>
  51. <input class="form-input" v-model="planForm.title" placeholder="请输入方案标题" :disabled="!editable" />
  52. </view>
  53. <view class="form-item">
  54. <text class="form-label">方案描述</text>
  55. <textarea class="form-textarea" v-model="planForm.description" placeholder="请输入方案描述" :disabled="!editable" />
  56. </view>
  57. <view class="form-item">
  58. <text class="form-label">状态</text>
  59. <text class="status-badge" :class="'status-' + planForm.status">{{ statusText(planForm.status) }}</text>
  60. </view>
  61. <view class="form-actions" v-if="editable && !isNew">
  62. <view class="btn-save" @click="savePlan">
  63. <text>保存</text>
  64. </view>
  65. </view>
  66. </view>
  67. <!-- 训练项目 -->
  68. <view class="section-card">
  69. <view class="section-header">
  70. <text class="section-title">训练项目</text>
  71. <view class="btn-add-item" v-if="editable" @click="showAddItem">
  72. <text>+ 添加项目</text>
  73. </view>
  74. </view>
  75. <view class="item-list" v-if="items.length > 0">
  76. <view class="plan-item" v-for="(item, idx) in items" :key="item.id">
  77. <view class="item-header">
  78. <view class="item-seq">{{ idx + 1 }}</view>
  79. <view class="item-dim-tag">{{ dimensionLabel(item.dimensionKey) }}</view>
  80. <view class="item-status-badge" :class="'item-' + item.status">{{ itemStatusText(item.status) }}</view>
  81. </view>
  82. <view class="item-body">
  83. <view class="item-field">
  84. <text class="item-field-label">任务标题</text>
  85. <input v-if="editable" class="item-input" v-model="item.taskTitle" @blur="updateItemField(item)" />
  86. <text v-else class="item-field-value">{{ item.taskTitle }}</text>
  87. </view>
  88. <view class="item-field">
  89. <text class="item-field-label">任务描述</text>
  90. <input v-if="editable" class="item-input" v-model="item.taskDesc" @blur="updateItemField(item)" />
  91. <text v-else class="item-field-value">{{ item.taskDesc }}</text>
  92. </view>
  93. <view class="item-field-row">
  94. <view class="item-field half">
  95. <text class="item-field-label">积分</text>
  96. <input v-if="editable" class="item-input" type="number" v-model="item.taskPoints" @blur="updateItemField(item)" />
  97. <text v-else class="item-field-value">{{ item.taskPoints || 2 }}</text>
  98. </view>
  99. <view class="item-field half">
  100. <text class="item-field-label">截止天数</text>
  101. <input v-if="editable" class="item-input" type="number" v-model="item.deadlineDays" @blur="updateItemField(item)" />
  102. <text v-else class="item-field-value">{{ item.deadlineDays || 7 }}天</text>
  103. </view>
  104. </view>
  105. <view class="item-field" v-if="item.minigameCode">
  106. <text class="item-field-label">小游戏</text>
  107. <text class="item-field-value">{{ item.minigameCode }}</text>
  108. </view>
  109. </view>
  110. <view class="item-actions" v-if="editable">
  111. <view class="item-action-btn delete" @click="deleteItem(item.id, idx)">
  112. <text>删除</text>
  113. </view>
  114. </view>
  115. </view>
  116. </view>
  117. <view class="empty-items" v-else>
  118. <text class="empty-items-text">暂无训练项目,点击上方"添加项目"开始配置</text>
  119. </view>
  120. </view>
  121. <!-- 操作栏 -->
  122. <view class="action-bar" v-if="editable && !isNew">
  123. <view class="btn-deploy" @click="handleDeploy" v-if="planForm.status === 'draft'">
  124. <text>下发训练方案</text>
  125. </view>
  126. </view>
  127. </view>
  128. <!-- 添加项目弹窗 -->
  129. <view class="modal-mask" v-if="showItemModal" @click="showItemModal = false">
  130. <view class="modal-card" @click.stop>
  131. <text class="modal-title">添加训练项目</text>
  132. <view class="modal-form">
  133. <view class="form-item">
  134. <text class="form-label">认知维度</text>
  135. <picker class="dim-picker" :value="dimPickerIdx" :range="dimensionOptions" range-key="label" @change="onDimChange">
  136. <text class="picker-text">{{ newItem.dimensionKey ? dimensionLabel(newItem.dimensionKey) : '请选择维度' }}</text>
  137. </picker>
  138. </view>
  139. <view class="form-item">
  140. <text class="form-label">任务标题</text>
  141. <input class="form-input" v-model="newItem.taskTitle" placeholder="请输入任务标题" />
  142. </view>
  143. <view class="form-item">
  144. <text class="form-label">任务描述</text>
  145. <textarea class="form-textarea" v-model="newItem.taskDesc" placeholder="请输入任务描述" />
  146. </view>
  147. <view class="form-item">
  148. <text class="form-label">建议文案</text>
  149. <textarea class="form-textarea" v-model="newItem.suggestion" placeholder="给家长的建议" />
  150. </view>
  151. <view class="form-item-row">
  152. <view class="form-item half">
  153. <text class="form-label">积分</text>
  154. <input class="form-input" type="number" v-model="newItem.taskPoints" />
  155. </view>
  156. <view class="form-item half">
  157. <text class="form-label">截止天数</text>
  158. <input class="form-input" type="number" v-model="newItem.deadlineDays" />
  159. </view>
  160. </view>
  161. </view>
  162. <view class="modal-actions">
  163. <view class="btn-cancel" @click="showItemModal = false"><text>取消</text></view>
  164. <view class="btn-confirm" @click="addItemConfirm"><text>确认添加</text></view>
  165. </view>
  166. </view>
  167. </view>
  168. <!-- 新建模式: 选择孩子 -->
  169. <view class="modal-mask" v-if="showChildModal" @click="showChildModal = false">
  170. <view class="modal-card" @click.stop>
  171. <text class="modal-title">选择孩子</text>
  172. <view class="modal-child-list">
  173. <view class="modal-child-item" v-for="child in bindedChildren" :key="child.id"
  174. @click="selectCreateChild(child.id)">
  175. <text>{{ child.name || '孩子' + child.id }}</text>
  176. </view>
  177. <view class="empty-small-text" v-if="bindedChildren.length === 0">暂无可用孩子</view>
  178. </view>
  179. <view class="modal-footer">
  180. <view class="btn-cancel" @click="showChildModal = false"><text>取消</text></view>
  181. </view>
  182. </view>
  183. </view>
  184. <view class="loading" v-if="loading"><text class="loading-text">加载中...</text></view>
  185. </view>
  186. </template>
  187. <script>
  188. import { getTrainingPlanDetail, createTrainingPlan, updateTrainingPlan, addTrainingPlanItem, updateTrainingPlanItem, deleteTrainingPlanItem, deployTrainingPlan, getExecutionBoard, getGuideBindedChildren } from '../../../utils/api.js'
  189. export default {
  190. data() {
  191. return {
  192. planId: null,
  193. isNew: false,
  194. mode: 'edit',
  195. planForm: { title: '', description: '', status: 'draft' },
  196. items: [],
  197. boardData: null,
  198. loading: false,
  199. editable: true,
  200. showItemModal: false,
  201. showChildModal: false,
  202. bindedChildren: [],
  203. newItem: { dimensionKey: '', taskTitle: '', taskDesc: '', suggestion: '', taskPoints: 2, deadlineDays: 7 },
  204. dimPickerIdx: 0,
  205. dimensionOptions: [
  206. { key: 'perception', label: '感知能力' },
  207. { key: 'focus', label: '专注力' },
  208. { key: 'memory', label: '记忆力' },
  209. { key: 'logic', label: '逻辑思维' },
  210. { key: 'spatial', label: '空间思维' },
  211. { key: 'processingSpeed', label: '加工速度' }
  212. ]
  213. }
  214. },
  215. onLoad(options) {
  216. this.planId = options.planId ? parseInt(options.planId) : null
  217. this.mode = options.mode || 'edit'
  218. if (!this.planId || this.planId === 0) {
  219. this.isNew = true
  220. this.planForm = { title: '认知训练方案', description: '', status: 'draft' }
  221. this.showChildModalForNew()
  222. } else {
  223. this.loadDetail()
  224. }
  225. },
  226. methods: {
  227. loadDetail() {
  228. var self = this
  229. self.loading = true
  230. if (self.mode === 'board') {
  231. getExecutionBoard(self.planId).then(function(res) {
  232. self.loading = false
  233. if (res.code === 200) {
  234. self.boardData = res.data
  235. }
  236. }).catch(function() { self.loading = false })
  237. } else {
  238. getTrainingPlanDetail(self.planId).then(function(res) {
  239. self.loading = false
  240. if (res.code === 200 && res.data) {
  241. self.planForm = res.data.plan || {}
  242. self.items = res.data.items || []
  243. self.editable = self.planForm.status === 'draft'
  244. }
  245. }).catch(function() { self.loading = false })
  246. }
  247. },
  248. showChildModalForNew() {
  249. getGuideBindedChildren().then(function(res) {
  250. if (res.code === 200 && res.data) {
  251. var children = []
  252. var data = res.data
  253. for (var i = 0; i < data.length; i++) {
  254. var family = data[i]
  255. if (family.children) {
  256. for (var j = 0; j < family.children.length; j++) {
  257. children.push(family.children[j])
  258. }
  259. }
  260. if (family.members) {
  261. for (var k = 0; k < family.members.length; k++) {
  262. var m = family.members[k]
  263. if (m.childId) children.push({ id: m.childId, name: m.name || m.childName })
  264. }
  265. }
  266. }
  267. self.bindedChildren = children
  268. }
  269. }).catch(function() {})
  270. this.showChildModal = true
  271. },
  272. selectCreateChild(childId) {
  273. var self = this
  274. self.showChildModal = false
  275. createTrainingPlan(childId, self.planForm.title, self.planForm.description).then(function(res) {
  276. if (res.code === 200 && res.data) {
  277. self.planId = res.data
  278. self.isNew = false
  279. uni.showToast({ title: '方案已创建', icon: 'success' })
  280. setTimeout(function() {
  281. uni.redirectTo({ url: '/pages/guide/training-plans/detail?planId=' + self.planId })
  282. }, 1000)
  283. } else {
  284. uni.showToast({ title: res.message || '创建失败', icon: 'none' })
  285. }
  286. }).catch(function() {
  287. uni.showToast({ title: '网络异常', icon: 'none' })
  288. })
  289. },
  290. savePlan() {
  291. var self = this
  292. updateTrainingPlan(self.planId, self.planForm.title, self.planForm.description).then(function(res) {
  293. if (res.code === 200) {
  294. uni.showToast({ title: '保存成功', icon: 'success' })
  295. } else {
  296. uni.showToast({ title: res.message || '保存失败', icon: 'none' })
  297. }
  298. }).catch(function() {
  299. uni.showToast({ title: '网络异常', icon: 'none' })
  300. })
  301. },
  302. showAddItem() {
  303. this.newItem = { dimensionKey: '', taskTitle: '', taskDesc: '', suggestion: '', taskPoints: 2, deadlineDays: 7 }
  304. this.dimPickerIdx = 0
  305. this.showItemModal = true
  306. },
  307. onDimChange(e) {
  308. this.dimPickerIdx = e.detail.value
  309. this.newItem.dimensionKey = this.dimensionOptions[this.dimPickerIdx].key
  310. },
  311. addItemConfirm() {
  312. var self = this
  313. if (!this.newItem.dimensionKey) {
  314. uni.showToast({ title: '请选择维度', icon: 'none' })
  315. return
  316. }
  317. if (!this.newItem.taskTitle) {
  318. uni.showToast({ title: '请输入任务标题', icon: 'none' })
  319. return
  320. }
  321. var payload = {
  322. planId: this.planId,
  323. dimensionKey: this.newItem.dimensionKey,
  324. taskTitle: this.newItem.taskTitle,
  325. taskDesc: this.newItem.taskDesc,
  326. suggestion: this.newItem.suggestion,
  327. taskPoints: parseInt(this.newItem.taskPoints) || 2,
  328. deadlineDays: parseInt(this.newItem.deadlineDays) || 7,
  329. sequence: this.items.length + 1
  330. }
  331. addTrainingPlanItem(payload).then(function(res) {
  332. if (res.code === 200) {
  333. self.showItemModal = false
  334. uni.showToast({ title: '已添加', icon: 'success' })
  335. self.loadDetail()
  336. } else {
  337. uni.showToast({ title: res.message || '添加失败', icon: 'none' })
  338. }
  339. }).catch(function() {
  340. uni.showToast({ title: '网络异常', icon: 'none' })
  341. })
  342. },
  343. updateItemField(item) {
  344. updateTrainingPlanItem(item).catch(function() {
  345. uni.showToast({ title: '保存失败', icon: 'none' })
  346. })
  347. },
  348. deleteItem(itemId, idx) {
  349. var self = this
  350. if (!itemId) {
  351. self.items.splice(idx, 1)
  352. return
  353. }
  354. uni.showModal({
  355. title: '确认删除',
  356. content: '确定删除此项目?',
  357. success: function(res) {
  358. if (res.confirm) {
  359. deleteTrainingPlanItem(itemId).then(function(r) {
  360. if (r.code === 200) {
  361. self.items.splice(idx, 1)
  362. uni.showToast({ title: '已删除', icon: 'success' })
  363. }
  364. }).catch(function() {})
  365. }
  366. }
  367. })
  368. },
  369. handleDeploy() {
  370. var self = this
  371. uni.showModal({
  372. title: '确认下发',
  373. content: '将一次性创建所有训练任务,是否继续?',
  374. success: function(res) {
  375. if (res.confirm) {
  376. uni.showLoading({ title: '下发中...' })
  377. deployTrainingPlan(self.planId).then(function(r) {
  378. uni.hideLoading()
  379. if (r.code === 200) {
  380. uni.showToast({ title: '下发成功', icon: 'success' })
  381. self.loadDetail()
  382. } else {
  383. uni.showToast({ title: r.message || '下发失败', icon: 'none' })
  384. }
  385. }).catch(function() {
  386. uni.hideLoading()
  387. uni.showToast({ title: '网络异常', icon: 'none' })
  388. })
  389. }
  390. }
  391. })
  392. },
  393. dimensionLabel(key) {
  394. var map = { perception: '感知', focus: '专注', memory: '记忆', logic: '逻辑', spatial: '空间', processingSpeed: '加工速度' }
  395. return map[key] || key || ''
  396. },
  397. statusText(status) {
  398. var map = { draft: '草稿', published: '已下发', completed: '已完成' }
  399. return map[status] || status || '草稿'
  400. },
  401. itemStatusText(status) {
  402. var map = { pending: '待完成', completed: '已完成', skipped: '已跳过' }
  403. return map[status] || status || '待完成'
  404. }
  405. }
  406. }
  407. </script>
  408. <style scoped>
  409. .container {
  410. min-height: 100vh;
  411. background: #f5f7fa;
  412. padding-bottom: 60rpx;
  413. }
  414. /* ===== 看板 ===== */
  415. .board-section {
  416. padding: 30rpx;
  417. }
  418. .board-header {
  419. display: flex;
  420. flex-direction: row;
  421. justify-content: space-between;
  422. align-items: center;
  423. margin-bottom: 16rpx;
  424. }
  425. .board-title {
  426. font-size: 34rpx;
  427. font-weight: bold;
  428. color: #333;
  429. }
  430. .board-progress {
  431. font-size: 40rpx;
  432. font-weight: bold;
  433. color: #667eea;
  434. }
  435. .progress-bar-wrap {
  436. height: 12rpx;
  437. background: #e5e7eb;
  438. border-radius: 6rpx;
  439. overflow: hidden;
  440. margin-bottom: 24rpx;
  441. }
  442. .progress-bar-fill {
  443. height: 100%;
  444. background: linear-gradient(90deg, #667eea, #764ba2);
  445. border-radius: 6rpx;
  446. transition: width 0.3s;
  447. }
  448. .board-summary {
  449. display: flex;
  450. flex-direction: row;
  451. gap: 20rpx;
  452. margin-bottom: 24rpx;
  453. }
  454. .summary-item {
  455. flex: 1;
  456. background: #fff;
  457. border-radius: 16rpx;
  458. padding: 24rpx;
  459. text-align: center;
  460. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05);
  461. }
  462. .summary-value {
  463. font-size: 40rpx;
  464. font-weight: bold;
  465. color: #667eea;
  466. display: block;
  467. }
  468. .summary-label {
  469. font-size: 24rpx;
  470. color: #888;
  471. display: block;
  472. margin-top: 4rpx;
  473. }
  474. .board-items {
  475. background: #fff;
  476. border-radius: 20rpx;
  477. overflow: hidden;
  478. }
  479. .board-item {
  480. display: flex;
  481. flex-direction: row;
  482. align-items: center;
  483. padding: 24rpx;
  484. border-bottom: 1rpx solid #f0f0f0;
  485. }
  486. .board-item:last-child { border-bottom: none; }
  487. .bi-left {
  488. display: flex;
  489. flex-direction: row;
  490. align-items: center;
  491. flex: 1;
  492. gap: 16rpx;
  493. }
  494. .bi-check {
  495. width: 44rpx;
  496. height: 44rpx;
  497. border-radius: 50%;
  498. border: 3rpx solid #e5e7eb;
  499. display: flex;
  500. align-items: center;
  501. justify-content: center;
  502. font-size: 22rpx;
  503. color: #d1d5db;
  504. flex-shrink: 0;
  505. }
  506. .bi-check.done {
  507. background: #22c55e;
  508. border-color: #22c55e;
  509. color: #fff;
  510. }
  511. .bi-info {
  512. display: flex;
  513. flex-direction: column;
  514. gap: 4rpx;
  515. }
  516. .bi-dim {
  517. font-size: 22rpx;
  518. color: #888;
  519. }
  520. .bi-title {
  521. font-size: 28rpx;
  522. color: #333;
  523. font-weight: 500;
  524. }
  525. .bi-right {}
  526. .bi-status {
  527. font-size: 22rpx;
  528. padding: 4rpx 16rpx;
  529. border-radius: 16rpx;
  530. }
  531. .bi-pending { background: #f3f4f6; color: #666; }
  532. .bi-completed { background: #d1fae5; color: #059669; }
  533. .bi-skipped { background: #fee2e2; color: #dc2626; }
  534. /* ===== 编辑模式 ===== */
  535. .edit-section {
  536. padding: 20rpx 30rpx;
  537. }
  538. .section-card {
  539. background: #fff;
  540. border-radius: 20rpx;
  541. padding: 28rpx;
  542. margin-bottom: 24rpx;
  543. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
  544. }
  545. .section-header {
  546. display: flex;
  547. flex-direction: row;
  548. justify-content: space-between;
  549. align-items: center;
  550. margin-bottom: 16rpx;
  551. }
  552. .section-title {
  553. font-size: 30rpx;
  554. font-weight: bold;
  555. color: #333;
  556. margin-bottom: 16rpx;
  557. }
  558. .section-header .section-title {
  559. margin-bottom: 0;
  560. }
  561. .form-item {
  562. margin-bottom: 20rpx;
  563. }
  564. .form-item:last-child { margin-bottom: 0; }
  565. .form-label {
  566. font-size: 26rpx;
  567. color: #888;
  568. display: block;
  569. margin-bottom: 8rpx;
  570. }
  571. .form-input {
  572. width: 100%;
  573. height: 72rpx;
  574. background: #f9fafb;
  575. border-radius: 12rpx;
  576. padding: 0 20rpx;
  577. font-size: 28rpx;
  578. color: #333;
  579. box-sizing: border-box;
  580. border: 1rpx solid #e5e7eb;
  581. }
  582. .form-textarea {
  583. width: 100%;
  584. background: #f9fafb;
  585. border-radius: 12rpx;
  586. padding: 16rpx 20rpx;
  587. font-size: 28rpx;
  588. color: #333;
  589. box-sizing: border-box;
  590. border: 1rpx solid #e5e7eb;
  591. min-height: 120rpx;
  592. display: block;
  593. }
  594. .form-actions {
  595. display: flex;
  596. flex-direction: row;
  597. justify-content: flex-end;
  598. margin-top: 20rpx;
  599. }
  600. .btn-save {
  601. background: linear-gradient(135deg, #667eea, #764ba2);
  602. padding: 16rpx 48rpx;
  603. border-radius: 32rpx;
  604. }
  605. .btn-save:active { opacity: 0.8; }
  606. .btn-save text {
  607. color: #fff;
  608. font-size: 28rpx;
  609. font-weight: 500;
  610. }
  611. .status-badge {
  612. display: inline-block;
  613. font-size: 24rpx;
  614. padding: 6rpx 20rpx;
  615. border-radius: 16rpx;
  616. }
  617. .status-draft { background: #f3f4f6; color: #666; }
  618. .status-published { background: #dbeafe; color: #2563eb; }
  619. .status-completed { background: #d1fae5; color: #059669; }
  620. /* 添加项目按钮 */
  621. .btn-add-item {
  622. background: #eef2ff;
  623. padding: 8rpx 24rpx;
  624. border-radius: 20rpx;
  625. }
  626. .btn-add-item:active { opacity: 0.7; }
  627. .btn-add-item text {
  628. font-size: 26rpx;
  629. color: #667eea;
  630. font-weight: 500;
  631. }
  632. /* 项目列表 */
  633. .item-list {
  634. display: flex;
  635. flex-direction: column;
  636. gap: 16rpx;
  637. }
  638. .plan-item {
  639. background: #f9fafb;
  640. border-radius: 16rpx;
  641. padding: 20rpx;
  642. border: 1rpx solid #e5e7eb;
  643. }
  644. .item-header {
  645. display: flex;
  646. flex-direction: row;
  647. align-items: center;
  648. gap: 12rpx;
  649. margin-bottom: 12rpx;
  650. }
  651. .item-seq {
  652. width: 36rpx;
  653. height: 36rpx;
  654. background: #667eea;
  655. border-radius: 50%;
  656. color: #fff;
  657. font-size: 22rpx;
  658. display: flex;
  659. align-items: center;
  660. justify-content: center;
  661. font-weight: bold;
  662. }
  663. .item-dim-tag {
  664. background: #eef2ff;
  665. color: #667eea;
  666. font-size: 22rpx;
  667. padding: 4rpx 14rpx;
  668. border-radius: 12rpx;
  669. font-weight: 500;
  670. }
  671. .item-status-badge {
  672. font-size: 22rpx;
  673. padding: 4rpx 14rpx;
  674. border-radius: 12rpx;
  675. }
  676. .item-pending { background: #f3f4f6; color: #666; }
  677. .item-completed { background: #d1fae5; color: #059669; }
  678. .item-body {
  679. display: flex;
  680. flex-direction: column;
  681. gap: 10rpx;
  682. }
  683. .item-field {
  684. display: flex;
  685. flex-direction: column;
  686. gap: 4rpx;
  687. }
  688. .item-field-row {
  689. display: flex;
  690. flex-direction: row;
  691. gap: 12rpx;
  692. }
  693. .item-field.half {
  694. flex: 1;
  695. }
  696. .item-field-label {
  697. font-size: 22rpx;
  698. color: #999;
  699. }
  700. .item-field-value {
  701. font-size: 26rpx;
  702. color: #333;
  703. }
  704. .item-input {
  705. background: #fff;
  706. border: 1rpx solid #e5e7eb;
  707. border-radius: 8rpx;
  708. padding: 8rpx 12rpx;
  709. font-size: 26rpx;
  710. color: #333;
  711. }
  712. .item-actions {
  713. display: flex;
  714. flex-direction: row;
  715. justify-content: flex-end;
  716. margin-top: 10rpx;
  717. border-top: 1rpx solid #e5e7eb;
  718. padding-top: 10rpx;
  719. }
  720. .item-action-btn {
  721. padding: 8rpx 20rpx;
  722. border-radius: 16rpx;
  723. font-size: 24rpx;
  724. }
  725. .item-action-btn.delete {
  726. background: #fee2e2;
  727. color: #dc2626;
  728. }
  729. .item-action-btn.delete:active { opacity: 0.7; }
  730. .empty-items {
  731. padding: 40rpx 0;
  732. text-align: center;
  733. }
  734. .empty-items-text {
  735. font-size: 26rpx;
  736. color: #999;
  737. }
  738. /* 底部操作栏 */
  739. .action-bar {
  740. position: fixed;
  741. bottom: 0;
  742. left: 0;
  743. right: 0;
  744. background: #fff;
  745. padding: 20rpx 30rpx;
  746. box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.06);
  747. display: flex;
  748. flex-direction: row;
  749. justify-content: center;
  750. }
  751. .btn-deploy {
  752. background: linear-gradient(135deg, #667eea, #764ba2);
  753. padding: 20rpx 80rpx;
  754. border-radius: 40rpx;
  755. box-shadow: 0 4rpx 16rpx rgba(102,126,234,0.3);
  756. }
  757. .btn-deploy:active { opacity: 0.85; }
  758. .btn-deploy text {
  759. color: #fff;
  760. font-size: 30rpx;
  761. font-weight: bold;
  762. }
  763. /* 弹窗 */
  764. .modal-mask {
  765. position: fixed;
  766. top: 0;
  767. left: 0;
  768. right: 0;
  769. bottom: 0;
  770. background: rgba(0,0,0,0.5);
  771. z-index: 100;
  772. display: flex;
  773. align-items: center;
  774. justify-content: center;
  775. }
  776. .modal-card {
  777. background: #fff;
  778. border-radius: 24rpx;
  779. padding: 40rpx;
  780. width: 640rpx;
  781. max-height: 75vh;
  782. overflow-y: auto;
  783. }
  784. .modal-title {
  785. font-size: 32rpx;
  786. font-weight: bold;
  787. color: #333;
  788. display: block;
  789. margin-bottom: 24rpx;
  790. }
  791. .modal-form {
  792. margin-bottom: 24rpx;
  793. }
  794. .form-item-row {
  795. display: flex;
  796. flex-direction: row;
  797. gap: 16rpx;
  798. }
  799. .form-item.half {
  800. flex: 1;
  801. }
  802. .dim-picker {
  803. background: #f9fafb;
  804. border: 1rpx solid #e5e7eb;
  805. border-radius: 12rpx;
  806. padding: 16rpx 20rpx;
  807. }
  808. .picker-text {
  809. font-size: 28rpx;
  810. color: #333;
  811. }
  812. .modal-actions {
  813. display: flex;
  814. flex-direction: row;
  815. gap: 16rpx;
  816. justify-content: center;
  817. }
  818. .btn-cancel, .btn-confirm {
  819. padding: 16rpx 48rpx;
  820. border-radius: 32rpx;
  821. font-size: 28rpx;
  822. }
  823. .btn-cancel {
  824. background: #f3f4f6;
  825. color: #666;
  826. }
  827. .btn-confirm {
  828. background: linear-gradient(135deg, #667eea, #764ba2);
  829. color: #fff;
  830. }
  831. .btn-cancel:active, .btn-confirm:active { opacity: 0.7; }
  832. .modal-child-list {
  833. display: flex;
  834. flex-direction: column;
  835. gap: 12rpx;
  836. margin-bottom: 20rpx;
  837. }
  838. .modal-child-item {
  839. padding: 20rpx;
  840. background: #f9fafb;
  841. border-radius: 12rpx;
  842. }
  843. .modal-child-item:active { background: #eef2ff; }
  844. .modal-child-item text {
  845. font-size: 28rpx;
  846. color: #333;
  847. }
  848. .modal-footer {
  849. display: flex;
  850. flex-direction: row;
  851. justify-content: center;
  852. }
  853. .empty-small-text {
  854. font-size: 26rpx;
  855. color: #999;
  856. text-align: center;
  857. padding: 20rpx 0;
  858. }
  859. .loading {
  860. display: flex;
  861. align-items: center;
  862. justify-content: center;
  863. padding: 80rpx;
  864. }
  865. .loading-text {
  866. font-size: 28rpx;
  867. color: #999;
  868. }
  869. </style>