news.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <template>
  2. <CustomNav title="健康资讯管理" leftType="back" />
  3. <view class="page-container">
  4. <view class="action-bar">
  5. <view class="search-wrap">
  6. <input class="search-input" placeholder="搜索标题或概要" v-model="searchKey" @confirm="onSearch" />
  7. <view class="search-btn" @click="onSearch">
  8. <uni-icons type="search" size="22" color="#fff" />
  9. </view>
  10. </view>
  11. </view>
  12. <view v-if="isLoading" class="skeleton-list">
  13. <view v-for="i in 3" :key="i" class="skeleton-card">
  14. <view class="skeleton-cover" />
  15. <view class="skeleton-meta">
  16. <view class="skeleton-title" />
  17. <view class="skeleton-summary" />
  18. </view>
  19. </view>
  20. </view>
  21. <view class="news-card" v-for="item in filteredList" :key="item.id">
  22. <view class="news-content">
  23. <view v-if="item.coverImage" class="news-image-container">
  24. <image class="news-image" :src="item.coverImage" mode="aspectFill" />
  25. </view>
  26. <view v-else class="news-placeholder">
  27. <image class="placeholder-icon" src="/static/icons/remixicon/image-line.svg" />
  28. </view>
  29. <view class="meta">
  30. <text class="title">{{ item.title }}</text>
  31. <text class="summary">{{ item.summary }}</text>
  32. <text class="time">发布时间: {{ item.publishTime }}</text>
  33. </view>
  34. </view>
  35. <view class="action-buttons">
  36. <button class="action-btn primary" @click="viewNews(item)">查看</button>
  37. <button class="action-btn secondary" @click="editNews(item)">编辑</button>
  38. <button class="action-btn danger" @click="removeNews(item)">删除</button>
  39. </view>
  40. </view>
  41. <view class="empty-state" v-if="filteredList.length === 0">
  42. <view class="news-placeholder">
  43. <image class="placeholder-icon" src="/static/icons/remixicon/image-line.svg" />
  44. </view>
  45. <text class="empty-text">暂无资讯</text>
  46. <button class="create-cta" @click="onCreateNews">现在创建</button>
  47. </view>
  48. <view class="loadmore" v-if="hasMore">
  49. <button class="load-btn" @click="loadMore">加载更多</button>
  50. </view>
  51. </view>
  52. <!-- 悬浮新建按钮(样式参考 physical.vue) -->
  53. <view class="fab" @click="onCreateNews" role="button" aria-label="新建资讯">
  54. <view class="fab-inner">+</view>
  55. </view>
  56. </template>
  57. <script setup lang="ts">
  58. import { ref, computed } from 'vue'
  59. import { onShow, onPullDownRefresh } from '@dcloudio/uni-app'
  60. import CustomNav from '@/components/custom-nav.vue'
  61. interface NewsItem {
  62. id: string
  63. title: string
  64. summary?: string
  65. content?: string
  66. coverImage?: string
  67. publishTime?: string
  68. }
  69. const newsList = ref<NewsItem[]>([
  70. {
  71. id: '1',
  72. title: '如何管理血压:日常监测要点',
  73. summary: '本文介绍了家庭血压监测的常见问题及注意事项。',
  74. publishTime: '2025-01-01 10:00:00'
  75. },
  76. {
  77. id: '2',
  78. title: '糖尿病饮食小贴士',
  79. summary: '5条简单饮食建议,助你在日常生活中控制血糖。',
  80. publishTime: '2025-01-05 14:30:00'
  81. }
  82. ])
  83. const searchKey = ref('')
  84. const defaultCover = '/static/icons/remixicon/image-line.svg'
  85. const pageSize = ref(10)
  86. const currentPage = ref(1)
  87. const hasMore = ref(false)
  88. const isLoading = ref(false)
  89. const onCreateNews = () => {
  90. // 跳转到编辑/新建页(新建时不带 id)
  91. uni.navigateTo({ url: `/pages/doctor/manage/news-edit` })
  92. }
  93. const fetchNews = async () => {
  94. // 模拟网络请求,真实场景使用 API 请求
  95. isLoading.value = true
  96. await new Promise(resolve => setTimeout(resolve, 350))
  97. isLoading.value = false
  98. }
  99. const viewNews = (item: NewsItem) => {
  100. // 跳转到详情页(已迁移到 `pages/public/news-detail`)
  101. uni.navigateTo({ url: `/pages/public/news-detail?id=${item.id}` })
  102. }
  103. const editNews = (item: NewsItem) => {
  104. // 带 id 跳转到编辑页
  105. uni.navigateTo({ url: `/pages/doctor/manage/news-edit?id=${item.id}` })
  106. }
  107. const removeNews = (item: NewsItem) => {
  108. uni.showModal({
  109. title: '确认删除',
  110. content: `确定删除资讯《${item.title}》吗?`,
  111. success: (res) => {
  112. if (res.confirm) {
  113. const idx = newsList.value.findIndex(i => i.id === item.id)
  114. if (idx >= 0) {
  115. newsList.value.splice(idx, 1)
  116. uni.showToast({ title: '删除成功', icon: 'success' })
  117. }
  118. }
  119. }
  120. })
  121. }
  122. const onSearch = () => {
  123. // 简单的本地过滤,后续可替换为后端搜索API
  124. currentPage.value = 1
  125. }
  126. const filteredList = computed(() => {
  127. const key = searchKey.value.trim().toLowerCase()
  128. let list = newsList.value
  129. if (key) {
  130. list = list.filter(i => i.title.toLowerCase().includes(key) || (i.summary || '').toLowerCase().includes(key))
  131. }
  132. // 分页处理:简单模拟
  133. const start = 0
  134. const end = currentPage.value * pageSize.value
  135. hasMore.value = list.length > end
  136. return list.slice(start, end)
  137. })
  138. const loadMore = () => {
  139. currentPage.value += 1
  140. }
  141. onShow(() => {
  142. const token = uni.getStorageSync('token')
  143. if (!token) {
  144. uni.reLaunch({ url: '/pages/public/login/index' })
  145. }
  146. fetchNews()
  147. })
  148. onPullDownRefresh(() => {
  149. // 下拉刷新
  150. currentPage.value = 1
  151. fetchNews().then(() => uni.stopPullDownRefresh())
  152. })
  153. </script>
  154. <style scoped>
  155. .page-container {
  156. min-height: 100vh;
  157. background-color: #f5f5f5;
  158. padding-top: calc(var(--status-bar-height) + 44px);
  159. padding-bottom: 40rpx;
  160. }
  161. .action-bar {
  162. display: flex;
  163. justify-content: space-between;
  164. align-items: center;
  165. gap: 18rpx;
  166. padding: 28rpx 20rpx;
  167. }
  168. .create-btn {
  169. background-color: #2d8cf0;
  170. color: #fff;
  171. border-radius: 10rpx;
  172. padding: 18rpx 26rpx;
  173. font-size: 28rpx;
  174. min-width: 140rpx;
  175. }
  176. .search-wrap {
  177. display: flex;
  178. align-items: center;
  179. gap: 12rpx;
  180. flex: 1; /* 自动撑满剩余空间 */
  181. }
  182. /* 悬浮按钮 */
  183. .fab {
  184. position: fixed;
  185. right: 28rpx;
  186. bottom: 160rpx; /* 类似 physical.vue 的位置 */
  187. width: 110rpx;
  188. height: 110rpx;
  189. border-radius: 999px;
  190. background: linear-gradient(180deg, #4a90e2, #2d8cf0); /* 蓝色渐变 */
  191. display: flex;
  192. align-items: center;
  193. justify-content: center;
  194. box-shadow: 0 6rpx 18rpx rgba(0, 0, 0, 0.2);
  195. z-index: 1200;
  196. border: none;
  197. }
  198. .fab:active { transform: translateY(2rpx); }
  199. .fab:focus { outline: none; }
  200. .fab-inner { color: #fff; font-size: 56rpx; line-height: 56rpx }
  201. .search-input {
  202. flex: 1;
  203. height: 64rpx;
  204. border-radius: 12rpx;
  205. padding: 0 20rpx;
  206. border: 1rpx solid #e6e6e6;
  207. font-size: 28rpx;
  208. background: #fff;
  209. }
  210. .search-btn {
  211. width: 64rpx;
  212. height: 64rpx;
  213. border-radius: 12rpx;
  214. background-color: #4a90e2;
  215. display: flex;
  216. align-items: center;
  217. justify-content: center;
  218. }
  219. .news-card {
  220. background-color: #fff;
  221. margin: 18rpx 20rpx;
  222. border-radius: 12rpx;
  223. overflow: hidden;
  224. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  225. transition: transform 0.12s ease, box-shadow 0.12s ease;
  226. }
  227. .news-card:hover {
  228. transform: translateY(-4rpx);
  229. box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.12);
  230. }
  231. .news-content {
  232. display: flex;
  233. padding: 20rpx;
  234. gap: 20rpx;
  235. }
  236. .news-image-container {
  237. width: 180rpx;
  238. height: 130rpx;
  239. border-radius: 10rpx;
  240. margin-right: 20rpx;
  241. overflow: hidden;
  242. }
  243. .news-image {
  244. width: 100%;
  245. height: 100%;
  246. object-fit: cover;
  247. }
  248. .news-placeholder {
  249. width: 180rpx;
  250. height: 130rpx;
  251. background-color: #f0f0f0;
  252. border-radius: 10rpx;
  253. margin-right: 20rpx;
  254. display: flex;
  255. align-items: center;
  256. justify-content: center;
  257. }
  258. .placeholder-icon {
  259. width: 40rpx;
  260. height: 40rpx;
  261. opacity: 0.5;
  262. }
  263. /* empty-placeholder removed to keep strict parity with patient/index styles */
  264. .meta {
  265. flex: 1;
  266. display: flex;
  267. flex-direction: column;
  268. justify-content: space-between;
  269. }
  270. .title {
  271. font-size: 34rpx;
  272. line-height: 1.4;
  273. color: #222;
  274. font-weight: 600;
  275. }
  276. .summary {
  277. font-size: 28rpx;
  278. margin-top: 8rpx;
  279. color: #666;
  280. line-height: 1.6;
  281. display: -webkit-box;
  282. -webkit-box-orient: vertical;
  283. -webkit-line-clamp: 2; /* limit to 2 lines */
  284. line-clamp: 2;
  285. overflow: hidden;
  286. }
  287. .time {
  288. font-size: 24rpx;
  289. margin-top: 12rpx;
  290. }
  291. .action-buttons {
  292. display: flex;
  293. gap: 12rpx;
  294. padding: 12rpx 18rpx;
  295. border-top: 1rpx solid #f5f5f5;
  296. justify-content: flex-end;
  297. flex-wrap: wrap;
  298. }
  299. .action-btn {
  300. padding: 10rpx 22rpx;
  301. border-radius: 20rpx;
  302. min-width: 110rpx;
  303. height: 64rpx;
  304. line-height: 44rpx;
  305. background: #fff;
  306. color: #333;
  307. font-size: 28rpx;
  308. border: 1rpx solid #e6e6e6;
  309. box-shadow: none;
  310. display: inline-flex;
  311. align-items: center;
  312. justify-content: center;
  313. transition: transform 0.12s ease, box-shadow 0.12s ease;
  314. }
  315. .action-btn:active { transform: translateY(2rpx); }
  316. .action-btn:hover { box-shadow: 0 6rpx 12rpx rgba(0,0,0,0.06); }
  317. .primary {
  318. background: linear-gradient(180deg,#6fb7ff,#2d8cf0);
  319. color: #fff;
  320. border: none;
  321. box-shadow: 0 10rpx 28rpx rgba(45,140,240,0.12);
  322. }
  323. .danger {
  324. background: linear-gradient(180deg,#ff8a8a,#ff6b6b);
  325. color: #fff;
  326. border: none;
  327. box-shadow: 0 10rpx 28rpx rgba(255,107,107,0.12);
  328. }
  329. /* 次要按钮样式:比主按钮弱一点,但比纯白更协调 */
  330. .secondary {
  331. background: linear-gradient(180deg,#cfeeff,#9fd7ff); /* 更明显的浅蓝填充 */
  332. color: #063a7a; /* 深蓝文字,保持可读性 */
  333. border: none;
  334. box-shadow: 0 8rpx 20rpx rgba(45,140,240,0.08);
  335. }
  336. .secondary:hover {
  337. transform: translateY(-2rpx);
  338. box-shadow: 0 12rpx 28rpx rgba(45,140,240,0.12);
  339. }
  340. .empty-state {
  341. display: flex;
  342. flex-direction: column;
  343. align-items: center;
  344. justify-content: center;
  345. padding: 100rpx 40rpx;
  346. }
  347. .empty-icon {
  348. width: 160rpx;
  349. height: 160rpx;
  350. }
  351. .empty-text {
  352. font-size: 36rpx;
  353. }
  354. .skeleton-list {
  355. padding: 18rpx 18rpx;
  356. }
  357. .skeleton-card {
  358. background: #fff;
  359. display: flex;
  360. gap: 18rpx;
  361. padding: 16rpx;
  362. border-radius: 12rpx;
  363. margin-bottom: 16rpx;
  364. align-items: center;
  365. }
  366. .skeleton-cover {
  367. width: 160rpx;
  368. height: 96rpx;
  369. border-radius: 8rpx;
  370. background: linear-gradient(90deg, #eee, #f6f6f6);
  371. }
  372. .skeleton-meta {
  373. flex: 1;
  374. }
  375. .skeleton-title {
  376. width: 60%;
  377. height: 22rpx;
  378. background: linear-gradient(90deg, #eee, #f6f6f6);
  379. margin-bottom: 8rpx;
  380. border-radius: 6rpx;
  381. }
  382. .skeleton-summary {
  383. width: 80%;
  384. height: 16rpx;
  385. background: linear-gradient(90deg, #eee, #f6f6f6);
  386. border-radius: 6rpx;
  387. }
  388. .create-cta {
  389. margin-top: 18rpx;
  390. background-color: #2d8cf0;
  391. color: #fff;
  392. padding: 16rpx 28rpx;
  393. border-radius: 12rpx;
  394. }
  395. .loadmore {
  396. padding: 24rpx 40rpx;
  397. display: flex;
  398. justify-content: center;
  399. }
  400. .load-btn {
  401. background-color: #fff;
  402. border: 1rpx solid #e6e6e6;
  403. padding: 12rpx 30rpx;
  404. border-radius: 12rpx;
  405. }
  406. </style>