综合办公系统
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

DevicePicker.vue 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. <template>
  2. <uni-popup ref="devicePopup" type="bottom" @maskClick="close">
  3. <view class="modal-container">
  4. <!-- 搜索表单 -->
  5. <view class="search-box">
  6. <view class="search-type">
  7. <uni-data-select v-model="searchType" :localdata="searchTypeOptions" @change="handleSearchTypeChange">
  8. </uni-data-select>
  9. </view>
  10. <view class="search-item">
  11. <uni-easyinput v-show="searchType === 'code'" v-model="queryParams.code" placeholder="出厂编号"
  12. @blur="handleSearch" clearable>
  13. </uni-easyinput>
  14. <uni-data-select v-show="searchType === 'name'" v-model="queryParams.name" :localdata="nameList"
  15. @change="handleSearch" clearable>
  16. </uni-data-select>
  17. <uni-easyinput v-show="searchType === 'brand'" v-model="queryParams.brand" placeholder="设备品牌"
  18. @blur="handleSearch" clearable>
  19. </uni-easyinput>
  20. </view>
  21. </view>
  22. <!-- 设备列表 -->
  23. <scroll-view scroll-y class="list-container" @scrolltolower="loadMore" :scroll-top="scrollTop">
  24. <view v-for="(item, index) in list" :key="index" class="list-item" :class="{ selected: isSelected(item) }"
  25. @click="handleSelect(item)">
  26. <view class="item-content">
  27. <view class="item-header">
  28. <uni-tag :text="statusText(item.status)" :type="statusType(item.status)" size="small">
  29. </uni-tag>
  30. <text class="device-code">{{ item.code }}</text>
  31. </view>
  32. <view class="item-body">
  33. <text class="device-name">{{ item.name }}</text>
  34. <text class="device-info">{{ item.brand }} {{ item.series }}</text>
  35. <text class="device-place">📍 {{ item.place }}</text>
  36. </view>
  37. </view>
  38. <uni-icons v-if="isSelected(item)" type="checkmarkempty" color="#007AFF" size="18" />
  39. </view>
  40. <!-- 加载状态 -->
  41. <view class="loading-status">
  42. <uni-load-more :status="loading ? 'loading' : (hasMore ? 'more' : 'noMore')" :contentText="{
  43. contentdown: '上拉加载更多',
  44. contentrefresh: '正在加载',
  45. contentnomore: '没有更多设备'
  46. }">
  47. </uni-load-more>
  48. </view>
  49. </scroll-view>
  50. <!-- 底部操作栏 -->
  51. <view class="footer-actions">
  52. <button class="action-btn cancel" @click="close">取消</button>
  53. <button class="action-btn confirm" @click="confirm" :disabled="!selectedList.length">确定选择</button>
  54. </view>
  55. </view>
  56. </uni-popup>
  57. </template>
  58. <script>
  59. import { listDevice, listDeviceName } from "@/api/oa/device/device";
  60. import { debounce } from 'lodash-es';
  61. export default {
  62. props: {
  63. visible: Boolean,
  64. selected: {
  65. type: [Array, Object],
  66. default: () => ([])
  67. },
  68. multiple: {
  69. type: Boolean,
  70. default: true
  71. }
  72. },
  73. data() {
  74. return {
  75. searchType: 'code',
  76. searchTypeOptions: [
  77. { value: 'code', text: '出厂编号' },
  78. { value: 'name', text: '设备名称' },
  79. { value: 'brand', text: '设备品牌' }
  80. ],
  81. queryParams: {
  82. pageNum: 1,
  83. pageSize: 10,
  84. status: '1',
  85. type: '仪器设备',
  86. code: '',
  87. name: '',
  88. brand: ''
  89. },
  90. statusOptions: [
  91. { value: '0', text: '被领用' },
  92. { value: '1', text: '可领用' },
  93. { value: '2', text: '维修中' },
  94. { value: '3', text: '已停用' },
  95. { value: '4', text: '已报废' }
  96. ],
  97. list: [],
  98. selectedList: [],
  99. hasMore: true,
  100. loading: false,
  101. scrollTop: 0,
  102. nameList: []
  103. }
  104. },
  105. watch: {
  106. visible(val) {
  107. val ? this.$refs.devicePopup.open() : this.$refs.devicePopup.close();
  108. if (val) this.initData();
  109. },
  110. selected: {
  111. immediate: true,
  112. handler(val) {
  113. this.selectedList = Array.isArray(val) ? [...val] : [val];
  114. }
  115. },
  116. 'queryParams.status': {
  117. handler() {
  118. this.handleSearch();
  119. }
  120. }
  121. },
  122. created() {
  123. this.debouncedSearch = debounce(this.loadData, 300);
  124. this.getNameList();
  125. },
  126. methods: {
  127. // 初始化数据
  128. initData() {
  129. this.queryParams.pageNum = 1;
  130. this.hasMore = true;
  131. this.list = [];
  132. this.loadData();
  133. },
  134. // 加载设备数据
  135. async loadData() {
  136. if (this.loading) return;
  137. this.loading = true;
  138. try {
  139. const res = await listDevice(this.queryParams);
  140. // 如果是第一页,直接替换列表
  141. if (this.queryParams.pageNum === 1) {
  142. this.list = res.rows;
  143. } else {
  144. // 如果不是第一页,追加到列表
  145. this.list = [...this.list, ...res.rows];
  146. }
  147. // 保持已选择的设备状态
  148. const selectedDeviceIds = this.selectedList.map(item => item.deviceId);
  149. this.list.forEach(item => {
  150. if (selectedDeviceIds.includes(item.deviceId)) {
  151. const selectedItem = this.selectedList.find(s => s.deviceId === item.deviceId);
  152. if (selectedItem) {
  153. Object.assign(item, selectedItem);
  154. }
  155. }
  156. });
  157. this.hasMore = res.total > this.queryParams.pageNum * this.queryParams.pageSize;
  158. this.queryParams.pageNum++;
  159. } finally {
  160. this.loading = false;
  161. }
  162. },
  163. getNameList() {
  164. listDeviceName().then(res => {
  165. this.nameList = res.data.filter(function (item) {
  166. return item !== null;
  167. }).map(function (item) {
  168. return { value: item.name, text: item.name };
  169. });
  170. })
  171. },
  172. // 搜索类型改变处理
  173. handleSearchTypeChange() {
  174. // 清空其他搜索字段
  175. this.queryParams.code = '';
  176. this.queryParams.name = '';
  177. this.queryParams.brand = '';
  178. this.handleSearch();
  179. },
  180. // 搜索处理
  181. handleSearch() {
  182. // 重置分页参数
  183. this.queryParams.pageNum = 1;
  184. this.hasMore = true;
  185. this.list = [];
  186. this.loadData();
  187. },
  188. // 选择设备
  189. handleSelect(item) {
  190. if (this.multiple) {
  191. const index = this.selectedList.findIndex(d => d.deviceId === item.deviceId);
  192. index === -1
  193. ? this.selectedList.push(item)
  194. : this.selectedList.splice(index, 1);
  195. } else {
  196. this.selectedList = [item];
  197. }
  198. },
  199. // 确认选择
  200. confirm() {
  201. const result = this.multiple ? this.selectedList : this.selectedList[0];
  202. this.$emit('update:selected', result);
  203. this.$emit('confirm', result);
  204. this.close();
  205. },
  206. // 状态显示逻辑
  207. statusText(status) {
  208. const map = { '0': '被领用', '1': '可领用', '2': '维修中', '3': '已停用', '4': '已报废' };
  209. return map[status] || '未知状态';
  210. },
  211. statusType(status) {
  212. const typeMap = { '0': 'warning', '1': 'success', '2': 'info', '3': 'error', '4': 'default' };
  213. return typeMap[status] || 'default';
  214. },
  215. isSelected(item) {
  216. return this.selectedList.some(d => d.deviceId === item.deviceId);
  217. },
  218. // 加载更多
  219. loadMore() {
  220. if (!this.hasMore || this.loading) return;
  221. this.loadData();
  222. },
  223. close() {
  224. this.$emit('update:visible', false);
  225. }
  226. }
  227. }
  228. </script>
  229. <style lang="scss" scoped>
  230. .modal-container {
  231. background: #fff;
  232. border-radius: 24rpx 24rpx 0 0;
  233. padding: 32rpx;
  234. max-height: 80vh;
  235. }
  236. .search-box {
  237. display: flex;
  238. flex-direction: row;
  239. align-items: center;
  240. gap: 16rpx;
  241. margin-bottom: 32rpx;
  242. .search-type {
  243. width: 30%;
  244. ::v-deep .uni-data-select {
  245. width: 100%;
  246. }
  247. }
  248. .search-item {
  249. width: 70%;
  250. position: relative;
  251. height: 72rpx;
  252. /* 设置固定高度,与输入框高度一致 */
  253. ::v-deep .uni-easyinput__content,
  254. ::v-deep .uni-data-select {
  255. height: 72rpx;
  256. line-height: 72rpx;
  257. }
  258. }
  259. ::v-deep .uni-easyinput__content {
  260. border-radius: 16rpx;
  261. }
  262. }
  263. .list-container {
  264. max-height: 60vh;
  265. margin-bottom: 32rpx;
  266. .list-item {
  267. display: flex;
  268. align-items: center;
  269. padding: 24rpx;
  270. border-bottom: 1rpx solid #eee;
  271. &.selected {
  272. background-color: #f5f7fa;
  273. }
  274. .item-content {
  275. flex: 1;
  276. display: flex;
  277. flex-direction: column;
  278. gap: 8rpx;
  279. .item-header {
  280. display: flex;
  281. align-items: center;
  282. gap: 16rpx;
  283. .device-code {
  284. color: #666;
  285. font-size: 24rpx;
  286. }
  287. }
  288. .item-body {
  289. display: flex;
  290. flex-direction: column;
  291. gap: 4rpx;
  292. .device-name {
  293. color: #333;
  294. font-size: 28rpx;
  295. font-weight: 500;
  296. }
  297. .device-info {
  298. color: #666;
  299. font-size: 24rpx;
  300. }
  301. .device-place {
  302. color: #999;
  303. font-size: 24rpx;
  304. }
  305. }
  306. }
  307. }
  308. }
  309. .footer-actions {
  310. display: flex;
  311. gap: 32rpx;
  312. .action-btn {
  313. flex: 1;
  314. border-radius: 12rpx;
  315. font-size: 28rpx;
  316. &.confirm {
  317. background: #007AFF;
  318. color: #fff;
  319. &:disabled {
  320. background: #ccc;
  321. }
  322. }
  323. &.cancel {
  324. background: #f5f5f5;
  325. color: #666;
  326. }
  327. }
  328. }
  329. .loading-status {
  330. padding: 32rpx 0;
  331. text-align: center;
  332. }
  333. </style>