article-edit.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  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:#fff;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. title: '',
  100. coverImage: '',
  101. content: '',
  102. visibility: 'private',
  103. selectedDimensions: [],
  104. publishing: false,
  105. dimensionList: [
  106. { code: 'body', name: '身', color: BrandColors.body },
  107. { code: 'mind', name: '心', color: BrandColors.mind },
  108. { code: 'wisdom', name: '智', color: BrandColors.wisdom },
  109. { code: 'action', name: '行', color: BrandColors.success },
  110. { code: 'wealth', name: '富', color: BrandColors.wealth }
  111. ]
  112. }
  113. },
  114. computed: {
  115. canPublish: function() {
  116. return this.title.trim().length > 0 && this.content.trim().length > 0 && !this.publishing
  117. }
  118. },
  119. methods: {
  120. chooseCover() {
  121. var self = this
  122. pickImage({
  123. count: 1,
  124. sizeType: ['compressed'],
  125. sourceType: ['album', 'camera'],
  126. success: function(res) {
  127. var tempPath = res.tempFilePaths[0]
  128. // 上传图片
  129. var token = uni.getStorageSync('token')
  130. uni.uploadFile({
  131. url: (config.default && config.default.API_BASE_URL) || config.API_BASE_URL || '' + '/api/media/upload',
  132. filePath: tempPath,
  133. name: 'file',
  134. header: {
  135. 'Authorization': token ? 'Bearer ' + token : ''
  136. },
  137. success: function(uploadRes) {
  138. try {
  139. var data = JSON.parse(uploadRes.data)
  140. if (data && data.code === 200) {
  141. self.coverImage = data.data
  142. } else {
  143. uni.showToast({ title: '上传失败', icon: 'none' })
  144. }
  145. } catch (e) {
  146. uni.showToast({ title: '上传失败', icon: 'none' })
  147. }
  148. },
  149. fail: function() {
  150. uni.showToast({ title: '上传失败', icon: 'none' })
  151. }
  152. })
  153. }
  154. })
  155. },
  156. toggleDimension(code) {
  157. var idx = this.selectedDimensions.indexOf(code)
  158. if (idx >= 0) {
  159. this.selectedDimensions.splice(idx, 1)
  160. } else {
  161. this.selectedDimensions.push(code)
  162. }
  163. },
  164. onBack() {
  165. if (this.title || this.content) {
  166. var self = this
  167. uni.showModal({
  168. title: '提示',
  169. content: '是否保存草稿?',
  170. success: function(res) {
  171. if (res.confirm) {
  172. uni.setStorageSync('article_draft', {
  173. title: self.title,
  174. coverImage: self.coverImage,
  175. content: self.content,
  176. visibility: self.visibility,
  177. selectedDimensions: self.selectedDimensions
  178. })
  179. }
  180. uni.navigateBack()
  181. }
  182. })
  183. } else {
  184. uni.navigateBack()
  185. }
  186. },
  187. goMyPosts() {
  188. uni.navigateTo({ url: '/pages/article-center/my-posts' })
  189. },
  190. async onPublish() {
  191. if (!this.canPublish) return
  192. this.publishing = true
  193. try {
  194. var res = await publishArticle({
  195. title: this.title.trim(),
  196. content: this.content.trim(),
  197. coverImage: this.coverImage,
  198. visibility: this.visibility,
  199. relatedDimensions: this.selectedDimensions.join(',')
  200. })
  201. if (res.code === 200) {
  202. var msg = this.visibility === 'public'
  203. ? '发布成功,等待审核'
  204. : '发布成功,已对家庭可见'
  205. uni.showToast({ title: msg, icon: 'success', duration: 2000 })
  206. uni.removeStorageSync('article_draft')
  207. var self = this
  208. setTimeout(function() {
  209. uni.navigateBack()
  210. }, 1500)
  211. } else {
  212. uni.showToast({ title: res.message || '发布失败', icon: 'none' })
  213. }
  214. } catch (e) {
  215. uni.showToast({ title: '发布失败', icon: 'none' })
  216. } finally {
  217. this.publishing = false
  218. }
  219. }
  220. },
  221. onLoad() {
  222. // 加载草稿
  223. try {
  224. var draft = uni.getStorageSync('article_draft')
  225. if (draft) {
  226. this.title = draft.title || ''
  227. this.coverImage = draft.coverImage || ''
  228. this.content = draft.content || ''
  229. this.visibility = draft.visibility || 'private'
  230. this.selectedDimensions = draft.selectedDimensions || []
  231. }
  232. } catch (e) {}
  233. }
  234. }
  235. </script>
  236. <style scoped lang="scss">
  237. .edit-container { min-height: 100vh; background: $app-bg; }
  238. .edit-header {
  239. display: flex;
  240. align-items: center;
  241. justify-content: space-between;
  242. padding: 80rpx 30rpx 20rpx;
  243. background: $app-surface;
  244. position: sticky;
  245. top: 0;
  246. z-index: 10;
  247. }
  248. .header-left { display: flex; align-items: center; }
  249. .back-icon { font-size: 36rpx; margin-right: 8rpx; }
  250. .header-title { font-size: 32rpx; font-weight: bold; color: $facade-text-strong; }
  251. .header-right { }
  252. .history-text { font-size: 26rpx; color: $brand-primary; }
  253. .edit-form { padding: 20rpx 30rpx; }
  254. /* 封面 */
  255. .form-cover {
  256. width: 100%;
  257. height: 300rpx;
  258. background: $facade-hairline-2;
  259. border-radius: 16rpx;
  260. overflow: hidden;
  261. margin-bottom: 20rpx;
  262. position: relative;
  263. }
  264. .cover-preview { width: 100%; height: 100%; }
  265. .cover-placeholder {
  266. display: flex;
  267. flex-direction: column;
  268. align-items: center;
  269. justify-content: center;
  270. height: 100%;
  271. }
  272. .cover-icon { font-size: 60rpx; margin-bottom: 10rpx; }
  273. .cover-hint { font-size: 24rpx; color: $facade-text-muted; }
  274. .cover-change {
  275. position: absolute;
  276. right: 16rpx;
  277. bottom: 16rpx;
  278. background: rgba(0,0,0,0.5);
  279. color: $app-surface;
  280. padding: 6rpx 20rpx;
  281. border-radius: 20rpx;
  282. font-size: 22rpx;
  283. }
  284. /* 表单组 */
  285. .form-group {
  286. background: $app-surface;
  287. border-radius: 16rpx;
  288. padding: 24rpx;
  289. margin-bottom: 20rpx;
  290. }
  291. .form-input {
  292. width: 100%;
  293. font-size: 30rpx;
  294. color: $facade-text-strong;
  295. border: none;
  296. outline: none;
  297. }
  298. .title-input { font-weight: bold; }
  299. .char-count { display: block; text-align: right; font-size: 20rpx; color: $facade-text-ghost-light; margin-top: 8rpx; }
  300. .form-label { font-size: 24rpx; color: $facade-text-body; display: block; margin-bottom: 16rpx; }
  301. /* 维度 */
  302. .dimension-chips { display: flex; flex-wrap: wrap; gap: 16rpx; }
  303. .dim-chip {
  304. padding: 10rpx 28rpx;
  305. border-radius: 30rpx;
  306. font-size: 24rpx;
  307. border: 2rpx solid $facade-border-soft;
  308. color: $facade-text-body;
  309. }
  310. .dim-chip.active { color: $app-surface; border-color: transparent; }
  311. /* 文本框 */
  312. .form-textarea {
  313. width: 100%;
  314. height: 300rpx;
  315. font-size: 28rpx;
  316. color: $facade-text-strong;
  317. line-height: 1.6;
  318. border: none;
  319. outline: none;
  320. resize: none;
  321. }
  322. /* 可见范围 */
  323. .visibility-options { display: flex; gap: 20rpx; }
  324. .vis-option {
  325. flex: 1;
  326. padding: 24rpx;
  327. border-radius: 12rpx;
  328. border: 2rpx solid $facade-hairline-2;
  329. text-align: center;
  330. }
  331. .vis-option.active { border-color: $brand-primary; background: $uni-primary-pale; }
  332. .vis-icon { font-size: 40rpx; display: block; margin-bottom: 8rpx; }
  333. .vis-text { font-size: 26rpx; font-weight: bold; color: $facade-text-strong; display: block; }
  334. .vis-desc { font-size: 20rpx; color: $facade-text-muted; display: block; margin-top: 4rpx; }
  335. /* 发布按钮 */
  336. .publish-btn {
  337. width: 100%;
  338. height: 88rpx;
  339. line-height: 88rpx;
  340. background: linear-gradient(135deg, $brand-primary, $brand-primary-dark);
  341. color: $app-surface;
  342. font-size: 32rpx;
  343. font-weight: bold;
  344. border-radius: 44rpx;
  345. text-align: center;
  346. border: none;
  347. margin-top: 20rpx;
  348. }
  349. .publish-btn[disabled] { opacity: 0.4; }
  350. .publish-btn::after { border: none; }
  351. </style>