bzkf3 2 年之前
当前提交
17f34970dd

+ 78 - 0
.eslintrc-auto-import.json

@@ -0,0 +1,78 @@
+{
+  "globals": {
+    "$": true,
+    "$$": true,
+    "$computed": true,
+    "$customRef": true,
+    "$ref": true,
+    "$shallowRef": true,
+    "$toRef": true,
+    "EffectScope": true,
+    "REQUEST": true,
+    "client": true,
+    "computed": true,
+    "createApp": true,
+    "customRef": true,
+    "defineAsyncComponent": true,
+    "defineComponent": true,
+    "download": true,
+    "effectScope": true,
+    "getAvatarUrl": true,
+    "getCurrentInstance": true,
+    "getCurrentScope": true,
+    "getFullUrl": true,
+    "getPartUrl": true,
+    "h": true,
+    "inject": true,
+    "isProxy": true,
+    "isReactive": true,
+    "isReadonly": true,
+    "isRef": true,
+    "markRaw": true,
+    "nextTick": true,
+    "onActivated": true,
+    "onBeforeMount": true,
+    "onBeforeUnmount": true,
+    "onBeforeUpdate": true,
+    "onDeactivated": true,
+    "onErrorCaptured": true,
+    "onMounted": true,
+    "onRenderTracked": true,
+    "onRenderTriggered": true,
+    "onScopeDispose": true,
+    "onServerPrefetch": true,
+    "onUnmounted": true,
+    "onUpdated": true,
+    "paddingLeft": true,
+    "provide": true,
+    "reactive": true,
+    "readonly": true,
+    "ref": true,
+    "request": true,
+    "resolveComponent": true,
+    "resolveFileString": true,
+    "resolveSingleFileString": true,
+    "shallowReactive": true,
+    "shallowReadonly": true,
+    "shallowRef": true,
+    "timestamp2string": true,
+    "toRaw": true,
+    "toRef": true,
+    "toRefs": true,
+    "token": true,
+    "triggerRef": true,
+    "unref": true,
+    "useAttrs": true,
+    "useCssModule": true,
+    "useCssVars": true,
+    "useRoute": true,
+    "useRouter": true,
+    "useSlots": true,
+    "userId": true,
+    "userRoleId": true,
+    "watch": true,
+    "watchEffect": true,
+    "watchPostEffect": true,
+    "watchSyncEffect": true
+  }
+}

+ 27 - 0
.gitignore

@@ -0,0 +1,27 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+components.d.ts
+auto-imports.d.ts

+ 4 - 0
.husky/pre-applypatch

@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+. "$(dirname -- "$0")/_/husky.sh"
+
+pnpm lint:fix

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar"]
+}

+ 6 - 0
README.md

@@ -0,0 +1,6 @@
+# [windicss](https://windicss.org/)
+# [vant 4](https://vant-contrib.gitee.io/vant/v4/#/zh-CN/home)
+# [icon](https://icon-sets.iconify.design/)
+# [axios](https://www.axios-http.cn/docs/intro)
+# [vite](https://cn.vitejs.dev/)
+# [vue](https://cn.vuejs.org/)

+ 14 - 0
index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />
+    <title></title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script src="/config.js"></script>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 59 - 0
package.json

