create.vue 16 KB

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