什么?赶紧把这些重复性的东西封装起来吧,不就更多时间loaf on a job(懂的都懂)

前言

相信各位小伙伴很多时候都遇到过产品给你提后台增删改查的需求,这些需求往往不难,重复性的工作罢了,但是这些重复性的工作有没有把它整合起来呢,封装成公共的,这样下次产品再来提,就不用每次都手写一大堆,正常交付的同时还能有时间去做自己喜欢的事情,嘿嘿,下面是我的一些思路,能帮到各位小伙伴的话,那就点点赞吧,不要逼我跪下来求你。

背景

后台是基于vue3+ts+pinia+unocss+naiv ui 搭建的后台项目,很多ui库的组件其实都是大差不差的,喜欢啥用啥就好了。然后是根据naive ui提供的组件,封装了一个通用的表格,以及查询的组件,构成了页面最基础的增改查功能。

unocss文档:unocss.dev/

naive ui文档:www.naiveui.com/zh-CN/os-th...

pinia文档:pinia.vuejs.org/zh/core-con...

组件设计

1.表格上面根据需求有对应的按钮,比如:新增、导出、导入等等

2.最上面是基础的查询栏,根据需求选择不同的组件,例如:输入框、下拉选择框、时间选择器

废话不多说,先从查询栏开始

一、首先定义类型

form.ts 复制代码
import type { InputProps, SelectProps } from 'naive-ui'

export enum componentType {
  input = 'n-input',
  select = 'n-select',
  datePicker = 'date-picker',
}

interface FormItemProps {
  // 属性字段名称
  field?: string
  // 默认值
  defaultValue?: any
  // 名称
  label: string
  // 组件参数
  componentProps?: InputProps | SelectProps | any
  // 组件类型
  component: componentType
  focus?: boolean
  datePickerProps?: {
    startField?: string
    endField?: string
  }
  display?: boolean
  displayField?: string
  displayChoose?: Array<string | number>
  displayElse?: Array<string | number>
}

export type FormItemsProps = FormItemProps[]

二、定义组件的hooks

useFormItemsProps.ts 复制代码
import { computed, ref } from 'vue'
import type { FormItemsProps } from '../types'
import { componentType } from '../types'
import { isUndefined } from '~/src/utils/common'

export function useFormItemsProps(formItems: FormItemsProps) {
  const getFormItemsProps = computed(() => {
    const props = ref<any[]>([])

    formItems.forEach((item) => {
      const { componentProps, label } = item
      const prop: any = {}
      if (isUndefined(componentProps?.placeholder)) {
        switch (item.component) {
          case componentType.input:
            prop.placeholder = `请输入${label}`
            break
          case componentType.select:
            prop.placeholder = `请选择${label}`
            // 在传入的选项中没有对应当前值的选项时,这个值应该对应的选项。如果设为 false,不会为找不到对应选项的值生成回退选项也不会显示它,未在选项中的值会被视为不合法,操作过程中会被组件清除掉
            prop['fallback-option'] = false
            break
          case componentType.datePicker:
            prop.placeholder = `请选择${label}`
            break
        }
      }
      // prop.clearable = true
      prop.filterable = true
      props.value.push({
        ...prop,
        ...componentProps,
      })
    })

    return {
      ...props.value,
    }
  })

  return {
    getFormItemsProps,
  }
}
useFormValue.ts 复制代码
import type { FormItemsProps } from '../types'
import { componentType } from '../types'
import { DATE_DAYS, DATE_FORMAT, END_FIELD, START_FIELD } from '../const'
import { dateSub, formatToDate } from '~/src/utils/common/dateUtil'

export function useFormValue(formModel: any, formItems: FormItemsProps) {
  function initFormValue() {
    formItems.forEach((item) => {
      const { field, defaultValue, component } = item

      if (component === componentType.batchSelect) {
        formModel[field as string] = []
        return
      }

      if (component === componentType.datePicker) {
        if (defaultValue === '') {
          formModel[item.datePickerProps?.startField || START_FIELD] = ''
          formModel[item.datePickerProps?.endField || END_FIELD] = ''
        }
        else {
          formModel[item.datePickerProps?.startField || START_FIELD] = dateSub(new Date(), defaultValue ? (defaultValue as number) - 1 : DATE_DAYS - 1)
          formModel[item.datePickerProps?.endField || END_FIELD] = formatToDate(new Date(), DATE_FORMAT)
        }
        return
      }

      formModel[field as string] = defaultValue || ''
    })
  }

  function restFormValue() {
    initFormValue()
  }

  return {
    initFormValue,
    restFormValue,
  }
}