@@ -0,0 +1,59 @@
+{
+  "name": "ts-template",
+  "type": "module",
+  "version": "0.0.0",
+  "private": true,
+  "scripts": {
+    "dev": "vite --open",
+    "build": "vite build",
+    "preview": "vite preview",
+    "check": "vue-tsc --noEmit",
+    "lint": "eslint .",
+    "lint:fix": "eslint . --fix",
+    "prepare": "husky install"
+  },
+  "dependencies": {
+    "@vueuse/core": "^9.3.1",
+    "axios": "^0.27.2",
+    "vant": "4.0.0-beta.0",
+    "vue": "^3.2.41",
+    "vue-router": "^4.1.5"
+  },
+  "devDependencies": {
+    "@antfu/eslint-config": "^0.27.0",
+    "@iconify/json": "^2.1.123",
+    "@types/node": "^18.11.2",
+    "@vitejs/plugin-vue": "^3.1.2",
+    "eslint": "^8.25.0",
+    "husky": "^8.0.1",
+    "pnpm": "^7.13.5",
+    "typescript": "^4.8.4",
+    "unplugin-auto-import": "^0.10.3",
+    "unplugin-icons": "^0.14.12",
+    "unplugin-vue-components": "^0.22.8",
+    "vite": "^3.1.8",
+    "vite-plugin-windicss": "^1.8.8",
+    "vue-tsc": "^0.38.9",
+    "windicss": "^3.5.6"
+  },
+  "eslintConfig": {
+    "extends": [
+      "@antfu",
+      "./.eslintrc-auto-import.json"
+    ],
+    "overrides": [
+      {
+        "files": [
+          "*.js",
+          "*.ts",
+          "*.vue"
+        ],
+        "rules": {
+          "no-console": "off",
+          "semi": "off",
+          "comma-dangle": "off"
+        }
+      }
+    ]
+  }
+}

文件差异内容过多而无法显示
+ 3171 - 0
pnpm-lock.yaml


+ 31 - 0
public/config.js

@@ -0,0 +1,31 @@
+// 本地开发环境
+const local = {
+  web: 'http://jnjymf_web.bozedu.top/',
+  api: 'http://jnjymf_api.bozedu.top/',
+  oss: 'http://jnjymf_api.bozedu.top/'
+}
+
+// 测试服环境
+const development = {
+  web: 'http://jnjymf_web.bozedu.top/',
+  api: 'http://jnjymf_api.bozedu.top/',
+  oss: 'http://jnjymf_api.bozedu.top/'
+}
+
+// 正式服环境
+const production = {
+  web: 'http://60.188.226.44:8090/',
+  api: 'http://60.188.226.44:8090/',
+  oss: 'http://60.188.226.44:8090/'
+}
+
+function isWhich() {
+  if (window.location.href.startsWith(development.web)) {
+    return development
+  } else if (window.location.href.startsWith(production.web)) {
+    return production
+  } else {
+    return local
+  }
+}
+window.GLOBAL_CONFIG = isWhich()

+ 33 - 0
src/App.vue

@@ -0,0 +1,33 @@
+<script setup lang="ts">
+import type { ConfigProviderProps, ConfigProviderTheme } from 'vant'
+import { useDark, useToggle } from '@vueuse/core'
+
+const isDark = useDark()
+const toggleDark = useToggle(isDark)
+
+const theme = computed<ConfigProviderTheme>(() => isDark.value ? 'dark' : 'light')
+
+const themeVars: ConfigProviderProps['themeVars'] = {
+
+}
+</script>
+
+<template>
+  <van-config-provider
+    :theme="theme" :theme-vars="themeVars"
+    class="w-screen h-screen bg-light-900 text-dark-900 flex flex-col " :class="isDark && 'bg-dark-900 text-light-900'"
+  >
+    <!-- <van-nav-bar title="标题">
+      <template #right>
+        <button @click="toggleDark()" class="text-18px">
+          <i-carbon-moon v-show="isDark" class="text-light-900" />
+          <i-carbon-sun v-show="!isDark" class="text-dark-900" />
+        </button>
+      </template>
+    </van-nav-bar> -->
+    <div class="overflow-y-auto">
+      <router-view />
+    </div>
+  </van-config-provider>
+</template>
+

+ 59 - 0
src/components/FileUpload/index.vue

