article-edit.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. <template>
  2. <view class="edit-container">
  3. <!-- 自定义导航 -->
  4. <view class="edit-header">
  5. <view class="header-left" @click="onBack">
  6. <text class="back-icon">←</text>
  7. <text class="back-text">返回</text>
  8. </view>
  9. <text class="header-title">写成长记录</text>
  10. <view class="header-right" @click="goMyPosts">
  11. <text class="history-text">历史</text>
  12. </view>
  13. </view>
  14. <scroll-view scroll-y class="edit-form">
  15. <!-- 封面图 -->
  16. <view class="form-cover" @click="chooseCover">
  17. <image v-if="coverImage" :src="coverImage" mode="aspectFill" class="cover-preview" lazy-load />
  18. <view v-else class="cover-placeholder">
  19. <text class="cover-icon">📷</text>
  20. <text class="cover-hint">添加封面</text>
  21. </view>
  22. <view v-if="coverImage" class="cover-change" @click.stop="chooseCover">
  23. <text class="change-text">更换</text>
  24. </view>
  25. </view>
  26. <!-- 标题 -->
  27. <view class="form-group">
  28. <input
  29. class="form-input title-input"
  30. v-model="title"
  31. placeholder="给这篇记录取个标题"
  32. maxlength="50"
  33. />
  34. <text class="char-count">{{ title.length }}/50</text>
  35. </view>
  36. <!-- 维度选择 -->
  37. <view class="form-group">
  38. <text class="form-label">关联维度(选填)</text>
  39. <view class="dimension-chips">
  40. <view
  41. v-for="dim in dimensionList"
  42. :key="dim.code"
  43. :class="['dim-chip', selectedDimensions.indexOf(dim.code) >= 0 ? 'active' : '']"
  44. :style="selectedDimensions.indexOf(dim.code) >= 0 ? 'background:' + dim.color + ';color:' + whiteColor + ';border-color:' + dim.color : ''"
  45. @click="toggleDimension(dim.code)"
  46. >
  47. {{ dim.name }}
  48. </view>
  49. </view>
  50. </view>
  51. <!-- 正文 -->
  52. <view class="form-group">
  53. <textarea
  54. class="form-textarea"
  55. v-model="content"
  56. placeholder="记录成员的成长点滴..."
  57. maxlength="2000"
  58. />
  59. <text class="char-count">{{ content.length }}/2000</text>
  60. </view>
  61. <!-- 可见范围 -->
  62. <view class="form-group">
  63. <text class="form-label">可见范围</text>
  64. <view class="visibility-options">
  65. <view
  66. :class="['vis-option', visibility === 'private' ? 'active' : '']"
  67. @click="visibility = 'private'"
  68. >
  69. <text class="vis-icon">🏠</text>
  70. <text class="vis-text">仅家庭</text>
  71. <text class="vis-desc">仅家庭成员可见</text>
  72. </view>
  73. <view
  74. :class="['vis-option', visibility === 'public' ? 'active' : '']"
  75. @click="visibility = 'public'"
  76. >
  77. <text class="vis-icon">🌍</text>
  78. <text class="vis-text">公开发布</text>
  79. <text class="vis-desc">所有人可见,需审核</text>
  80. </view>
  81. </view>
  82. </view>
  83. <!-- 发布按钮 -->
  84. <button class="publish-btn" :disabled="!canPublish" @click="onPublish">
  85. {{ publishing ? '发布中...' : '发布' }}
  86. </button>
  87. <view style="height: 60rpx;"></view>
  88. </scroll-view>
  89. </view>
  90. </template>
  91. <script>
  92. import { BrandColors } from '../../utils/brand-colors.js'
  93. import { publishArticle } from '@/utils/api.js'
  94. import config from '@/config.js'
  95. import { pickImage } from '@/utils/image-picker.js'
  96. export default {
  97. data() {
  98. return {
  99. whiteColor: BrandColors.white,
  100. title: '',
  101. coverImage: '',
  102. content: '',
  103. visibility: 'private',
  104. selectedDimensions: [],
  105. publishing: false,
  106. dimensionList: [
  107. { code: 'body', name: '身', color: BrandColors.body },
  108. { code: 'mind', name: '心', color: BrandColors.mind },
  109. { code: 'wisdom', name: '智', color: BrandColors.wisdom },
  110. { code: 'action', name: '行', color: BrandColors.success },
  111. { code: 'wealth', name: '富', color: BrandColors.wealth }
  112. ]
  113. }
  114. },
  115. computed: {
  116. canPublish: function() {
  117. return this.title.trim().length > 0 && this.content.trim().length > 0 && !this.publishing
  118. }
  119. },
  120. methods: {
  121. chooseCover() {
  122. var self = this
  123. pickImage({
  124. count: 1,
  125. sizeType: ['compressed'],
  126. sourceType: ['album', 'camera'],
  127. success: function(res) {
  128. var tempPath = res.tempFilePaths[0]
  129. // 上传图片
  130. var token = uni.getStorageSync('token')
  131. uni.uploadFile({
  132. url: (config.default && config.default.API_BASE_URL) || config.API_BASE_URL || '' + '/api/media/upload',
  133. filePath: tempPath,
  134. name: 'file',
  135. header: {
  136. 'Authorization': token ? 'Bearer ' + token : ''
  137. },
  138. success: function(uploadRes) {
  139. try {
  140. var data = JSON.parse(uploadRes.data)
  141. if (data && data.code === 200) {
  142. self.coverImage = data.data
  143. } else {
  144. uni.showToast({ title: '上传失败', icon: 'none' })
  145. }
  146. } catch (e) {
  147. uni.showToast({ title: '上传失败', icon: 'none' })
  148. }
  149. },
  150. fail: function() {
  151. uni.showToast({ title: '上传失败', icon: 'none' })
  152. }
  153. })
  154. }
  155. })
  156. },
  157. toggleDimension(code) {
  158. var idx = this.selectedDimensions.indexOf(code)
  159. if (idx >= 0) {
  160. this.selectedDimensions.splice(idx, 1)
  161. } else {
  162. this.selectedDimensions.push(code)
  163. }
  164. },
  165. onBack() {
  166. if (this.title || this.content) {
  167. var self = this
  168. uni.showModal({
  169. title: '提示',
  170. content: '是否保存草稿?',
  171. success: function(res) {
  172. if (res.confirm) {
  173. uni.setStorageSync('article_draft', {
  174. title: self.title,
  175. coverImage: self.coverImage,
  176. content: self.content,
  177. visibility: self.visibility,
  178. selectedDimensions: self.selectedDimensions
  179. })
  180. }
  181. uni.navigateBack()
  182. }
  183. })
  184. } else {
  185. uni.navigateBack()
  186. }
  187. },
  188. goMyPosts() {
  189. uni.navigateTo({ url: '/pages/article-center/my-posts' })
  190. },
  191. async onPublish() {
  192. if (!this.canPublish) return
  193. this.publishing = true
  194. try {
  195. var res = await publishArticle({
  196. title: this.title.trim(),
  197. content: this.content.trim(),
  198. coverImage: this.coverImage,
  199. visibility: this.visibility,
  200. relatedDimensions: this.selectedDimensions.join(',')
  201. })
  202. if (res.code === 200) {
  203. var msg = this.visibility === 'public'
  204. ? '发布成功,等待审核'
  205. : '发布成功,已对家庭可见'
  206. uni.showToast({ title: msg, icon: 'success', duration: 2000 })
  207. uni.removeStorageSync('article_draft')
  208. var self = this
  209. setTimeout(function() {
  210. uni.navigateBack()
  211. }, 1500)
  212. } else {
  213. uni.showToast({ title: res.message || '发布失败', icon: 'none' })
  214. }
  215. } catch (e) {
  216. uni.showToast({ title: '发布失败', icon: 'none' })
  217. } finally {
  218. this.publishing = false
  219. }
  220. }
  221. },
  222. onLoad() {
  223. // 加载草稿
  224. try {
  225. var draft = uni.getStorageSync('article_draft')
  226. if (draft) {
  227. this.title = draft.title || ''
  228. this.coverImage = draft.coverImage || ''
  229. this.content = draft.content || ''
  230. this.visibility = draft.visibility || 'private'
  231. this.selectedDimensions = draft.selectedDimensions || []
  232. }
  233. } catch (e) {}
  234. }
  235. }
  236. </script>
  237. <style scoped lang="scss">
  238. .edit-container { min-height: 100vh; background: $app-bg; }
  239. .edit-header {
  240. display: flex;
  241. align-items: center;
  242. justify-content: space-between;
  243. padding: 80rpx 30rpx 20rpx;
  244. background: $app-surface;
  245. position: sticky;
  246. top: 0;
  247. z-index: 10;
  248. }
  249. .header-left { display: flex; align-items: center; }
  250. .back-icon { font-size: 36rpx; margin-right: 8rpx; }
  251. .header-title { font-size: 32rpx; font-weight: bold; color: $facade-text-strong; }
  252. .header-right { }
  253. .history-text { font-size: 26rpx; color: $brand-primary; }
  254. .edit-form { padding: 20rpx 30rpx; }
  255. /* 封面 */
  256. .form-cover {
  257. width: 100%;
  258. height: 300rpx;
  259. background: $facade-hairline-2;
  260. border-radius: 16rpx;
  261. overflow: hidden;
  262. margin-bottom: 20rpx;
  263. position: relative;
  264. }
  265. .cover-preview { width: 100%; height: 100%; }
  266. .cover-placeholder {
  267. display: flex;
  268. flex-direction: column;
  269. align-items: center;
  270. justify-content: center;
  271. height: 100%;
  272. }
  273. .cover-icon { font-size: 60rpx; margin-bottom: 10rpx; }
  274. .cover-hint { font-size: 24rpx; color: $facade-text-muted; }
  275. .cover-change {
  276. position: absolute;
  277. right: 16rpx;
  278. bottom: 16rpx;
  279. background: rgba(0,0,0,0.5);
  280. color: $app-surface;
  281. padding: 6rpx 20rpx;
  282. border-radius: 20rpx;
  283. font-size: 22rpx;
  284. }
  285. /* 表单组 */
  286. .form-group {
  287. background: $app-surface;
  288. border-radius: 16rpx;
  289. padding: 24rpx;
  290. margin-bottom: 20rpx;
  291. }
  292. .form-input {
  293. width: 100%;
  294. font-size: 30rpx;
  295. color: $facade-text-strong;
  296. border: none;
  297. outline: none;
  298. }
  299. .title-input { font-weight: bold; }
  300. .char-count { display: block; text-align: right; font-size: 20rpx; color: $facade-text-ghost-light; margin-top: 8rpx; }
  301. .form-label { font-size: 24rpx; color: $facade-text-body; display: block; margin-bottom: 16rpx; }
  302. /* 维度 */
  303. .dimension-chips { display: flex; flex-wrap: wrap; gap: 16rpx; }
  304. .dim-chip {
  305. padding: 10rpx 28rpx;
  306. border-radius: 30rpx;
  307. font-size: 24rpx;
  308. border: 2rpx solid $facade-border-soft;
  309. color: $facade-text-body;
  310. }
  311. .dim-chip.active { color: $app-surface; border-color: transparent; }
  312. /* 文本框 */
  313. .form-textarea {
  314. width: 100%;
  315. height: 300rpx;
  316. font-size: 28rpx;
  317. color: $facade-text-strong;
  318. line-height: 1.6;
  319. border: none;
  320. outline: none;
  321. resize: none;
  322. }
  323. /* 可见范围 */
  324. .visibility-options { display: flex; gap: 20rpx; }
  325. .vis-option {
  326. flex: 1;
  327. padding: 24rpx;
  328. border-radius: 12rpx;
  329. border: 2rpx solid $facade-hairline-2;
  330. text-align: center;
  331. }
  332. .vis-option.active { border-color: $brand-primary; background: $uni-primary-pale; }
  333. .vis-icon { font-size: 40rpx; display: block; margin-bottom: 8rpx; }
  334. .vis-text { font-size: 26rpx; font-weight: bold; color: $facade-text-strong; display: block; }
  335. .vis-desc { font-size: 20rpx; color: $facade-text-muted; display: block; margin-top: 4rpx; }
  336. /* 发布按钮 */
  337. .publish-btn {
  338. width: 100%;
  339. height: 88rpx;
  340. line-height: 88rpx;
  341. background: linear-gradient(135deg, $brand-primary, $brand-primary-dark);
  342. color: $app-surface;
  343. font-size: 32rpx;
  344. font-weight: bold;
  345. border-radius: 44rpx;
  346. text-align: center;
  347. border: none;
  348. margin-top: 20rpx;
  349. }
  350. .publish-btn[disabled] { opacity: 0.4; }
  351. .publish-btn::after { border: none; }
  352. </style>