article-edit.vue 11 KB

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