什么?赶紧把这些重复性的东西封装起来吧,不就更多时间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>
相关推荐
baiduopenmap6 分钟前
百度世界2024精选公开课:基于地图智能体的导航出行AI应用创新实践
前端·人工智能·百度地图
loooseFish14 分钟前
小程序webview我爱死你了 小程序webview和H5通讯
前端
请叫我欧皇i26 分钟前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
533_29 分钟前
[vue] 深拷贝 lodash cloneDeep
前端·javascript·vue.js
guokanglun35 分钟前
空间数据存储格式GeoJSON
前端
zhang-zan1 小时前
nodejs操作selenium-webdriver
前端·javascript·selenium
猫爪笔记1 小时前
前端:HTML (学习笔记)【2】
前端·笔记·学习·html
brief of gali1 小时前
记录一个奇怪的前端布局现象
前端
Json_181790144802 小时前
电商拍立淘按图搜索API接口系列,文档说明参考
前端·数据库
风尚云网3 小时前
风尚云网前端学习:一个简易前端新手友好的HTML5页面布局与样式设计
前端·css·学习·html·html5·风尚云网