@@ -0,0 +1,59 @@
+<script setup>
+import { REQUEST } from '~/utils/request'
+import { resolveFileString } from '~/utils/helper'
+
+const props = defineProps({
+  // part: String,
+  // full: String,
+  modelValue: String,
+})
+
+// const emits = defineEmits(['update:part', 'update:full'])
+const emits = defineEmits(['update:modelValue'])
+const fileList = $ref(resolveFileString(props.modelValue).map(_ => ({ ..._, res: _ })))
+
+// if (props.part) {
+//   fileList = resolveFileString(props.part)
+// }
+// if (props.full) {
+//   fileList = resolveFileString(props.full)
+// }
+
+const handleAfterRead = (fileProxy) => {
+  fileProxy.status = 'uploading'
+  fileProxy.message = '上传中...'
+  const { file } = fileProxy
+  REQUEST.upload({
+    url: '/upload/main/file',
+    data: { filedata: file },
+  }).then((res) => {
+    console.log('res :>> ', res)
+    console.log('fileList :>> ', fileList)
+    if (res.code === '1') {
+      // fileList.value.push(res.data)
+      fileProxy.url = `${window.GLOBAL_CONFIG.oss}/${res.data.url}`
+      fileProxy.res = {
+        name: res.data.file_name,
+        url: fileProxy.url,
+        origin: res.data.url,
+      }
+      fileProxy.status = 'done'
+      fileProxy.message = ''
+
+      // emits('update:part', fileList.map((item) => item.res.name + ',' + item.res.url).join(';'))
+      // emits('update:part', fileList.map((item) => item.res.name + ',' + item.res.url).join(';'))
+      emits('update:modelValue', fileList.map(item => `${item.res.name},${item.res.origin}`).join(';'))
+    }
+    else {
+      fileProxy.status = 'failed'
+      fileProxy.message = '上传失败'
+    }
+  }).catch((err) => {
+    console.error(err)
+  })
+}
+</script>
+
+<template>
+  <van-uploader v-model="fileList" :after-read="handleAfterRead" />
+</template>

+ 78 - 0
src/components/RemoteList/index.vue

@@ -0,0 +1,78 @@
+<script setup>
+const props = defineProps({
+  url: {
+    type: String,
+    required: true,
+  },
+  d: {
+    type: Object,
+    required: false,
+  },
+})
+
+let page = 1
+
+let loading = $ref(false)
+let error = $ref(false)
+let finished = $ref(false)
+let refreshing = $ref(false)
+
+let list = $ref([])
+
+// request({
+//   url: props.url,
+//   data: {
+//     page,
+//     ...props.d
+//   },
+// })
+
+function onLoad() {
+  console.log('onLoad')
+  loading = true
+  request({
+    url: props.url,
+    data: {
+      page,
+      ...props.d,
+    },
+  }).then((res) => {
+    if (res.code === '1') {
+      if (refreshing) {
+        list = []
+        refreshing = false
+      }
+      list = list.concat(res.data.page_data)
+      loading = false
+      page++
+      if (res.data.page_now === res.data.total_page)
+        finished = true
+    }
+    else {
+      error = true
+    }
+  }).catch((err) => {
+    console.log(err)
+  })
+}
+
+function onRefresh() {
+  console.log('onRefresh')
+  finished = false
+  page = 1
+  onLoad()
+}
+</script>
+
+<template>
+  <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
+    <van-list
+      v-model:loading="loading" v-model:error="error" error-text="请求失败,点击重新加载" :finished="finished"
+      finished-text="没有更多了" @load="onLoad" @click.stop
+    >
+      <template v-for="item in list">
+        <slot :row="item" />
+      </template>
+    </van-list>
+  </van-pull-refresh>
+</template>

+ 19 - 0
src/main.ts

@@ -0,0 +1,19 @@
+import { createApp } from 'vue'
+import 'virtual:windi.css'
+
+import App from './App.vue'
+import router from './router/index'
+
+// Toast
+import 'vant/es/toast/style'
+// Dialog
+import 'vant/es/dialog/style'
+// Notify
+import 'vant/es/notify/style'
+// ImagePreview
+import 'vant/es/image-preview/style'
+
+const app = createApp(App)
+app.use(router)
+
+app.mount('#app')

+ 14 - 0
src/pages/index.vue