const.ts 文件是定义属性字段的

const.ts 复制代码
// 日期选择开始时间字段
export const START_FIELD = 'start_time'

// 日期选择结束时间字段
export const END_FIELD = 'end_time'

// 日期选择格式
export const DATE_FORMAT = 'yyyy-MM-dd'

// 日期选择默认天数
export const DATE_DAYS = 7

isUndefined是我在utils定义的typeof类型

typeof.ts 复制代码
enum EnumDataType {
  number = '[object Number]',
  string = '[object String]',
  boolean = '[object Boolean]',
  null = '[object Null]',
  undefined = '[object Undefined]',
  object = '[object Object]',
  array = '[object Array]',
  date = '[object Date]',
  regexp = '[object RegExp]',
  set = '[object Set]',
  map = '[object Map]',
  file = '[object File]',
}

export function isNumber(data: unknown) {
  return Object.prototype.toString.call(data) === EnumDataType.number
}
export function isString(data: unknown) {
  return Object.prototype.toString.call(data) === EnumDataType.string
}
export function isBoolean(data: unknown) {
  return Object.prototype.toString.call(data) === EnumDataType.boolean
}
export function isNull(data: unknown) {
  return Object.prototype.toString.call(data) === EnumDataType.null
}
export function isUndefined(data: unknown) {
  return Object.prototype.toString.call(data) === EnumDataType.undefined
}
export function isObject(data: unknown) {
  return Object.prototype.toString.call(data) === EnumDataType.object
}
export function isArray(data: unknown): data is Array<any> {
  return Object.prototype.toString.call(data) === EnumDataType.array
}
export function isDate(data: unknown) {
  return Object.prototype.toString.call(data) === EnumDataType.date
}
export function isRegExp(data: unknown) {
  return Object.prototype.toString.call(data) === EnumDataType.regexp
}
export function isSet(data: unknown) {
  return Object.prototype.toString.call(data) === EnumDataType.set
}
export function isMap(data: unknown) {
  return Object.prototype.toString.call(data) === EnumDataType.map
}
export function isFile(data: unknown) {
  return Object.prototype.toString.call(data) === EnumDataType.file
}

三、QueryForm.vue文件的代码

js 复制代码
<script setup lang="ts">
import { computed, onMounted, reactive, ref, unref, watch } from 'vue'
import { componentType } from './types'
import type { FormItemsProps } from './types'
import { END_FIELD, START_FIELD } from './const'
import { useFormItemsProps, useFormValue } from './hooks'

interface Props {
  formItems: FormItemsProps
  // 是否展示重置按钮
  showResetButton?: boolean
  // 是否展示查询按钮
  showQueryButton?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  showResetButton: true,
  showQueryButton: true,
})

const emits = defineEmits(['search', 'reset'])
const getProps = computed(() => {
  return {
    ...props,
  }
})

const formModel = reactive({})
const focusIndex = ref(-1)

const { initFormValue, restFormValue } = useFormValue(formModel, getProps.value.formItems)

const { getFormItemsProps } = useFormItemsProps(getProps.value.formItems)

onMounted(() => {
  //这里是输入完值后除了点击查询按钮会进行查询操作,按回车也能进行查询操作
  window.addEventListener('keydown', handkeyCode, true)
  initFormValue()
  emits('search', unref(formModel))
})

function handkeyCode(e) {
  if (e.keyCode === 13)
    handleSearch()
}

function handleSearch() {
  emits('search', unref(formModel))
}

watch(
  formModel,
  async (newValue) => {
    for (let index = 0; index < props.formItems.length; index++) {
      const item = props.formItems[index] as any
      if (('display' in item) === false)
        item.display = true
      if (item.displayField) {
        if (item.displayChoose?.length > 0) {
          item.displayChoose?.forEach((displayItem) => {
            if (formModel[item.displayField] === displayItem)
              item.display = true
            else
              item.display = false
          })
        }
        if (item.displayElse?.length > 0) {
          item.displayElse?.forEach((displayItem) => {
            if (formModel[item.displayField] === displayItem)
              item.display = false
            else
              item.display = true
          })
        }
      }
    }
  },
  { immediate: true },
)

