index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. <script setup lang="tsx">
  2. import {
  3. NButton,
  4. NDataTable,
  5. NCard,
  6. NForm,
  7. NFormItem,
  8. NInput,
  9. NSelect,
  10. NPopconfirm,
  11. useMessage,
  12. useModal,
  13. NPagination,
  14. NButtonGroup,
  15. NDropdown,
  16. NTag,
  17. NNumberAnimation,
  18. NAlert,
  19. } from 'naive-ui'
  20. import { defineComponent, reactive, ref, useTemplateRef, nextTick } from 'vue'
  21. import { useComponentModifier } from '@/composable/comp/useComponentModifier'
  22. import { useDataTable } from '@/composable/comp/useDataTable'
  23. import { useResettableReactive } from '@/composable/useResettable'
  24. import ModalData from './ModalData.vue'
  25. import type { DataTableColumns, PaginationProps, FormRules, DropdownProps } from 'naive-ui'
  26. import type { PropType } from 'vue'
  27. export interface UserInfo {
  28. address: string
  29. age: number | null
  30. company: string
  31. email: string
  32. fullName: string
  33. number: number
  34. id: number | string
  35. phone: string
  36. registerDate: null | null
  37. sex: string | null
  38. children: UserInfo[]
  39. }
  40. interface UserForm extends Partial<UserInfo> {}
  41. defineComponent({
  42. name: 'DataTable',
  43. })
  44. const formRef = useTemplateRef<InstanceType<typeof NForm>>('formRef')
  45. const dataTableRef = useTemplateRef<InstanceType<typeof NDataTable>>('dataTableRef')
  46. const { maxHeight, updateMaxHeight } = useDataTable(dataTableRef)
  47. const message = useMessage()
  48. const modal = useModal()
  49. const { getPopconfirmModifier } = useComponentModifier()
  50. const [form, , resetForm] = useResettableReactive<UserForm>({
  51. fullName: '',
  52. sex: null,
  53. phone: '',
  54. company: '',
  55. })
  56. const rules: FormRules = {
  57. sex: {
  58. required: true,
  59. message: '请选择性别',
  60. },
  61. }
  62. const sexOptions = [
  63. { label: '男', value: '男' },
  64. { label: '女', value: '女' },
  65. ]
  66. const loading = ref(false)
  67. const striped = ref(false)
  68. const scrollX = ref(true)
  69. const singleLine = ref(true)
  70. const showDropdown = ref(false)
  71. const enableContextmenu = ref(true)
  72. const contextmenuId = ref<number | string | null>(null)
  73. const userList = ref<UserInfo[]>([])
  74. const checkedRowKeys = ref<Array<number | string>>([])
  75. const CellActions = (row: UserInfo) => (
  76. <div class='flex gap-2'>
  77. <NButton
  78. secondary
  79. type='primary'
  80. size='small'
  81. onClick={() => createOrEditUser(row)}
  82. >
  83. 编辑
  84. </NButton>
  85. <NPopconfirm
  86. {...getPopconfirmModifier()}
  87. positiveText='确定'
  88. negativeText='取消'
  89. onPositiveClick={() => {
  90. message.success('点击了删除')
  91. }}
  92. >
  93. {{
  94. default: () => '确认删除吗?',
  95. trigger: () => (
  96. <NButton
  97. secondary
  98. type='error'
  99. size='small'
  100. >
  101. 删除
  102. </NButton>
  103. ),
  104. }}
  105. </NPopconfirm>
  106. </div>
  107. )
  108. const ShowOrEdit = defineComponent({
  109. name: 'ShowOrEdit',
  110. props: {
  111. value: {
  112. type: String,
  113. required: true,
  114. },
  115. onUpdateValue: {
  116. type: Function as PropType<(value: string) => void>,
  117. },
  118. },
  119. setup(props) {
  120. const isEdit = ref(false)
  121. const inputRef = ref<InstanceType<typeof NInput> | null>(null)
  122. const inputValue = ref(props.value)
  123. function handleOnClick() {
  124. isEdit.value = true
  125. nextTick(() => {
  126. inputRef.value?.focus()
  127. })
  128. }
  129. function handleChange() {
  130. if (!inputValue.value.trim()) {
  131. message.error('为空就再也编辑不了了')
  132. inputValue.value = props.value
  133. }
  134. isEdit.value = false
  135. props.onUpdateValue?.(inputValue.value)
  136. }
  137. return () => (
  138. <div onClick={handleOnClick}>
  139. {isEdit.value ? (
  140. <NInput
  141. ref={inputRef}
  142. value={inputValue.value}
  143. clearable
  144. onUpdateValue={(value) => {
  145. inputValue.value = value
  146. }}
  147. onBlur={handleChange}
  148. />
  149. ) : (
  150. <span>{props.value}</span>
  151. )}
  152. </div>
  153. )
  154. },
  155. })
  156. const columns: DataTableColumns<UserInfo> = [
  157. {
  158. type: 'selection',
  159. options: [
  160. 'all',
  161. 'none',
  162. {
  163. label: '选中前 3 行可选数据',
  164. key: 'f2',
  165. onSelect: (pageData) => {
  166. checkedRowKeys.value = pageData
  167. .filter((row) => row.number < 500)
  168. .map((row) => row.id)
  169. .slice(0, 3)
  170. },
  171. },
  172. ],
  173. disabled: (row) => {
  174. return ['4', '5', '8', '9'].includes(String(row.number)[0])
  175. },
  176. },
  177. {
  178. key: 'number',
  179. title: '编号',
  180. width: 100,
  181. },
  182. {
  183. key: 'fullName',
  184. width: 160,
  185. title: () => {
  186. return (
  187. <div class='flex items-center gap-x-2'>
  188. <span>姓名</span>
  189. <span class='iconify ph--pencil-simple-line' />
  190. </div>
  191. )
  192. },
  193. render: (row, index) => (
  194. <ShowOrEdit
  195. value={row.fullName}
  196. onUpdateValue={(value) => {
  197. userList.value[index].fullName = value
  198. }}
  199. />
  200. ),
  201. },
  202. {
  203. key: 'sex',
  204. title: '性别',
  205. width: 100,
  206. render: (row) => {
  207. const isMale = row.sex === '男'
  208. return (
  209. <div>
  210. <span
  211. class={
  212. isMale
  213. ? 'iconify text-sky-500 ph--gender-male'
  214. : 'iconify text-pink-500 ph--gender-female'
  215. }
  216. />
  217. </div>
  218. )
  219. },
  220. },
  221. {
  222. key: 'age',
  223. title: '年龄',
  224. width: 100,
  225. render: (row) => {
  226. const age = row.age ?? 0
  227. return (
  228. <NTag
  229. bordered={false}
  230. size='small'
  231. type={age > 50 ? 'error' : age > 40 ? 'warning' : age > 30 ? 'info' : 'success'}
  232. >
  233. {row.age}
  234. </NTag>
  235. )
  236. },
  237. },
  238. {
  239. key: 'email',
  240. title: '邮箱',
  241. },
  242. {
  243. key: 'phone',
  244. title: '电话',
  245. },
  246. {
  247. key: 'address',
  248. title: '地址',
  249. },
  250. {
  251. key: 'company',
  252. title: '公司',
  253. },
  254. {
  255. key: 'registerDate',
  256. title: '注册日期',
  257. },
  258. {
  259. width: 140,
  260. key: 'actions',
  261. align: 'center',
  262. title: '操作',
  263. fixed: 'right',
  264. render: (row) => <CellActions {...row} />,
  265. },
  266. ]
  267. function rowProps(row: UserInfo) {
  268. return {
  269. onContextmenu: (e: MouseEvent) => {
  270. e.preventDefault()
  271. showDropdown.value = false
  272. nextTick().then(() => {
  273. contextmenuId.value = row.number
  274. showDropdown.value = true
  275. dropdownOptions.x = e.clientX
  276. dropdownOptions.y = e.clientY
  277. })
  278. },
  279. }
  280. }
  281. const pagination = reactive<PaginationProps>({
  282. page: 1,
  283. pageSize: 10,
  284. showSizePicker: true,
  285. pageSizes: [10, 20, 50, 100],
  286. itemCount: 0,
  287. showQuickJumper: true,
  288. showQuickJumpDropdown: true,
  289. onUpdatePage: (page: number) => {
  290. pagination.page = page
  291. getUserList()
  292. },
  293. onUpdatePageSize: (pageSize: number) => {
  294. pagination.pageSize = pageSize
  295. pagination.page = 1
  296. getUserList()
  297. },
  298. })
  299. const prevItemCount = ref(0)
  300. const paginationPrefix: PaginationProps['prefix'] = (info) => {
  301. const { itemCount } = info
  302. return (
  303. itemCount && (
  304. <div>
  305. <span>总&nbsp;</span>
  306. <NNumberAnimation
  307. from={prevItemCount.value}
  308. to={itemCount}
  309. onFinish={() => {
  310. prevItemCount.value = itemCount
  311. }}
  312. />
  313. <span>&nbsp;条</span>
  314. </div>
  315. )
  316. )
  317. }
  318. const dropdownOptions = reactive<DropdownProps>({
  319. x: 0,
  320. y: 0,
  321. options: [
  322. {
  323. label: '编辑',
  324. key: 'edit',
  325. },
  326. {
  327. label: () => <span class='text-red-500'>删除</span>,
  328. key: 'delete',
  329. },
  330. ],
  331. onClickoutside: () => {
  332. showDropdown.value = false
  333. },
  334. onSelect: () => {
  335. message.info(`id: ${contextmenuId.value}`)
  336. showDropdown.value = false
  337. },
  338. })
  339. async function request(pageSize: number): Promise<{ data: UserInfo[]; total: number }> {
  340. return fetch(`https://lithe-admin-serverless.vercel.app/api/faker?limit=${pageSize}`, {
  341. method: 'GET',
  342. }).then((res) => res.json())
  343. }
  344. function inputOnlyAllowNumber(value: string) {
  345. return !value || /^\d+$/.test(value)
  346. }
  347. function createOrEditUser(data?: UserInfo) {
  348. const title = data ? '编辑数据' : '新增数据'
  349. const handleSubmitClick = () => {
  350. message.success('点击了提交')
  351. m.destroy()
  352. }
  353. function handleUpdateClick() {
  354. message.info('点击了更新')
  355. m.destroy()
  356. }
  357. function handleCancelClick() {
  358. m.destroy()
  359. }
  360. const m = modal.create({
  361. title,
  362. preset: 'card',
  363. draggable: true,
  364. style: {
  365. width: '500px',
  366. },
  367. content: () => (
  368. <ModalData
  369. data={data || {}}
  370. onSubmit={handleSubmitClick}
  371. onUpdate={handleUpdateClick}
  372. onCancel={handleCancelClick}
  373. />
  374. ),
  375. })
  376. }
  377. const handleQueryClick = () => {
  378. formRef.value?.validate((errors) => {
  379. if (!errors) {
  380. getUserList()
  381. }
  382. })
  383. }
  384. function handleDownloadCsvClick() {
  385. if (!dataTableRef.value) return
  386. dataTableRef.value.downloadCsv()
  387. }
  388. async function getUserList() {
  389. loading.value = true
  390. const pageSize = pagination.pageSize || 10
  391. const res = await request(pageSize).finally(() => {
  392. loading.value = false
  393. })
  394. userList.value = res.data
  395. pagination.itemCount = 300
  396. }
  397. getUserList()
  398. </script>
  399. <template>
  400. <div class="main-wrap flex flex-col gap-y-2 p-4">
  401. <NAlert
  402. type="info"
  403. closable
  404. @after-leave="updateMaxHeight"
  405. >
  406. 一个数据表格的例子,不算复杂,有一个高度的计算,也许对你有帮助
  407. </NAlert>
  408. <NCard>
  409. <div class="mb-2 flex gap-x-4">
  410. <NForm
  411. ref="formRef"
  412. :model="form"
  413. :rules="rules"
  414. inline
  415. label-placement="left"
  416. >
  417. <NFormItem
  418. label="姓名"
  419. path="fullName"
  420. >
  421. <NInput
  422. v-model:value="form.fullName"
  423. style="width: 120px"
  424. clearable
  425. />
  426. </NFormItem>
  427. <NFormItem
  428. label="性别"
  429. path="sex"
  430. >
  431. <NSelect
  432. v-model:value="form.sex"
  433. :options="sexOptions"
  434. style="width: 88px"
  435. clearable
  436. />
  437. </NFormItem>
  438. <NFormItem
  439. label="联系方式"
  440. path="phone"
  441. >
  442. <NInput
  443. v-model:value="form.phone"
  444. clearable
  445. :allow-input="inputOnlyAllowNumber"
  446. />
  447. </NFormItem>
  448. <NFormItem
  449. label="公司"
  450. path="company"
  451. >
  452. <NInput
  453. v-model:value="form.company"
  454. clearable
  455. />
  456. </NFormItem>
  457. </NForm>
  458. <div class="flex gap-2">
  459. <NButton
  460. type="success"
  461. @click="createOrEditUser()"
  462. >
  463. <template #icon>
  464. <span class="iconify ph--plus-circle" />
  465. </template>
  466. 新增数据
  467. </NButton>
  468. <NButton
  469. type="info"
  470. @click="handleQueryClick"
  471. :loading="loading"
  472. :disabled="loading"
  473. >
  474. <template #icon>
  475. <span class="iconify ph--magnifying-glass" />
  476. </template>
  477. 查询
  478. </NButton>
  479. <NButton
  480. type="warning"
  481. @click="resetForm"
  482. >
  483. <template #icon>
  484. <span class="iconify ph--arrow-clockwise" />
  485. </template>
  486. 重置
  487. </NButton>
  488. </div>
  489. </div>
  490. <div>
  491. <NDataTable
  492. ref="dataTableRef"
  493. v-model:checked-row-keys="checkedRowKeys"
  494. :remote="true"
  495. :max-height="maxHeight"
  496. :scroll-x="scrollX ? 1800 : 0"
  497. :min-height="166.6"
  498. :columns="columns"
  499. :data="userList"
  500. :row-key="(row) => row.id"
  501. :loading="loading"
  502. :striped="striped"
  503. :row-props="rowProps"
  504. :single-line="singleLine"
  505. />
  506. <div class="mt-3 flex items-end justify-between">
  507. <div class="flex items-center gap-x-3">
  508. <span>已选择&nbsp;{{ checkedRowKeys.length }}&nbsp; 条</span>
  509. <NButtonGroup
  510. size="small"
  511. :ghost="true"
  512. >
  513. <NButton
  514. @click="striped = !striped"
  515. :type="striped ? 'primary' : 'default'"
  516. secondary
  517. >
  518. 条纹风格
  519. </NButton>
  520. <NButton
  521. @click="singleLine = !singleLine"
  522. :type="!singleLine ? 'primary' : 'default'"
  523. secondary
  524. >
  525. 单线风格
  526. </NButton>
  527. <NButton
  528. @click="scrollX = !scrollX"
  529. :type="scrollX ? 'primary' : 'default'"
  530. secondary
  531. >
  532. 横向滚动
  533. </NButton>
  534. <NButton
  535. @click="enableContextmenu = !enableContextmenu"
  536. :type="enableContextmenu ? 'primary' : 'default'"
  537. secondary
  538. >
  539. 右键菜单
  540. </NButton>
  541. <NButton
  542. @click="handleDownloadCsvClick"
  543. secondary
  544. type="info"
  545. >
  546. 下载为Csv
  547. </NButton>
  548. </NButtonGroup>
  549. </div>
  550. <NPagination
  551. v-bind="pagination"
  552. :prefix="paginationPrefix"
  553. />
  554. </div>
  555. </div>
  556. </NCard>
  557. <NDropdown
  558. placement="bottom-start"
  559. trigger="manual"
  560. v-bind="dropdownOptions"
  561. :show="enableContextmenu && showDropdown"
  562. />
  563. </div>
  564. </template>