projectList.vue 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. <template>
  2. <view class="app-container pb-24">
  3. <TopBar title="项目库" show-back />
  4. <!-- 顶部搜索栏 -->
  5. <view class="bg-white px-4 py-3 sticky top-0 z-40">
  6. <view class="flex items-center bg-gray-100 rounded-2xl px-4 py-2">
  7. <text class="uni-icons uniui-search text-gray-400 mr-2 text-base"></text>
  8. <input
  9. class="flex-1 bg-transparent text-sm text-gray-700"
  10. placeholder="搜索项目名称"
  11. v-model="searchKeyword"
  12. />
  13. </view>
  14. </view>
  15. <!-- 顶部筛选标签:药丸式分段控件 -->
  16. <view class="filter-tabs">
  17. <view
  18. v-for="(label, index) in filterLabels"
  19. :key="index"
  20. class="filter-tab"
  21. :class="{ 'filter-tab-active': currentFilterIndex === index }"
  22. @click="onFilterChange(index)"
  23. >
  24. {{ label }}
  25. </view>
  26. </view>
  27. <!-- 项目列表 - 使用 uni-list + uni-card + uni-swipe-action -->
  28. <view class="mt-2">
  29. <view v-if="projectStore.loading" class="py-16 text-center">
  30. <text class="text-gray-400">加载中...</text>
  31. </view>
  32. <view v-else-if="filteredProjects.length === 0">
  33. <EmptyState message="暂无项目" />
  34. </view>
  35. <uni-list v-else border>
  36. <uni-swipe-action
  37. v-for="project in filteredProjects"
  38. :key="project.id"
  39. :actions="swipeActions"
  40. @click="onSwipeAction(project.id, $event)"
  41. >
  42. <uni-list-item
  43. :title="project.name"
  44. :note="project.address + ' | ' + project.customerName"
  45. :right-text="project.type"
  46. show-arrow
  47. @click="goToDetail(project.id)"
  48. >
  49. <template #header>
  50. <view class="w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center mr-3">
  51. <text class="uni-icons uniui-folder-add-filled text-blue-500 text-lg"></text>
  52. </view>
  53. </template>
  54. </uni-list-item>
  55. </uni-swipe-action>
  56. </uni-list>
  57. </view>
  58. <!-- 新增悬浮按钮:避开胶囊按钮 -->
  59. <view class="fab" @click="goToAdd">
  60. <text class="uni-icons uniui-plusempty fab-icon"></text>
  61. <text class="fab-text">新增</text>
  62. </view>
  63. <!-- 加载更多 -->
  64. <view v-if="filteredProjects.length > 0" class="px-4 mt-4 mb-4">
  65. <view v-if="projectStore.hasMore" class="text-center py-4">
  66. <text
  67. class="text-sm text-blue-500 font-medium"
  68. @click="loadMore"
  69. >
  70. {{ projectStore.loadingMore ? '加载中...' : '加载更多' }}
  71. </text>
  72. </view>
  73. <view v-else class="text-center py-4">
  74. <text class="text-xs text-gray-400">已经到底了</text>
  75. </view>
  76. </view>
  77. </view>
  78. </template>
  79. <script setup lang="ts">
  80. import { ref, computed, onMounted } from 'vue'
  81. import { onPullDownRefresh } from '@dcloudio/uni-app'
  82. import { useProjectStore } from '../../stores/project'
  83. import EmptyState from '../../components/common/EmptyState.vue'
  84. import TopBar from '../../components/common/TopBar.vue'
  85. import UniList from '../../components/uni-list/uni-list.vue'
  86. import UniListItem from '../../components/uni-list/uni-list-item.vue'
  87. import UniSwipeAction from '../../components/uni-swipe-action/uni-swipe-action.vue'
  88. const projectStore = useProjectStore()
  89. const searchKeyword = ref('')
  90. const currentFilterIndex = ref(0)
  91. const filterTypes = [
  92. { value: 'all', label: '全部' },
  93. { value: '包年', label: '包年' },
  94. { value: '单次', label: '单次' },
  95. { value: '季度', label: '季度' },
  96. { value: '月付', label: '月付' },
  97. ]
  98. const filterLabels = filterTypes.map(t => t.label)
  99. const filteredProjects = computed(() => {
  100. let list = projectStore.projects
  101. const filterValue = filterTypes[currentFilterIndex.value].value
  102. if (filterValue !== 'all') {
  103. list = list.filter(p => p.type === filterValue)
  104. }
  105. if (searchKeyword.value) {
  106. list = list.filter(p => p.name.includes(searchKeyword.value))
  107. }
  108. return list
  109. })
  110. const swipeActions: { text: string; color: string; key: string }[] = [
  111. { text: '编辑', color: '#4f8ef7', key: 'edit' },
  112. { text: '删除', color: '#ff3b30', key: 'delete' },
  113. ]
  114. function onFilterChange(index: number) {
  115. currentFilterIndex.value = index
  116. }
  117. function onSwipeAction(projectId: string, action: { key?: string; text?: string }) {
  118. if (action.key === 'delete') {
  119. uni.showModal({
  120. title: '确认删除',
  121. content: '删除后无法恢复,是否继续?',
  122. success: (res) => {
  123. if (res.confirm) {
  124. projectStore.deleteProject(projectId)
  125. uni.showToast({ title: '已删除', icon: 'success' })
  126. }
  127. }
  128. })
  129. } else if (action.key === 'edit') {
  130. projectStore.setCurrentProject(projectId)
  131. uni.navigateTo({ url: '/subPackages/pages-common/addProject?mode=edit' })
  132. }
  133. }
  134. function goToDetail(projectId: string) {
  135. projectStore.setCurrentProject(projectId)
  136. uni.navigateTo({ url: '/subPackages/pages-common/projectDetail' })
  137. }
  138. function goToAdd() {
  139. uni.navigateTo({ url: '/subPackages/pages-common/addProject' })
  140. }
  141. async function refreshData() {
  142. await projectStore.fetchProjects(true)
  143. uni.stopPullDownRefresh()
  144. }
  145. function loadMore() {
  146. if (projectStore.loadingMore) return
  147. projectStore.loadMore()
  148. }
  149. onMounted(() => {
  150. projectStore.fetchProjects(true)
  151. })
  152. // 下拉刷新
  153. onPullDownRefresh(() => {
  154. refreshData()
  155. })
  156. </script>
  157. <style scoped>
  158. /* ==================== 筛选标签:药丸式分段控件 ==================== */
  159. .filter-tabs {
  160. display: flex;
  161. gap: 8px;
  162. padding: 10px 16px;
  163. background-color: #ffffff;
  164. overflow-x: auto;
  165. white-space: nowrap;
  166. }
  167. .filter-tab {
  168. flex-shrink: 0;
  169. padding: 6px 16px;
  170. border-radius: 999px;
  171. font-size: 13px;
  172. color: #709484; /* 晨雾灰绿 */
  173. background-color: #f6fbf9; /* 云雾白 */
  174. border: 1px solid rgba(164, 216, 152, 0.4);
  175. transition: all 0.18s ease;
  176. }
  177. .filter-tab-active {
  178. color: #ffffff;
  179. background: linear-gradient(135deg, #368f6f 0%, #5ab8d0 100%); /* 青山绿 → 流水青 */
  180. border-color: transparent;
  181. box-shadow: 0 6px 14px -6px rgba(54, 143, 111, 0.6);
  182. }
  183. /* ==================== 新增悬浮按钮 ==================== */
  184. .fab {
  185. position: fixed;
  186. right: 20px;
  187. bottom: calc(28px + env(safe-area-inset-bottom));
  188. z-index: 99;
  189. display: flex;
  190. align-items: center;
  191. gap: 4px;
  192. height: 50px;
  193. padding: 0 22px;
  194. border-radius: 999px;
  195. background: linear-gradient(135deg, #368f6f 0%, #5ab8d0 100%);
  196. box-shadow: 0 10px 22px -8px rgba(54, 143, 111, 0.65);
  197. }
  198. .fab:active {
  199. transform: scale(0.95);
  200. opacity: 0.9;
  201. }
  202. .fab-icon {
  203. color: #ffffff;
  204. font-size: 18px;
  205. }
  206. .fab-text {
  207. color: #ffffff;
  208. font-size: 15px;
  209. font-weight: 600;
  210. letter-spacing: 1px;
  211. }
  212. </style>