function clearOneForm(fieldArr: [string]) {
  fieldArr.forEach((item) => {
    formModel[item] = ''
  })
}
function handleReset() {
  restFormValue()
  handleSearch()
  emits('reset')
}

defineExpose({
  handleSearch,
  clearOneForm,
})
</script>

<template>
  <div class="pt-10px">
    <n-form inline label-placement="left" :show-feedback="false" :show-label="false" class="flex" style="flex-wrap: wrap;margin-bottom: -20px;">
      <div v-for="(item, index) in formItems" :key="index">
        <n-form-item v-if="item.display" :key="index" style="width:300px;margin-right: 20px;margin-bottom: 20px;">
          <p class="flex absolute left-6px z-10 text-14px lable-bg pl-2px pr-2px" :class="focusIndex === index ? 'form-lebel__focus' : ''">
            {{ item.label }}
          </p>
          
          <DatePicker
            v-if="item.component === componentType.datePicker"
            v-model:startTime="formModel[item.datePickerProps?.startField || START_FIELD]"
            v-model:endTime="formModel[item.datePickerProps?.endField || END_FIELD]"
            :on-focus="() => { focusIndex = index }"
            :on-blur="() => { focusIndex = -1 }"
          />
          
          <component
            v-bind="getFormItemsProps[index]"
            :is="item.component"
            v-else
            v-model:value="formModel[item.field as string]"
            :on-focus="() => { focusIndex = index }"
            :on-blur="() => { focusIndex = -1 }"
          />
        </n-form-item>
      </div>
      <n-form-item style="width:300px;margin-right: 20px;margin-bottom: 20px;">
        <n-space>
          <n-button v-if="showQueryButton" type="primary" @click="handleSearch">
            查询
            <template #icon>
              <svg-icon icon="ri:search-line" />
            </template>
          </n-button>
          <n-button v-if="showResetButton" @click="handleReset">
            重置
          </n-button>
          <slot name="buttons" />
        </n-space>
      </n-form-item>
    </n-form>
  </div>
</template>

