index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. <template>
  2. <div class="main-wrap p-4 flex flex-col gap-y-4">
  3. <NCard
  4. ref="mainCardRef"
  5. class="h-full"
  6. content-class="h-full flex flex-col"
  7. :style="cardHeight"
  8. >
  9. <div class="flex gap-2 min-h-6 items-center">
  10. <NForm inline label-placement="left" label-width="auto" :show-feedback="false" class="flex-1 flex-wrap">
  11. <NFormItem label="Status">
  12. <NSelect
  13. v-model:value="selectStatus"
  14. class="w-180px"
  15. :options="optionsStatus"
  16. multiple
  17. clearable
  18. :max-tag-count="2"
  19. />
  20. </NFormItem>
  21. <NFormItem label="Platform">
  22. <NSelect
  23. v-model:value="selectPlatform"
  24. class="w-160px"
  25. :options="optionsPlatform"
  26. multiple
  27. clearable
  28. :max-tag-count="1"
  29. />
  30. </NFormItem>
  31. <NFormItem label="GICCID">
  32. <NInput
  33. v-model:value="searchKeyword"
  34. round
  35. clearable
  36. placeholder="搜索..."
  37. style="max-width: 200px"
  38. size="small"
  39. />
  40. </NFormItem>
  41. <NFormItem>
  42. <NButton text :focusable="false" style="font-size: 18px" @click="clickSearch">
  43. <span class="i-mdi:search" />
  44. </NButton>
  45. </NFormItem>
  46. </NForm>
  47. <NButton
  48. class="shrink-0"
  49. text
  50. :focusable="false"
  51. style="font-size: 18px"
  52. disabled
  53. @click="clickExportSiteInfo"
  54. >
  55. <span class="i-material-symbols:download-rounded" />
  56. </NButton>
  57. </div>
  58. <NDivider style="margin: 16px 0" />
  59. <div class="flex flex-1 flex-col min-h-0">
  60. <div ref="tableWrapper" class="flex-1 min-h-0 overflow-hidden">
  61. <NDataTable
  62. ref="dataTableRef"
  63. v-model:checked-row-keys="checkedRowKeys"
  64. class="max-height-auto"
  65. :remote="true"
  66. size="small"
  67. :columns="tableColumns"
  68. :data="tableData"
  69. :row-key="tableRowKey"
  70. :loading="isRequestLoading"
  71. striped
  72. :single-line="false"
  73. :style="{ height: `${tableHeight}px` }"
  74. flex-height
  75. :scroll-x="dataTableMaxWidth"
  76. />
  77. </div>
  78. <div class="mt-3 flex items-end justify-between">
  79. <div class="flex gap-x-3 items-center">
  80. <span>已选择&nbsp;{{ checkedRowKeys.length }}&nbsp; 条</span>
  81. </div>
  82. <NPagination v-bind="tablePagination" :prefix="paginationPrefix" />
  83. </div>
  84. </div>
  85. </NCard>
  86. <SimCardDataView v-model:show="showSimCardDataView" :data="simCardData" />
  87. <SiteInfoView ref="siteInfoViewRef" />
  88. </div>
  89. </template>
  90. <script setup lang="tsx">
  91. import { useClipboard } from '@vueuse/core'
  92. import {
  93. useDialog,
  94. useMessage,
  95. NCard,
  96. NButton,
  97. NInput,
  98. NDivider,
  99. NDataTable,
  100. NNumberAnimation,
  101. NSelect,
  102. NForm,
  103. NFormItem,
  104. NScrollbar,
  105. NSpin,
  106. } from 'naive-ui'
  107. import type { PaginationProps, DataTableColumns, SelectOption } from 'naive-ui'
  108. import { useFullCard } from '@/composable/comp/useFullCard'
  109. import { getSimCardInfo } from '@/service/simCardManage'
  110. import type {
  111. SimCardInfo,
  112. SimCardMonthlyData,
  113. SimCardPlatform,
  114. SimCardStatus,
  115. } from '@/service/simCardManage/interface'
  116. import SimCardDataView from './component/simCardDataView.vue'
  117. import SiteInfoView from '../site-info-manage/component/siteInfoView.vue'
  118. import { useSimCardMonthlyCache, cacheKey } from './useSimCardMonthlyCache'
  119. const dialog = useDialog()
  120. const message = useMessage()
  121. const { memoryCache, loadingSet, failedSet, computeDailyIncrement, onPageChange, getWithPriority } =
  122. useSimCardMonthlyCache()
  123. const mainCardRef = useTemplateRef<InstanceType<typeof NCard>>('mainCardRef')
  124. const dataTableRef = useTemplateRef<InstanceType<typeof NDataTable>>('dataTableRef')
  125. const siteInfoViewRef = useTemplateRef<InstanceType<typeof SiteInfoView>>('siteInfoViewRef')
  126. const { height } = useFullCard(mainCardRef)
  127. const searchKeyword = ref('')
  128. const clickSearch = async () => {
  129. // 请求查询数据
  130. handleListQuery()
  131. }
  132. const isRequestLoading = ref(false)
  133. const checkedRowKeys = ref<Array<number | string>>([])
  134. const tableWrapper = ref<HTMLDivElement | null>(null)
  135. const tableHeight = ref(100)
  136. const cardHeight = computed(() => {
  137. return { height: `${height.value === undefined || height.value === 0 ? 480 : height.value}px` }
  138. })
  139. const prevUserListTotal = ref(0)
  140. const paginationPrefix: PaginationProps['prefix'] = (info) => {
  141. const { itemCount } = info
  142. return (
  143. itemCount && (
  144. <div>
  145. <span>总 &nbsp; </span>
  146. <NNumberAnimation
  147. from={prevUserListTotal.value}
  148. to={itemCount}
  149. onFinish={() => {
  150. prevUserListTotal.value = itemCount
  151. }}
  152. />
  153. <span> &nbsp; 条 </span>
  154. </div>
  155. )
  156. )
  157. }
  158. const tableRowKey = (rowData: SimCardInfo) => {
  159. return rowData.giccid
  160. }
  161. const showSimCardDataView = ref<boolean>(false)
  162. const simCardData = ref<SimCardMonthlyData>({
  163. simId: '',
  164. updateTime: '',
  165. month: '',
  166. allDataUsage: 0,
  167. dailyUsage: {},
  168. })
  169. function createColumns(): DataTableColumns<SimCardInfo> {
  170. return [
  171. {
  172. title: 'No.',
  173. key: '',
  174. width: 50,
  175. minWidth: 50,
  176. fixed: 'left',
  177. render(_, index) {
  178. const page = tablePagination.page ?? 1
  179. const pageSize = tablePagination.pageSize ?? 10
  180. const n = (page - 1) * pageSize + index + 1
  181. return `${n}`
  182. },
  183. },
  184. {
  185. title: 'GICCID',
  186. key: 'giccid',
  187. width: 200,
  188. fixed: 'left',
  189. render(rowData, rowIndex) {
  190. return h(
  191. 'div',
  192. { class: 'flex gap-1' },
  193. {
  194. default: () => [
  195. h(
  196. NButton,
  197. {
  198. text: true,
  199. onClick: () => {
  200. getWithPriority(rowData.giccid, rowData.month)
  201. .then((resp) => {
  202. simCardData.value = resp
  203. showSimCardDataView.value = true
  204. })
  205. .catch((err: unknown) => {
  206. if (err instanceof Error) {
  207. message.error(err.message)
  208. } else {
  209. message.error(String(err))
  210. }
  211. })
  212. },
  213. },
  214. { default: () => rowData.giccid },
  215. ),
  216. h(
  217. NButton,
  218. {
  219. text: true,
  220. focusable: false,
  221. onClick: () => {
  222. useClipboard()
  223. .copy(rowData.giccid)
  224. .then(() => {
  225. message.success('复制成功')
  226. })
  227. .catch(() => {
  228. message.error('复制失败')
  229. })
  230. },
  231. },
  232. {
  233. default: () => h('span', { class: 'i-ph:copy-duotone' }),
  234. },
  235. ),
  236. ],
  237. },
  238. )
  239. },
  240. },
  241. {
  242. title: 'SN',
  243. key: 'sn',
  244. width: 200,
  245. render(rowData, rowIndex) {
  246. return h(
  247. NScrollbar,
  248. {
  249. style: 'max-height: 120px',
  250. trigger: 'none',
  251. },
  252. {
  253. // default: () => rowData.sn.join(',\n'),
  254. default: () => [
  255. ...rowData.sn.map((sn) => {
  256. return h(
  257. NButton,
  258. {
  259. text: true,
  260. onClick: () => siteInfoViewRef.value?.show(sn),
  261. },
  262. {
  263. default: () => sn,
  264. },
  265. )
  266. }),
  267. ],
  268. },
  269. )
  270. },
  271. },
  272. {
  273. title: 'Status',
  274. key: 'status',
  275. width: 150,
  276. },
  277. {
  278. title: 'Platform',
  279. key: 'platform',
  280. width: 120,
  281. },
  282. {
  283. title: 'Usage (MB)',
  284. key: 'allDataUsage',
  285. minWidth: 100,
  286. render(rowData) {
  287. return (rowData.allDataUsage / 1024).toFixed(2)
  288. },
  289. },
  290. {
  291. title: 'Increment (MB)',
  292. key: 'increment',
  293. minWidth: 130,
  294. render(rowData) {
  295. const key = cacheKey(rowData.giccid, rowData.month)
  296. // 请求失败:停止转圈,显示 "-"
  297. if (failedSet.has(key)) return '-'
  298. // 正在加载 或 尚未开始加载:显示加载态
  299. if (loadingSet.has(key) || !memoryCache.has(key)) {
  300. return h(NSpin, { size: 'small' })
  301. }
  302. const increment = computeDailyIncrement(rowData.giccid, rowData.month)
  303. if (increment === null) return '-'
  304. const valueMB = (increment / 1024).toFixed(2)
  305. return increment >= 0 ? `+${valueMB}` : valueMB
  306. },
  307. },
  308. {
  309. title: 'UpdateTime',
  310. key: 'updateTime',
  311. width: 300,
  312. },
  313. ]
  314. }
  315. const selectStatus = ref<SimCardStatus[]>([])
  316. const optionsStatus: SelectOption[] = [
  317. { label: 'UNKNOWN', value: 'UNKNOWN' },
  318. { label: 'NOT_ACTIVATED', value: 'NOT_ACTIVATED' },
  319. { label: 'ACTIVATED', value: 'ACTIVATED' },
  320. { label: 'DEACTIVATED', value: 'DEACTIVATED' },
  321. { label: 'RETIRED', value: 'RETIRED' },
  322. ]
  323. const selectPlatform = ref<SimCardPlatform[]>([])
  324. const optionsPlatform: SelectOption[] = [
  325. { label: 'SmartHub', value: 'SmartHub' },
  326. { label: 'Acceleronix', value: 'Acceleronix' },
  327. ]
  328. const tableData = ref<SimCardInfo[]>([])
  329. const dataTableMaxWidth = ref<number | undefined>(1250)
  330. const tableColumns = computed<DataTableColumns<SimCardInfo>>(createColumns)
  331. const tablePagination = reactive<PaginationProps>({
  332. simple: false,
  333. page: 1,
  334. pageSize: 10,
  335. showSizePicker: true,
  336. pageSizes: [10, 20, 50, 100],
  337. pageSlot: 5,
  338. itemCount: 0,
  339. showQuickJumper: false,
  340. showQuickJumpDropdown: true,
  341. onUpdatePage: (page: number) => {
  342. tablePagination.page = page
  343. handleListQuery()
  344. },
  345. onUpdatePageSize: (pageSize: number) => {
  346. tablePagination.pageSize = pageSize
  347. tablePagination.page = 1
  348. handleListQuery()
  349. },
  350. })
  351. const clickExportSiteInfo = () => {
  352. dialog.warning({
  353. title: '确认导出',
  354. content: `是否确定导出当前查询条件的${tablePagination.itemCount}条数据?`,
  355. positiveText: '确定',
  356. negativeText: '取消',
  357. maskClosable: false,
  358. onPositiveClick: () => {
  359. console.log('开始导出')
  360. },
  361. onNegativeClick: () => {
  362. console.log('取消导出')
  363. },
  364. })
  365. }
  366. const querySimCardInfoList = async (
  367. pageSize: number,
  368. pageNum: number,
  369. search: string,
  370. status: string | undefined,
  371. platform: string | undefined,
  372. ) => {
  373. const responseList = await getSimCardInfo(pageNum, pageSize, search, status, platform)
  374. if (responseList.data != null) {
  375. tableData.value = responseList.data
  376. } else {
  377. tableData.value = []
  378. }
  379. tablePagination.itemCount = responseList.total
  380. // 触发预加载队列(已缓存的会被跳过)
  381. if (tableData.value.length > 0) {
  382. onPageChange(tableData.value)
  383. }
  384. }
  385. const handleListQuery = async () => {
  386. const pageSize = tablePagination.pageSize ?? 10
  387. const pageNum = tablePagination.page ?? 1
  388. const status = selectStatus.value.length > 0 ? selectStatus.value.join(',') : undefined
  389. const platform = selectPlatform.value.length > 0 ? selectPlatform.value.join(',') : undefined
  390. await querySimCardInfoList(pageSize, pageNum, searchKeyword.value, status, platform)
  391. }
  392. onMounted(async () => {
  393. // 请求查询数据
  394. await handleListQuery()
  395. nextTick(() => {
  396. const observer = new ResizeObserver((entries) => {
  397. tableHeight.value = entries[0].contentRect.height
  398. })
  399. if (tableWrapper.value) {
  400. observer.observe(tableWrapper.value)
  401. }
  402. })
  403. })
  404. </script>