create.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. <template>
  2. <view class="container">
  3. <view class="form-card">
  4. <view class="form-title">{{ isNew ? '创建活动' : '编辑活动' }}</view>
  5. <!-- 活动标题 -->
  6. <view class="form-item">
  7. <text class="form-label">活动标题 *</text>
  8. <input class="form-input" v-model="form.title" placeholder="请输入活动标题" maxlength="50" />
  9. </view>
  10. <!-- 活动维度 -->
  11. <view class="form-item">
  12. <text class="form-label">所属维度 *</text>
  13. <picker class="form-picker" :value="dimIdx" :range="dimensionOptions" range-key="label" @change="onDimChange">
  14. <text class="picker-text">{{ form.dimensionCode ? dimLabel(form.dimensionCode) : '请选择维度' }}</text>
  15. </picker>
  16. </view>
  17. <!-- 活动类型 -->
  18. <view class="form-item">
  19. <text class="form-label">活动类型 *</text>
  20. <picker class="form-picker" :value="typeIdx" :range="typeOptions" range-key="label" @change="onTypeChange">
  21. <text class="picker-text">{{ form.activityType ? typeLabel(form.activityType) : '请选择类型' }}</text>
  22. </picker>
  23. </view>
  24. <!-- 开始时间 -->
  25. <view class="form-item">
  26. <text class="form-label">开始时间 *</text>
  27. <picker class="form-picker" mode="date" :value="datePart" @change="onStartDateChange">
  28. <text class="picker-text">{{ datePart || '选择日期' }}</text>
  29. </picker>
  30. <picker class="form-picker" mode="time" :value="timePart" @change="onStartTimeChange" style="margin-top: 16rpx;">
  31. <text class="picker-text">{{ timePart || '选择时间' }}</text>
  32. </picker>
  33. </view>
  34. <!-- 结束时间 -->
  35. <view class="form-item">
  36. <text class="form-label">结束时间</text>
  37. <picker class="form-picker" mode="date" :value="endDatePart" @change="onEndDateChange">
  38. <text class="picker-text">{{ endDatePart || '选择日期(可选)' }}</text>
  39. </picker>
  40. <picker class="form-picker" mode="time" :value="endTimePart" @change="onEndTimeChange" style="margin-top: 16rpx;">
  41. <text class="picker-text">{{ endTimePart || '选择时间(可选)' }}</text>
  42. </picker>
  43. </view>
  44. <!-- 报名截止时间 -->
  45. <view class="form-item">
  46. <text class="form-label">报名截止时间</text>
  47. <picker class="form-picker" mode="date" :value="deadlineDatePart" @change="onDeadlineDateChange">
  48. <text class="picker-text">{{ deadlineDatePart || '选择日期(可选)' }}</text>
  49. </picker>
  50. <picker class="form-picker" mode="time" :value="deadlineTimePart" @change="onDeadlineTimeChange" style="margin-top: 16rpx;">
  51. <text class="picker-text">{{ deadlineTimePart || '选择时间(可选)' }}</text>
  52. </picker>
  53. </view>
  54. <!-- 活动地点 -->
  55. <view class="form-item">
  56. <text class="form-label">活动地点</text>
  57. <input class="form-input" v-model="form.location" placeholder="请输入活动地点" maxlength="100" />
  58. </view>
  59. <!-- 人数 + 价格 -->
  60. <view class="form-item-row">
  61. <view class="form-item half">
  62. <text class="form-label">最大人数</text>
  63. <input class="form-input" type="number" v-model="maxParticipantsStr" placeholder="0=不限" />
  64. </view>
  65. <view class="form-item half">
  66. <text class="form-label">价格(元)</text>
  67. <input class="form-input" type="digit" v-model="priceYuan" placeholder="0=免费" />
  68. </view>
  69. </view>
  70. <!-- 会员价 -->
  71. <view class="form-item">
  72. <text class="form-label">会员价(元)</text>
  73. <input class="form-input" type="digit" v-model="memberPriceYuan" placeholder="留空等于普通价" />
  74. </view>
  75. <!-- 封面图片 -->
  76. <view class="form-item">
  77. <text class="form-label">封面图片</text>
  78. <input class="form-input" v-model="form.coverImage" placeholder="输入图片URL(可选)" />
  79. <view class="cover-preview" v-if="form.coverImage">
  80. <image class="preview-img" :src="form.coverImage" mode="aspectFill" @error="onImgError" />
  81. </view>
  82. </view>
  83. <!-- 活动描述 -->
  84. <view class="form-item">
  85. <text class="form-label">活动描述</text>
  86. <textarea class="form-textarea" v-model="form.description" placeholder="请输入活动详细介绍" maxlength="500"
  87. auto-height />
  88. </view>
  89. <!-- 操作按钮 -->
  90. <view class="form-actions">
  91. <view class="btn-save" @click="handleSave">
  92. <text>保存为草稿</text>
  93. </view>
  94. <view class="btn-publish" @click="handlePublish">
  95. <text>保存并发布</text>
  96. </view>
  97. </view>
  98. </view>
  99. <view class="loading" v-if="loading">
  100. <view class="loading-spinner"></view>
  101. <text>加载中...</text>
  102. </view>
  103. </view>
  104. </template>
  105. <script>
  106. import { adminCreateActivity, adminUpdateActivity, adminPublishActivity, getActivityDetail } from '../../utils/api.js'
  107. import config from '../../config.js'
  108. var BASE_URL = config.api('')
  109. export default {
  110. data() {
  111. return {
  112. isNew: true,
  113. activityId: null,
  114. loading: false,
  115. childId: '',
  116. form: {
  117. title: '',
  118. description: '',
  119. coverImage: '',
  120. dimensionCode: '',
  121. activityType: '',
  122. status: 'draft',
  123. startTime: null,
  124. endTime: null,
  125. registrationDeadline: null,
  126. location: '',
  127. maxParticipants: 0,
  128. price: 0,
  129. memberPrice: 0,
  130. visibility: 'public',
  131. requireRegistration: 1,
  132. requireCheckinReview: 0,
  133. checkinPoints: 0
  134. },
  135. priceYuan: '0',
  136. memberPriceYuan: '',
  137. maxParticipantsStr: '0',
  138. datePart: '',
  139. timePart: '',
  140. endDatePart: '',
  141. endTimePart: '',
  142. deadlineDatePart: '',
  143. deadlineTimePart: '',
  144. dimIdx: 0,
  145. typeIdx: 0,
  146. dimensionOptions: [
  147. { key: 'body', label: '身 · 主动健康' },
  148. { key: 'mind', label: '心 · 情感丰盈' },
  149. { key: 'wisdom', label: '智 · 赋能未来' },
  150. { key: 'action', label: '行 · 知行合一' },
  151. { key: 'wealth', label: '富 · 物质精神' }
  152. ],
  153. typeOptions: [
  154. { key: 'offline', label: '线下活动' },
  155. { key: 'online', label: '线上活动' },
  156. { key: 'campaign', label: '营销活动' }
  157. ]
  158. }
  159. },
  160. onLoad(options) {
  161. if (options.id) {
  162. this.activityId = parseInt(options.id)
  163. this.isNew = false
  164. uni.setNavigationBarTitle({ title: '编辑活动' })
  165. this.loadDetail()
  166. } else {
  167. uni.setNavigationBarTitle({ title: '创建活动' })
  168. }
  169. },
  170. methods: {
  171. loadDetail() {
  172. var self = this
  173. self.loading = true
  174. getActivityDetail(self.activityId).then(function(res) {
  175. self.loading = false
  176. if (res && res.code === 200 && res.data) {
  177. var d = res.data
  178. self.form.title = d.title || ''
  179. self.form.description = d.description || ''
  180. self.form.coverImage = d.coverImage || ''
  181. self.form.dimensionCode = d.dimensionCode || ''
  182. self.form.activityType = d.activityType || ''
  183. self.form.location = d.location || ''
  184. self.form.maxParticipants = d.maxParticipants || 0
  185. self.form.price = d.price || 0
  186. self.form.memberPrice = d.memberPrice || 0
  187. self.form.status = d.status || 'draft'
  188. self.form.visibility = d.visibility || 'public'
  189. self.maxParticipantsStr = String(self.form.maxParticipants)
  190. self.priceYuan = self.form.price ? (self.form.price / 100).toString() : '0'
  191. self.memberPriceYuan = self.form.memberPrice && self.form.memberPrice > 0 ? (self.form.memberPrice / 100).toString() : ''
  192. if (d.startTime) {
  193. var sd = new Date(d.startTime.replace ? d.startTime.replace(/-/g, '/') : d.startTime)
  194. self.datePart = self.fmtDate(sd)
  195. self.timePart = self.fmtTime(sd)
  196. self.form.startTime = sd.toISOString()
  197. }
  198. if (d.endTime) {
  199. var ed = new Date(d.endTime.replace ? d.endTime.replace(/-/g, '/') : d.endTime)
  200. self.endDatePart = self.fmtDate(ed)
  201. self.endTimePart = self.fmtTime(ed)
  202. self.form.endTime = ed.toISOString()
  203. }
  204. if (d.registrationDeadline) {
  205. var dl = new Date(d.registrationDeadline.replace ? d.registrationDeadline.replace(/-/g, '/') : d.registrationDeadline)
  206. self.deadlineDatePart = self.fmtDate(dl)
  207. self.deadlineTimePart = self.fmtTime(dl)
  208. self.form.registrationDeadline = dl.toISOString()
  209. }
  210. // sync pickers
  211. for (var i = 0; i < self.dimensionOptions.length; i++) {
  212. if (self.dimensionOptions[i].key === self.form.dimensionCode) self.dimIdx = i
  213. }
  214. for (var j = 0; j < self.typeOptions.length; j++) {
  215. if (self.typeOptions[j].key === self.form.activityType) self.typeIdx = j
  216. }
  217. }
  218. }).catch(function() { self.loading = false })
  219. },
  220. onDimChange(e) {
  221. this.dimIdx = e.detail.value
  222. this.form.dimensionCode = this.dimensionOptions[this.dimIdx].key
  223. },
  224. onTypeChange(e) {
  225. this.typeIdx = e.detail.value
  226. this.form.activityType = this.typeOptions[this.typeIdx].key
  227. },
  228. onStartDateChange(e) {
  229. this.datePart = e.detail.value
  230. this.syncStartTime()
  231. },
  232. onStartTimeChange(e) {
  233. this.timePart = e.detail.value
  234. this.syncStartTime()
  235. },
  236. onEndDateChange(e) {
  237. this.endDatePart = e.detail.value
  238. this.syncEndTime()
  239. },
  240. onEndTimeChange(e) {
  241. this.endTimePart = e.detail.value
  242. this.syncEndTime()
  243. },
  244. onDeadlineDateChange(e) {
  245. this.deadlineDatePart = e.detail.value
  246. this.syncDeadline()
  247. },
  248. onDeadlineTimeChange(e) {
  249. this.deadlineTimePart = e.detail.value
  250. this.syncDeadline()
  251. },
  252. syncStartTime() {
  253. if (this.datePart && this.timePart) {
  254. this.form.startTime = this.datePart + 'T' + this.timePart + ':00'
  255. }
  256. },
  257. syncEndTime() {
  258. if (this.endDatePart && this.endTimePart) {
  259. this.form.endTime = this.endDatePart + 'T' + this.endTimePart + ':00'
  260. } else {
  261. this.form.endTime = null
  262. }
  263. },
  264. syncDeadline() {
  265. if (this.deadlineDatePart && this.deadlineTimePart) {
  266. this.form.registrationDeadline = this.deadlineDatePart + 'T' + this.deadlineTimePart + ':00'
  267. } else {
  268. this.form.registrationDeadline = null
  269. }
  270. },
  271. chooseImage() {
  272. var self = this
  273. uni.chooseImage({
  274. count: 1,
  275. sizeType: ['compressed'],
  276. success: function(res) {
  277. self.uploadCover(res.tempFilePaths[0])
  278. }
  279. })
  280. },
  281. uploadCover(filePath) {
  282. var self = this
  283. uni.uploadFile({
  284. url: BASE_URL + '/api/admin/articles/upload/image',
  285. filePath: filePath,
  286. name: 'file',
  287. header: { 'Authorization': 'Bearer ' + (uni.getStorageSync('token') || '') },
  288. success: function(res) {
  289. try {
  290. var data = JSON.parse(res.data)
  291. if (data.code === 200 && data.data) {
  292. self.form.coverImage = data.data
  293. } else {
  294. uni.showToast({ title: '上传失败', icon: 'none' })
  295. }
  296. } catch (e) {
  297. uni.showToast({ title: '上传失败', icon: 'none' })
  298. }
  299. },
  300. fail: function() {
  301. uni.showToast({ title: '上传失败', icon: 'none' })
  302. }
  303. })
  304. },
  305. onImgError() {
  306. this.form.coverImage = ''
  307. },
  308. validate() {
  309. if (!this.form.title) {
  310. uni.showToast({ title: '请输入活动标题', icon: 'none' })
  311. return false
  312. }
  313. if (!this.form.dimensionCode) {
  314. uni.showToast({ title: '请选择所属维度', icon: 'none' })
  315. return false
  316. }
  317. if (!this.form.activityType) {
  318. uni.showToast({ title: '请选择活动类型', icon: 'none' })
  319. return false
  320. }
  321. if (!this.datePart || !this.timePart) {
  322. uni.showToast({ title: '请选择开始时间', icon: 'none' })
  323. return false
  324. }
  325. return true
  326. },
  327. buildPayload() {
  328. var payload = Object.assign({}, this.form)
  329. payload.maxParticipants = parseInt(this.maxParticipantsStr) || 0
  330. payload.price = Math.round((parseFloat(this.priceYuan) || 0) * 100)
  331. payload.memberPrice = this.memberPriceYuan ? Math.round(parseFloat(this.memberPriceYuan) * 100) : payload.price
  332. if (this.activityId) payload.id = this.activityId
  333. return payload
  334. },
  335. handleSave() {
  336. if (!this.validate()) return
  337. this.submit(false)
  338. },
  339. handlePublish() {
  340. if (!this.validate()) return
  341. this.submit(true)
  342. },
  343. submit(publishAfter) {
  344. var self = this
  345. self.loading = true
  346. var payload = self.buildPayload()
  347. var doPublish = function(id) {
  348. adminPublishActivity(id).then(function(r) {
  349. self.loading = false
  350. if (r && r.code === 200) {
  351. uni.showToast({ title: '已发布', icon: 'success' })
  352. setTimeout(function() {
  353. uni.navigateBack()
  354. }, 1200)
  355. } else {
  356. uni.showToast({ title: (r && r.message) || '发布失败', icon: 'none' })
  357. }
  358. }).catch(function() {
  359. self.loading = false
  360. uni.showToast({ title: '发布失败', icon: 'none' })
  361. })
  362. }
  363. if (self.isNew) {
  364. adminCreateActivity(payload).then(function(res) {
  365. if (res && res.code === 200) {
  366. var newId = res.data.id || res.data
  367. if (publishAfter) {
  368. doPublish(newId)
  369. } else {
  370. self.loading = false
  371. uni.showToast({ title: '已保存草稿', icon: 'success' })
  372. setTimeout(function() { uni.navigateBack() }, 1200)
  373. }
  374. } else {
  375. self.loading = false
  376. uni.showToast({ title: (res && res.message) || '保存失败', icon: 'none' })
  377. }
  378. }).catch(function() {
  379. self.loading = false
  380. uni.showToast({ title: '网络异常', icon: 'none' })
  381. })
  382. } else {
  383. adminUpdateActivity(payload).then(function(res) {
  384. if (res && res.code === 200) {
  385. if (publishAfter && self.form.status === 'draft') {
  386. doPublish(self.activityId)
  387. } else {
  388. self.loading = false
  389. uni.showToast({ title: '已保存', icon: 'success' })
  390. setTimeout(function() { uni.navigateBack() }, 1200)
  391. }
  392. } else {
  393. self.loading = false
  394. uni.showToast({ title: (res && res.message) || '保存失败', icon: 'none' })
  395. }
  396. }).catch(function() {
  397. self.loading = false
  398. uni.showToast({ title: '网络异常', icon: 'none' })
  399. })
  400. }
  401. },
  402. dimLabel(key) {
  403. for (var i = 0; i < this.dimensionOptions.length; i++) {
  404. if (this.dimensionOptions[i].key === key) return this.dimensionOptions[i].label
  405. }
  406. return '请选择维度'
  407. },
  408. typeLabel(key) {
  409. for (var i = 0; i < this.typeOptions.length; i++) {
  410. if (this.typeOptions[i].key === key) return this.typeOptions[i].label
  411. }
  412. return '请选择类型'
  413. },
  414. fmtDate(d) {
  415. var y = d.getFullYear()
  416. var m = ('0' + (d.getMonth() + 1)).slice(-2)
  417. var day = ('0' + d.getDate()).slice(-2)
  418. return y + '-' + m + '-' + day
  419. },
  420. fmtTime(d) {
  421. var h = ('0' + d.getHours()).slice(-2)
  422. var mi = ('0' + d.getMinutes()).slice(-2)
  423. return h + ':' + mi
  424. }
  425. }
  426. }
  427. </script>
  428. <style scoped>
  429. .container {
  430. min-height: 100vh;
  431. background: #F5F7FA;
  432. padding: 24rpx;
  433. }
  434. .form-card {
  435. background: #fff;
  436. border-radius: 20rpx;
  437. padding: 32rpx 28rpx;
  438. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
  439. }
  440. .form-title {
  441. font-size: 36rpx;
  442. font-weight: bold;
  443. color: #1E293B;
  444. margin-bottom: 24rpx;
  445. }
  446. .form-item {
  447. margin-bottom: 28rpx;
  448. }
  449. .form-item-row {
  450. display: flex;
  451. flex-direction: row;
  452. gap: 20rpx;
  453. margin-bottom: 28rpx;
  454. }
  455. .form-item.half {
  456. flex: 1;
  457. }
  458. .form-label {
  459. font-size: 26rpx;
  460. color: #475569;
  461. font-weight: 600;
  462. margin-bottom: 12rpx;
  463. display: block;
  464. }
  465. .form-input {
  466. width: 100%;
  467. height: 80rpx;
  468. border: 1rpx solid #E2E8F0;
  469. border-radius: 12rpx;
  470. padding: 0 20rpx;
  471. font-size: 28rpx;
  472. color: #1E293B;
  473. box-sizing: border-box;
  474. }
  475. .form-picker {
  476. width: 100%;
  477. height: 80rpx;
  478. border: 1rpx solid #E2E8F0;
  479. border-radius: 12rpx;
  480. padding: 0 20rpx;
  481. font-size: 28rpx;
  482. color: #1E293B;
  483. box-sizing: border-box;
  484. display: flex;
  485. align-items: center;
  486. }
  487. .picker-text {
  488. font-size: 28rpx;
  489. color: #1E293B;
  490. }
  491. .form-textarea {
  492. width: 100%;
  493. min-height: 160rpx;
  494. border: 1rpx solid #E2E8F0;
  495. border-radius: 12rpx;
  496. padding: 16rpx 20rpx;
  497. font-size: 28rpx;
  498. color: #1E293B;
  499. box-sizing: border-box;
  500. }
  501. .cover-upload {
  502. width: 100%;
  503. height: 320rpx;
  504. border: 2rpx dashed #CBD5E1;
  505. border-radius: 12rpx;
  506. display: flex;
  507. align-items: center;
  508. justify-content: center;
  509. overflow: hidden;
  510. }
  511. .preview-img {
  512. width: 100%;
  513. height: 100%;
  514. }
  515. .upload-placeholder {
  516. display: flex;
  517. flex-direction: column;
  518. align-items: center;
  519. }
  520. .upload-icon {
  521. font-size: 64rpx;
  522. color: #94A3B8;
  523. }
  524. .upload-text {
  525. font-size: 24rpx;
  526. color: #94A3B8;
  527. margin-top: 8rpx;
  528. }
  529. .form-actions {
  530. display: flex;
  531. flex-direction: row;
  532. gap: 20rpx;
  533. margin-top: 40rpx;
  534. }
  535. .btn-save,
  536. .btn-publish {
  537. flex: 1;
  538. height: 88rpx;
  539. border-radius: 44rpx;
  540. display: flex;
  541. align-items: center;
  542. justify-content: center;
  543. font-size: 30rpx;
  544. font-weight: 600;
  545. }
  546. .btn-save {
  547. background: #fff;
  548. color: #F97316;
  549. border: 2rpx solid #F97316;
  550. }
  551. .btn-publish {
  552. background: linear-gradient(135deg, #F97316, #FB923C);
  553. color: #fff;
  554. }
  555. .loading {
  556. position: fixed;
  557. top: 0;
  558. left: 0;
  559. right: 0;
  560. bottom: 0;
  561. background: rgba(255, 255, 255, 0.6);
  562. display: flex;
  563. flex-direction: column;
  564. align-items: center;
  565. justify-content: center;
  566. z-index: 999;
  567. }
  568. .loading-spinner {
  569. width: 60rpx;
  570. height: 60rpx;
  571. border: 4rpx solid #FED7AA;
  572. border-top: 4rpx solid #F97316;
  573. border-radius: 50%;
  574. animation: spin 0.8s linear infinite;
  575. margin-bottom: 16rpx;
  576. }
  577. @keyframes spin {
  578. to { transform: rotate(360deg); }
  579. }
  580. </style>