<style scoped>
.lable-bg {
  background-image: linear-gradient(#fff, #fff);
  top: -12px;
  line-height: normal !important;
}

.form-lebel__focus {
  transition: color .3s cubic-bezier(0.4, 0, 0.2, 1);
  color:#40a9ff !important;
}
</style>

DatePicker是根据ui库的日期选择器+产品的需求封装的业务组件,也一并分享出来

DatePicker.vue 复制代码
<script setup lang="ts">
import { computed } from 'vue'
import { formatToDate, getTimeStamp } from '@/utils/common/dateUtil'

const props = defineProps(['startTime', 'endTime'])
const emit = defineEmits(['update:startTime', 'update:endTime'])

const getValue = computed(() => {
  if (props.startTime && props.endTime)
    return [getTimeStamp(new Date(props.startTime)), getTimeStamp(new Date(props.endTime))] as [number, number]
  else
    return null
})

function handleUpdate(value: [number, number] | null) {
  if (value) {
    emit('update:startTime', formatToDate(value[0]))
    emit('update:endTime', formatToDate(value[1]))
  }
  else {
    emit('update:startTime', '')
    emit('update:endTime', '')
  }
}
</script>

<template>
  <n-date-picker :value="getValue" type="daterange" close-on-select clearable @update:value="handleUpdate" />
</template>
dateUtil.ts 复制代码
import { addDays, format, getTime, subDays } from 'date-fns'

const DATE_TIME_FORMAT = 'yyyy-MM-dd hh:mm:ss'
const DATE_FORMAT = 'yyyy-MM-dd'

export function formatToDateTime(date: number | Date, formatStr = DATE_TIME_FORMAT): string {
  return format(date, formatStr)
}

export function formatToDate(date: number | Date, formatStr = DATE_FORMAT): string {
  return format(date, formatStr)
}

export function dateAdd(date: number | Date, days: number, formatStr = DATE_FORMAT): string {
  return format(addDays(date, days), formatStr)
}

export function dateSub(date: number | Date, days: number, formatStr = DATE_FORMAT): string {
  return format(subDays(date, days), formatStr)
}

export function getTimeStamp(date?: number | Date): number {
  return getTime(date || new Date())
}

// 获取当天时间段
export function getToDayTime(): { startTime: string; endTime: string } {
  return {
    startTime: `${format(new Date(), DATE_FORMAT)} 00:00:00`,
    endTime: `${format(new Date(), DATE_FORMAT)} 23:59:59`,
  }
}

然后是数据表格

一、定义props的类型和button的类型

props.ts 复制代码
import type { ButtonProps } from 'naive-ui'
import type { TableBaseColumn } from 'naive-ui/es/data-table/src/interface'

interface ActionColumn extends ButtonProps {
  name: string
  onClick: (arg: any) => void
}

export interface BasicColumnItem extends TableBaseColumn {
  // 内容类型
  contentType?: 'text' | 'copy' | 'switch' | 'img' | 'actions'
  // 图片style
  imgStyle?: string
  actionColumn?: ActionColumn[]
  switchRequest?: (arg: any) => void
  switchId?: string
  displayKey?: string
  switchOptkey?: string[]
  type?: any
}

export type RequestFn = (opt?: any) => void

export type BasicColumns = BasicColumnItem[]
buttons.ts 复制代码
import type { ButtonProps } from 'naive-ui'

interface BasicButtonProps extends ButtonProps {
  name: any
  icon?: string
}

export type BasicButtonsProps = BasicButtonProps[]

二、接下来是hooks,我分为了三部分,表格分页的hook、表格刷新的hook、表格属性的hook

useColumns.tsx 复制代码
import type { ComputedRef } from 'vue'
import { computed, ref, unref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { NButton, NEllipsis, NImage, NSpace, NSwitch } from 'naive-ui'
import { useLoanStore } from 'loan-package/config'
import type { BasicColumns } from '../types'
import { copyToClipboard } from '~/src/utils/quick/copyPaste'

export function useColumns(basicColumns: ComputedRef<BasicColumns>) {
  const columnsRef = ref(unref(basicColumns))
  const { getApplet } = storeToRefs(useLoanStore())

  watch(() => unref(basicColumns), (columns) => {
    columnsRef.value = columns
  })

  const getColumns = computed(() => {
    return columnsRef.value.map((item) => {
      if (item.contentType === 'copy') {
        item.render = function (row) {
          return <div style='cursor: pointer;max-width:170px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;' onClick={() => {
            copyToClipboard(row[item.key] as string)
          }}>{row[item.key]}</div>
        }
      }

      if (item.contentType === 'switch') {
        item.render = function (row) {
          return <NSwitch checked-value="1" unchecked-value="0" value={row[item.key] as any} on-update:value={async (value: boolean) => {
            const srId = item.switchId || 'id'
            const displayKey = item.displayKey || item.key || 'display'
            const options = {}
            if (item.switchOptkey?.length !== 0) {
              if (row.applet_id) {
                item.switchOptkey?.forEach((key) => {
                  options[key] = row[key]
                })
              }
              else {
                row.applet_id = getApplet.value[0]
                item.switchOptkey?.forEach((key) => {
                  options[key] = row[key]
                })
              }
            }
            if (item.switchRequest) {
              const res = await item.switchRequest({
                [srId]: row[srId],
                [displayKey]: value,
                ...options,
              })
              if (!res.error)
                row[item.key] = value
            }
          } } ></NSwitch>
        }
      }

      if (item.contentType === 'img') {
        item.render = function (row) {
          const value = row[item.key]
          if (Array.isArray(value)) {
            return value.map((img) => {
              return <NImage style={item.imgStyle} src={img as string} ></NImage>
            })
          }

          return <NImage style={item.imgStyle} src={row[item.key] as string} ></NImage>
        }
      }

      if (item.contentType === 'actions') {
        item.render = function (row) {
          return <NSpace>{item.actionColumn?.map((action) => {
            const btnProps: any = { ...action }
            delete btnProps.onClick
            return <NButton {...btnProps} onClick={() => {
              action.onClick(row)
            }}>{action.name}</NButton>
          })}</NSpace>
        }
      }

      return {
        ...item,
        ellipsis: {
          tooltip: true,
	      },
	      ellipsisComponent: 'performant-ellipsis'
      }
    })
  })

  return {
    getColumns,
  }
}
usePagination.ts 复制代码
import { reactive } from 'vue'
import { DEFAULT_PAGE_SIZE, PAGE_SIZES } from '../const'
export function usePagination() {
  const paginationInfo = reactive({
    page: 1,
    pageSize: DEFAULT_PAGE_SIZE,
    itemCount: 0,
    showSizePicker: true,
    pageSizes: PAGE_SIZES,
    onChange: (page: number) => {
      paginationInfo.page = page
    },
    onUpdatePageSize: (pageSize: number) => {
      paginationInfo.pageSize = pageSize
      paginationInfo.page = 1
    },
    restPage: () => {
      paginationInfo.page = 1
    },
  })

  return { paginationInfo }
}
const.ts 复制代码
// 当前页的字段名
export const PAGE_FIELD = 'page'

// 每页数量字段名
export const SIZE_FIELD = 'step'

// 接口返回的数据字段名
export const LIST_FIELD = 'list'

// 接口返回总条数字段名
export const TOTAL_FIELD = 'total'

// 默认分页数量
export const DEFAULT_PAGE_SIZE = 10

// 可切换每页数量集合
export const PAGE_SIZES = [10, 20, 30, 100]
useDataSource.ts 复制代码
import type { Ref } from 'vue'
import type { PaginationProps } from 'naive-ui/es/pagination'
import { LIST_FIELD, PAGE_FIELD, SIZE_FIELD, TOTAL_FIELD } from '../const'
import type { RequestFn } from '../types'

interface UseDataSourceProps {
  paginationInfo: PaginationProps
  request: RequestFn
  resCallBack?: any
  loading: Ref<boolean>
}

export function useDataSource(tableData: Ref<any>, { paginationInfo, request, loading, resCallBack }: UseDataSourceProps) {
  let options = {}

  async function fetchDataSource(opt?: any) {
    if (opt) {
      (paginationInfo as any).restPage()
      options = opt
    }
    loading.value = true
    const { page = 1, pageSize = 10 } = paginationInfo
    const res: any = await request({ [PAGE_FIELD]: page, [SIZE_FIELD]: pageSize, ...options })
    if (resCallBack)
      resCallBack(res)
    paginationInfo.itemCount = res.data[TOTAL_FIELD]
    tableData.value = res.data[LIST_FIELD] || res.data
    loading.value = false
  }

  return {
    fetchDataSource,
  }
}

三、最后是BasicTable.vue文件

BasicTable.vue 复制代码
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import type { DataTableRowKey } from 'naive-ui'
import type { DataTableProps } from 'naive-ui/lib/data-table'
import type { BasicButtonsProps, BasicColumns, RequestFn } from './types'
import { useColumns, useDataSource, usePagination } from './hooks'

interface Props extends DataTableProps {
  rowKey?: (rowData: any) => (number | string)
  checkRowKey?: DataTableRowKey[]
  basicColumns: BasicColumns
  request: RequestFn
  resCallBack?: any
  buttons?: BasicButtonsProps
  scrollX?: number | string
}

const props = withDefaults(defineProps<Props>(), {
})

const emits = defineEmits(['update:checkRowKey', 'sort'])

const getProps = computed(() => {
  return {
    ...props,
  }
})

const loading = ref(false)
const tableData = ref()

const { paginationInfo } = usePagination()

const getBasicColumns = computed(() => props.basicColumns)

const { getColumns } = useColumns(getBasicColumns)

const { fetchDataSource } = useDataSource(tableData, { loading, paginationInfo, request: getProps.value.request, resCallBack: getProps.value.resCallBack })

function hanleUpdatePage() {
  fetchDataSource()
}

function hanleUpdatePageSize() {
  fetchDataSource()
}

function handleCheck(rowKeys: DataTableRowKey[]) {
  emits('update:checkRowKey', rowKeys)
}

function handleSorterChange(sorter) {
  emits('sort', sorter)
}

const getBindValue = computed(() => {
  return {
    scrollX: props.scrollX,
  }
})
// table的高度
const tableHeight = ref(document.documentElement.clientHeight * 0.7)

onMounted(() => {
  window.onresize = () => {
    return (() => {
      tableHeight.value = document.documentElement.clientHeight * 0.7
    })()
  }
})

defineExpose({
  fetchDataSource,
})
</script>

<template>
  <div>
    <div class="flex justify-between items-center">
      <div class="flex items-center">
        <template v-if="buttons">
          <n-space>
            <n-button v-for="(item, index) in buttons" :key="index" :type="item.type || 'primary'" :size="item.size" :disabled="item.disabled" @click="item.onClick">
              {{ item.name }}
              <template v-if="item.icon" #icon>
                <svg-icon :icon="item.icon" />
              </template>
            </n-button>
          </n-space>
        </template>
        <slot v-else name="left" />
        <slot name="number" />
      </div>
      <div slotclass="flex items-center">
        <slot name="right" />
      </div>
    </div>
    <div class="mt-10px">
      <n-data-table
        v-bind="getBindValue"
        :row-key="rowKey"
        :remote="true"
        :data="tableData"
        :loading="loading"
        :columns="getColumns"
        :pagination="paginationInfo"
        @update-page="hanleUpdatePage"
        @update-page-size="hanleUpdatePageSize"
        @update:checked-row-keys="handleCheck"
        @update:sorter="handleSorterChange"
      />
    </div>
  </div>
</template>

最就是示例环节啦

代码如下:我由于安装了插件,默认全局注册了组件,直接使用即可,各位小伙伴没有安装插件的,就用import引入

vue 复制代码
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { fetchEditLpr, fetchLprList } from 'bank-package/api'
import type { BasicButtonsProps, BasicColumns } from '@/components/business/Table/types'
import type { FormItemsProps } from '@/components/business/QueryForm/types'
import { componentType } from '@/components/business/QueryForm/types'

const basicTableRef = ref()

// 查询栏
const formItems: FormItemsProps = reactive([
  {
    label: '一年期LPR',
    field: 'one_year_lpr',
    component: componentType.input,
  },
  {
    label: '是否上架',
    field: 'display',
    component: componentType.select,
    componentProps: {
      options: [],
    },
  },
  {
    label: '更新时间',
    component: componentType.datePicker,
    datePickerProps: {
      startField: 'start_time',
      endField: 'end_time',
    },
  },
])

// 查询函数
function handleSearch(opt) {
  // 表格刷新
  basicTableRef.value.fetchDataSource(opt)
}

// 表格
const basicColumns: BasicColumns = reactive([
  { title: '发布时间', key: 'pubdate' },
  { title: '一年期LPR(%)', key: 'one_year_lpr' },
  { title: '五年期LPR(%)', key: 'five_year_lpr' },
  { title: '是否上架', key: 'display', contentType: 'switch', switchRequest: fetchEditLpr, switchId: 'lpr_id' },
  { title: '添加时间', key: 'add_time' },
  {
    title: '操作',
    key: 'action',
    contentType: 'actions',
    actionColumn: [{
      name: '编辑',
      onClick: (row) => {
        // 写你的表单逻辑
      },
    }],
  }])

const basicButtons: BasicButtonsProps = [{
  name: '新增',
  icon: 'material-symbols:post-add-rounded',
  onClick: () => {
    // 写你的表单逻辑
  },
}]

// 给下拉选择器的options赋值
function getDictList() {
  const data = [
    {
      label: '全部',
      value: '',
    },
    {
      label: '上架',
      value: '1',
    },
    {
      label: '下架',
      value: '0',
    },
  ]
  formItems[formItems.findIndex(item => item.field === 'display')].componentProps.options = data
}
getDictList()
</script>

<template>
  <div class="h-full">
    <n-card class="shadow-sm rounded-5px">
      <QueryForm :form-items="formItems" @search="handleSearch" />
    </n-card>
    <n-card class="shadow-sm rounded-5px mt-10px">
      <BasicTable ref="basicTableRef" :basic-columns="basicColumns" :request="fetchLprList" :buttons="basicButtons" />
    </n-card>
  </div>
</template>
相关推荐
拉不动的猪32 分钟前
前端常见数组分析
前端·javascript·面试
小吕学编程1 小时前
ES练习册
java·前端·elasticsearch
Asthenia04121 小时前
Netty编解码器详解与实战
前端
袁煦丞1 小时前
每天省2小时!这个网盘神器让我告别云存储混乱(附内网穿透神操作)
前端·程序员·远程工作
一个专注写代码的程序媛2 小时前
vue组件间通信
前端·javascript·vue.js
一笑code2 小时前
美团社招一面
前端·javascript·vue.js
懒懒是个程序员3 小时前
layui时间范围
前端·javascript·layui
NoneCoder3 小时前
HTML响应式网页设计与跨平台适配
前端·html
凯哥19703 小时前
在 Uni-app 做的后台中使用 Howler.js 实现强大的音频播放功能
前端
烛阴3 小时前
面试必考!一招教你区分JavaScript静态函数和普通函数,快收藏!
前端·javascript