@@ -0,0 +1,14 @@
+<script setup lang="ts">
+let popupShow = $ref(false)
+const handleRightClick = () => {
+  console.log('right click')
+  popupShow = true
+}
+</script>
+
+<template>
+  <van-nav-bar title="标题" right-text="筛选" @click-right="handleRightClick" />
+  <van-popup v-model:show="popupShow" position="right" class="w-75vw h-screen">
+    <h1>侧边栏</h1>
+  </van-popup>
+</template>

+ 20 - 0
src/router/index.ts

@@ -0,0 +1,20 @@
+import type { RouteRecordRaw } from 'vue-router'
+import { createRouter, createWebHashHistory } from 'vue-router'
+
+const modules: Record<string, { default: RouteRecordRaw }> = import.meta.glob('./routes/*.ts', { eager: true })
+const routes = Object.values(modules).map(module => module.default)
+
+console.debug('routes :>> ', routes)
+
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes,
+})
+
+export default router
+
+router.beforeEach((to, from) => {
+  console.groupCollapsed(`%c${from.name?.toString()} => ${to.name?.toString()}`, 'color:#0ff')
+  console.log(`%c${from.meta.title} => ${to.meta.title}`, 'color:#0cc')
+  console.groupEnd()
+})

+ 6 - 0
src/router/routes/example.ts

@@ -0,0 +1,6 @@
+import type { RouteRecordRaw } from 'vue-router'
+
+export default <RouteRecordRaw>{
+  path: '',
+  component: () => import('~/pages/index.vue'),
+}

+ 4 - 0
src/store/index.ts

@@ -0,0 +1,4 @@
+export const token = '2d0ee1JYallYIYnP933n6e1SAChayy9g9HiFlJr2Uqqex_b8iQSgqf9ai1uJYjiR2xUbl9FwcAL2mi6AxNzoTGENzp7H5Ib0'
+export const userId = '2'
+export const client = ''
+export const userRoleId = ''

+ 30 - 0
src/utils/helper.ts

@@ -0,0 +1,30 @@
+export function resolveFileString(str: string) {
+  return str ? str.split(';').map(s => resolveSingleFileString(s)) : []
+}
+
+export function resolveSingleFileString(str: string, sign = '|') {
+  const [url, name] = str.split(sign)
+  return {
+    name,
+    url: url.startsWith('http') ? url : `${window.GLOBAL_CONFIG.oss}/${url}`,
+    origin: url,
+  }
+}
+
+// 获取完整文件地址
+export function getFullUrl(url: string) {
+  if (!url)
+    return ''
+  return url.startsWith('http') ? url : `${window.GLOBAL_CONFIG.oss}/${url}`
+}
+
+// 获取不完整文件地址
+export function getPartUrl(url: string) {
+  if (!url)
+    return ''
+  return url.startsWith('http') ? url.startsWith(window.GLOBAL_CONFIG.oss) ? url.substring(window.GLOBAL_CONFIG.oss.length) : Error('oss 地址错误') : url
+}
+
+export function getAvatarUrl(id: string) {
+  return `${window.GLOBAL_CONFIG.oss}/user/main/user_avatar?user_id=${id}`
+}

+ 90 - 0
src/utils/request.ts

