123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619 |
- <script setup lang="tsx">
- import {
- NButton,
- NDataTable,
- NCard,
- NForm,
- NFormItem,
- NInput,
- NSelect,
- NPopconfirm,
- useMessage,
- useModal,
- NPagination,
- NButtonGroup,
- NDropdown,
- NTag,
- NNumberAnimation,
- NAlert,
- } from 'naive-ui'
- import { defineComponent, reactive, ref, useTemplateRef, nextTick } from 'vue'
- import { ScrollContainer } from '@/components'
- import { useInjection, useComponentModifier, useResettableReactive } from '@/composables'
- import { mediaQueryInjectionKey } from '@/injection'
- import ActionModal from './ActionModal.vue'
- import type { DataTableColumns, PaginationProps, FormRules, DropdownProps } from 'naive-ui'
- import type { PropType } from 'vue'
- export interface UserInfo {
- address: string
- age: number | null
- company: string
- email: string
- fullName: string
- number: number
- id: number | string
- phone: string
- registerDate: null | null
- sex: string | null
- children: UserInfo[]
- }
- defineOptions({
- name: 'DataTable',
- })
- const { isMaxMd, isMaxLg } = useInjection(mediaQueryInjectionKey)
- const formRef = useTemplateRef<InstanceType<typeof NForm>>('formRef')
- const dataTableRef = useTemplateRef<InstanceType<typeof NDataTable>>('dataTableRef')
- const message = useMessage()
- const modal = useModal()
- const { getPopconfirmModifier } = useComponentModifier()
- const [form, , resetForm] = useResettableReactive<Partial<UserInfo>>({
- fullName: '',
- sex: null,
- phone: '',
- company: '',
- })
- const rules: FormRules = {
- sex: {
- required: true,
- message: '请选择性别',
- },
- }
- const sexOptions = [
- { label: '男', value: '男' },
- { label: '女', value: '女' },
- ]
- const isRequestLoading = ref(false)
- const enableStriped = ref(false)
- const enableScrollX = ref(true)
- const enableSingleLine = ref(true)
- const enableContextmenu = ref(true)
- const showDropdown = ref(false)
- const contextmenuId = ref<number | string | null>(null)
- const dataList = ref<UserInfo[]>([])
- const checkedRowKeys = ref<Array<number | string>>([])
- const CellActions = (row: UserInfo) => (
- <div class='flex gap-2'>
- <NButton
- secondary
- type='primary'
- size='small'
- onClick={() => createOrEditData(row)}
- >
- 编辑
- </NButton>
- <NPopconfirm
- {...getPopconfirmModifier()}
- positiveText='确定'
- negativeText='取消'
- onPositiveClick={() => {
- message.success('点击了删除')
- }}
- >
- {{
- default: () => '确认删除吗?',
- trigger: () => (
- <NButton
- secondary
- type='error'
- size='small'
- >
- 删除
- </NButton>
- ),
- }}
- </NPopconfirm>
- </div>
- )
- const ShowOrEdit = defineComponent({
- name: 'ShowOrEdit',
- props: {
- value: {
- type: String,
- required: true,
- },
- onUpdateValue: {
- type: Function as PropType<(value: string) => void>,
- },
- },
- setup(props) {
- const isEdit = ref(false)
- const inputRef = ref<InstanceType<typeof NInput> | null>(null)
- const inputValue = ref(props.value)
- function onClick() {
- isEdit.value = true
- nextTick(() => {
- inputRef.value?.focus()
- })
- }
- function onBlur() {
- if (!inputValue.value.trim()) {
- message.error('为空就再也编辑不了了')
- inputValue.value = props.value
- }
- isEdit.value = false
- props.onUpdateValue?.(inputValue.value)
- }
- return () => (
- <div onClick={onClick}>
- {isEdit.value ? (
- <NInput
- ref={inputRef}
- value={inputValue.value}
- clearable
- onUpdateValue={(value) => {
- inputValue.value = value
- }}
- onBlur={onBlur}
- />
- ) : (
- <span>{props.value}</span>
- )}
- </div>
- )
- },
- })
- const columns: DataTableColumns<UserInfo> = [
- {
- type: 'selection',
- options: [
- 'all',
- 'none',
- {
- label: '选中前 3 行可选数据',
- key: 'f2',
- onSelect: (pageData) => {
- checkedRowKeys.value = pageData
- .filter((row) => row.number < 500)
- .map((row) => row.id)
- .slice(0, 3)
- },
- },
- ],
- disabled: (row) => {
- return ['4', '5', '8', '9'].includes(String(row.number)[0])
- },
- },
- {
- key: 'number',
- title: '编号',
- width: 100,
- },
- {
- key: 'fullName',
- width: 160,
- title: () => {
- return (
- <div class='flex items-center gap-x-2'>
- <span>姓名</span>
- <span class='iconify ph--pencil-simple-line' />
- </div>
- )
- },
- render: (row, index) => (
- <ShowOrEdit
- value={row.fullName}
- onUpdateValue={(value) => {
- dataList.value[index].fullName = value
- }}
- />
- ),
- },
- {
- key: 'sex',
- title: '性别',
- width: 100,
- render: (row) => {
- const isMale = row.sex === '男'
- return (
- <div>
- <span
- class={
- isMale
- ? 'iconify text-sky-500 ph--gender-male'
- : 'iconify text-pink-500 ph--gender-female'
- }
- />
- </div>
- )
- },
- },
- {
- key: 'age',
- title: '年龄',
- width: 100,
- render: (row) => {
- const age = row.age ?? 0
- return (
- <NTag
- bordered={false}
- size='small'
- type={age > 50 ? 'error' : age > 40 ? 'warning' : age > 30 ? 'info' : 'success'}
- >
- {row.age}
- </NTag>
- )
- },
- },
- {
- key: 'email',
- title: '邮箱',
- },
- {
- key: 'phone',
- title: '电话',
- },
- {
- key: 'address',
- title: '地址',
- },
- {
- key: 'company',
- title: '公司',
- },
- {
- key: 'registerDate',
- title: '注册日期',
- },
- {
- width: 140,
- key: 'actions',
- align: 'center',
- title: '操作',
- fixed: 'right',
- render: (row) => <CellActions {...row} />,
- },
- ]
- function rowProps(row: UserInfo) {
- return {
- onContextmenu: (e: MouseEvent) => {
- e.preventDefault()
- showDropdown.value = false
- nextTick().then(() => {
- contextmenuId.value = row.number
- showDropdown.value = true
- dropdownOptions.x = e.clientX
- dropdownOptions.y = e.clientY
- })
- },
- }
- }
- const pagination = reactive<PaginationProps>({
- page: 1,
- pageSize: 10,
- showSizePicker: true,
- pageSizes: [10, 20, 50, 100],
- itemCount: 0,
- showQuickJumper: true,
- showQuickJumpDropdown: true,
- onUpdatePage: (page: number) => {
- pagination.page = page
- getDataList()
- },
- onUpdatePageSize: (pageSize: number) => {
- pagination.pageSize = pageSize
- pagination.page = 1
- getDataList()
- },
- })
- const prevUserListTotal = ref(0)
- const paginationPrefix: PaginationProps['prefix'] = (info) => {
- const { itemCount } = info
- return (
- itemCount && (
- <div>
- <span>总 </span>
- <NNumberAnimation
- from={prevUserListTotal.value}
- to={itemCount}
- onFinish={() => {
- prevUserListTotal.value = itemCount
- }}
- />
- <span> 条</span>
- </div>
- )
- )
- }
- const dropdownOptions = reactive<DropdownProps>({
- x: 0,
- y: 0,
- options: [
- {
- label: '编辑',
- key: 'edit',
- },
- {
- label: () => <span class='text-red-500'>删除</span>,
- key: 'delete',
- },
- ],
- onClickoutside: () => {
- showDropdown.value = false
- },
- onSelect: () => {
- message.info(`id: ${contextmenuId.value}`)
- showDropdown.value = false
- },
- })
- async function request(pageSize: number): Promise<{ data: UserInfo[]; total: number }> {
- return fetch(`https://lithe-admin-serverless.havenovelgod.com/api/faker?limit=${pageSize}`, {
- method: 'GET',
- }).then((res) => res.json())
- }
- function inputOnlyAllowNumber(value: string) {
- return !value || /^\d+$/.test(value)
- }
- function createOrEditData(data?: UserInfo) {
- const title = data ? '编辑数据' : '新增数据'
- const handleSubmitClick = () => {
- message.success('点击了提交')
- m.destroy()
- }
- function handleUpdateClick() {
- message.info('点击了更新')
- m.destroy()
- }
- function handleCancelClick() {
- m.destroy()
- }
- const m = modal.create({
- title,
- preset: 'card',
- draggable: true,
- style: {
- width: '500px',
- ...(isMaxMd.value ? { marginInline: '16px' } : {}),
- },
- content: () => (
- <ActionModal
- data={data || {}}
- onSubmit={handleSubmitClick}
- onUpdate={handleUpdateClick}
- onCancel={handleCancelClick}
- />
- ),
- })
- }
- const handleQueryClick = () => {
- formRef.value?.validate((errors) => {
- if (!errors) {
- getDataList()
- }
- })
- }
- function handleDownloadCsvClick() {
- if (!dataTableRef.value) return
- dataTableRef.value.downloadCsv()
- }
- async function getDataList() {
- isRequestLoading.value = true
- const pageSize = pagination.pageSize || 10
- const res = await request(pageSize).finally(() => {
- isRequestLoading.value = false
- })
- dataList.value = res.data
- pagination.itemCount = 300
- }
- getDataList()
- </script>
- <template>
- <ScrollContainer
- wrapper-class="flex flex-col gap-y-2"
- :scrollable="isMaxLg"
- >
- <NAlert
- type="info"
- closable
- >
- 一个数据表格的例子,不算复杂,也许对你有帮助
- </NAlert>
- <NCard
- :size="isMaxMd ? 'small' : undefined"
- class="flex-1"
- content-class="flex flex-col"
- >
- <div class="mb-2 flex justify-end gap-x-4 max-xl:mb-4 max-xl:flex-wrap">
- <NForm
- ref="formRef"
- :model="form"
- :rules="rules"
- :inline="!isMaxLg"
- label-placement="left"
- class="max-lg:w-full max-lg:flex-col"
- :label-width="isMaxLg ? 70 : undefined"
- >
- <NFormItem
- label="姓名"
- path="fullName"
- >
- <NInput
- v-model:value="form.fullName"
- clearable
- />
- </NFormItem>
- <NFormItem
- label="性别"
- path="sex"
- >
- <NSelect
- v-model:value="form.sex"
- :options="sexOptions"
- style="min-width: 88px"
- clearable
- />
- </NFormItem>
- <NFormItem
- label="联系方式"
- path="phone"
- >
- <NInput
- v-model:value="form.phone"
- clearable
- :allow-input="inputOnlyAllowNumber"
- />
- </NFormItem>
- <NFormItem
- label="公司"
- path="company"
- >
- <NInput
- v-model:value="form.company"
- clearable
- />
- </NFormItem>
- </NForm>
- <div class="flex gap-2">
- <NButton
- type="success"
- @click="createOrEditData()"
- >
- <template #icon>
- <span class="iconify ph--plus-circle" />
- </template>
- 新增数据
- </NButton>
- <NButton
- type="info"
- @click="handleQueryClick"
- :loading="isRequestLoading"
- :disabled="isRequestLoading"
- >
- <template #icon>
- <span class="iconify ph--magnifying-glass" />
- </template>
- 查询
- </NButton>
- <NButton
- type="warning"
- @click="resetForm"
- >
- <template #icon>
- <span class="iconify ph--arrow-clockwise" />
- </template>
- 重置
- </NButton>
- </div>
- </div>
- <div class="flex flex-1 flex-col">
- <NDataTable
- class="flex-1"
- ref="dataTableRef"
- v-model:checked-row-keys="checkedRowKeys"
- :remote="true"
- :flex-height="!isMaxLg"
- :scroll-x="enableScrollX ? 1800 : 0"
- :columns="columns"
- :data="dataList"
- :row-key="(row) => row.id"
- :loading="isRequestLoading"
- :striped="enableStriped"
- :row-props="rowProps"
- :single-line="enableSingleLine"
- />
- <div class="mt-3 flex items-end justify-between max-xl:flex-col max-xl:gap-y-2">
- <div class="flex items-center justify-between gap-x-3">
- <span>已选择 {{ checkedRowKeys.length }} 条</span>
- <NButtonGroup
- size="small"
- :ghost="true"
- >
- <NButton
- @click="enableStriped = !enableStriped"
- :type="enableStriped ? 'primary' : 'default'"
- secondary
- >
- 条纹风格
- </NButton>
- <NButton
- @click="enableSingleLine = !enableSingleLine"
- :type="!enableSingleLine ? 'primary' : 'default'"
- secondary
- >
- 单线风格
- </NButton>
- <NButton
- @click="enableScrollX = !enableScrollX"
- :type="enableScrollX ? 'primary' : 'default'"
- secondary
- >
- 横向滚动
- </NButton>
- <NButton
- v-show="!isMaxMd"
- @click="enableContextmenu = !enableContextmenu"
- :type="enableContextmenu ? 'primary' : 'default'"
- secondary
- >
- 右键菜单
- </NButton>
- <NButton
- v-show="!isMaxMd"
- @click="handleDownloadCsvClick"
- secondary
- type="info"
- >
- 下载为Csv
- </NButton>
- </NButtonGroup>
- </div>
- <NPagination
- v-bind="pagination"
- :prefix="paginationPrefix"
- :page-slot="isMaxMd ? 5 : undefined"
- :show-quick-jump-dropdown="!isMaxMd"
- :show-quick-jumper="!isMaxMd"
- :show-size-picker="!isMaxMd"
- />
- </div>
- </div>
- </NCard>
- <NDropdown
- placement="bottom-start"
- trigger="manual"
- v-bind="dropdownOptions"
- :show="enableContextmenu && showDropdown"
- />
- </ScrollContainer>
- </template>
|