@@ -0,0 +1,90 @@
+import axios from 'axios'
+import type { AxiosRequestConfig } from 'axios'
+import { showFailToast } from 'vant'
+import { token } from '~/store/index'
+
+const _request = axios.create({
+  baseURL: window.GLOBAL_CONFIG.api,
+  timeout: 3 * 1000,
+  headers: {
+    'Content-Type': 'application/x-www-form-urlencoded',
+  },
+  method: 'post',
+})
+
+_request.interceptors.request.use(
+  async (config) => {
+    if (config.method?.toLocaleLowerCase() === 'get') {
+      config.params = Object.assign({ token }, config.params)
+    }
+    else {
+      config.data = Object.assign(
+        {
+          token,
+          client: 'web',
+          api: 'json',
+          issubmit: (config.url?.endsWith('add') || config.url?.endsWith('edit')) ? '1' : undefined,
+        },
+        config.data)
+    }
+    return config
+  },
+  (error) => {
+    console.error('request error: ', error)
+    return Promise.reject(error)
+  },
+)
+
+// response interceptor
+_request.interceptors.response.use(
+  (response) => {
+    response.data.code = response.data?.code?.toString()
+    response.data.msg = response.data.msg.replaceAll(/<.*?>/g, ' ')
+    const { code, msg } = response.data
+    if (code !== '1')
+      showFailToast(msg)
+
+    return response.data
+  },
+  (error) => {
+    console.error(`response error: ${error}`)
+    return Promise.reject(error)
+  },
+)
+
+export default _request
+
+const obj2form = (data: { [key: string]: any }) => {
+  const formData = new FormData()
+  Object.keys(data).forEach(key => formData.append(key, data[key]))
+  return formData
+}
+
+export const REQUEST = {
+  empty: axios,
+  default: _request,
+  import: (c: Partial<AxiosRequestConfig>) => _request({
+    timeout: 10 * 60 * 1000,
+    transformRequest: [obj2form],
+    ...c,
+  }),
+  upload: (c: Partial<AxiosRequestConfig>) => _request({
+    timeout: 3 * 60 * 1000,
+    transformRequest: [obj2form],
+    ...c,
+  }),
+  download: (c: Partial<AxiosRequestConfig>) => _request({
+    timeout: 1 * 60 * 1000,
+    method: 'get',
+    params: { token, limit: 10000, page: 1, api: 'xls', ...c },
+  }),
+}
+
+export function download(url: string, data?: object | null) {
+  const params = Object.assign({ token, limit: 10000, page: 1, api: 'xls' }, data)
+  const paramsStr = Object.entries(params).map(([k, v]) => `${k}=${v}`).join('&')
+  const el = document.createElement('a')
+  const href = `${window.GLOBAL_CONFIG.api}${url}?${paramsStr}`
+  el.setAttribute('href', href)
+  el.click()
+}

+ 11 - 0
src/utils/string.ts

@@ -0,0 +1,11 @@
+export function paddingLeft(str: string, length = 2, pad = '0'): string {
+  str = str.toString()
+  if (str.length >= length)
+    return str
+
+  return paddingLeft(pad + str, length, pad)
+}
+
+export function timestamp2string(timestamp: string | number | Date) {
+  return (new Date(timestamp)).toLocaleString()
+}

+ 15 - 0
src/vite-env.d.ts

@@ -0,0 +1,15 @@
+/// <reference types="vite/client" />
+
+declare module '*.vue' {
+  import type { DefineComponent } from 'vue'
+  const component: DefineComponent<{}, {}, any>
+  export default component
+}
+
+
+interface Window {
+  GLOBAL_CONFIG: {
+    api: string;
+    oss: string;
+  },
+}

+ 42 - 0
tsconfig.json

@@ -0,0 +1,42 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "moduleResolution": "Node",
+    "strict": true,
+    "jsx": "preserve",
+    "sourceMap": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "esModuleInterop": true,
+    "lib": [
+      "ESNext",
+      "DOM"
+    ],
+    "skipLibCheck": true,
+    "allowJs": true,
+    "baseUrl": "./",
+    "paths": {
+      "@/*": [
+        "src/*"
+      ],
+      "~/*": [
+        "src/*"
+      ]
+    },
+    "types": []
+  },
+  "include": [
+    "src/**/*.ts",
+    "src/**/*.d.ts",
+    "src/**/*.tsx",
+    "src/**/*.vue",
+    "./auto-imports.d.ts"
+  ],
+  "references": [
+    {
+      "path": "./tsconfig.node.json"
+    }
+  ]
+}

+ 9 - 0
tsconfig.node.json

@@ -0,0 +1,9 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "module": "ESNext",
+    "moduleResolution": "Node",
+    "allowSyntheticDefaultImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 81 - 0
vite.config.ts

@@ -0,0 +1,81 @@
+import path from 'path'
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import WindiCss from 'vite-plugin-windicss'
+import AutoImport from 'unplugin-auto-import/vite'
+import Components from 'unplugin-vue-components/vite'
+import { VantResolver } from 'unplugin-vue-components/resolvers'
+import Icons from 'unplugin-icons/vite'
+import IconsResolver from 'unplugin-icons/resolver'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  resolve: {
+    alias: {
+      '@': path.resolve(__dirname, 'src'),
+      '~': path.resolve(__dirname, 'src'),
+      // '~components': path.resolve(__dirname, 'src/components'),
+      // '~pages': path.resolve(__dirname, 'src/pages'),
+      // '~utils': path.resolve(__dirname, 'src/utils'),
+      // '~assets': path.resolve(__dirname, 'src/assets'),
+      // '~styles': path.resolve(__dirname, 'src/styles'),
+      // '~lib': path.resolve(__dirname, 'src/lib'),
+      // '~plugins': path.resolve(__dirname, 'src/plugins'),
+      // '~router': path.resolve(__dirname, 'src/router'),
+      // '~store': path.resolve(__dirname, 'src/store'),
+      // '~config': path.resolve(__dirname, 'src/config'),
+      // '~api': path.resolve(__dirname, 'src/api'),
+      // '~constants': path.resolve(__dirname, 'src/constants'),
+      // '~locales': path.resolve(__dirname, 'src/locales'),
+    },
+  },
+  plugins: [
+    vue({
+      reactivityTransform: true,
+    }),
+    WindiCss(),
+    AutoImport({
+      // dts: 'src/auto-imports.d.ts',
+      imports: ['vue', 'vue/macros', 'vue-router'],
+      dirs: [
+        'src/composables',
+        'src/store',
+        'src/utils',
+      ],
+      resolvers: [VantResolver()],
+      vueTemplate: true,
+      eslintrc: {
+        enabled: true,
+        // enabled: false, // Default `false`
+        // filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
+        // globalsPropValue: true, // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
+      },
+    }),
+    Components({
+      // dts: 'src/components.d.ts',
+      dirs: ['src/components/'],
+      // allow auto load markdown components under `./src/components/`
+      extensions: ['vue', 'md'],
+      // allow auto import and register components used in markdown
+      include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
+      resolvers: [VantResolver(), IconsResolver()],
+    }),
+    Icons({
+      compiler: 'vue3',
+      autoInstall: true,
+    }),
+  ],
+  server: {
+    host: true,
+  },
+  build: {
+    rollupOptions: {
+      output: {
+        manualChunks: {
+          axios: ['axios'],
+          vant: ['vant'],
+        },
+      },
+    },
+  },
+})

+ 17 - 0
windi.config.ts

@@ -0,0 +1,17 @@
+import { defineConfig } from 'windicss/helpers'
+
+export default defineConfig({
+  darkMode: 'media',
+  shortcuts: {
+    card: 'rounded-xl bg-light-50 p-3 relative box-border',
+    divider: 'w-full h-0 border border-solid border-gray-100 my-2',
+    divider_y: 'w-0 h-full border border-solid border-gray-100 mx-2',
+    icon: 'w-24px h-24px fill-blue-600 bg-light-100 rounded-2px cursor-pointer  mx-4px box-border',
+    icon_reserve:
+      'w-24px h-24px bg-blue-600 fill-light-100 rounded-2px cursor-pointer mx-4px p-2px box-border',
+    flex_center: 'flex justify-center items-center',
+    flex_start: 'flex justify-start items-center',
+    area: 'flex bg-gray-100 text-gray-600 my-2 p-4 text-sm h-300px overflow-auto',
+    pre: 'before:content-["|"] before:w-4px before:h-full before:inline-block before:text-transparent before:mr-6px before:bg-blue-400 text-gray-700',
+  },
+})