feat: 首次提交

master
绝弹 2023-07-08 14:18:41 +08:00
commit 107726be25
126 changed files with 26572 additions and 0 deletions

43
.env Normal file
View File

@ -0,0 +1,43 @@
# ============================================================
# 项目配置文件
# ============================================================
# 网站标题
VITE_APP_TITLE = 应用管理系统
# 网站副标题
VITE_APP_SUBTITLE = 快速开发web应用的模板工具
# Axios基本URL
VITE_APP_API_BASE_URL = /api
# ============================================================
# 开发设置
# ============================================================
# API接口地址(开发环境)
VITE_API_BASE_URL = http://127.0.0.1:3030
# API代理地址(开发环境)
VITE_API_PROXY_URL = /api
# API文档地址(开发环境) 备注需为openapi规范的json文件
# VITE_API_DOCS_URL = http://127.0.0.1:3030/openapi-json
VITE_API_DOCS_URL = https://petstore.swagger.io/v2/swagger.json
# 端口号(开发环境)
VITE_DEV_PORT = 3020
# 主机地址(开发环境)
VITE_DEV_HOST = 0.0.0.0
# ============================================================
# 构建设置
# ============================================================
# 构建时加载的文件后缀. 例如设置为todo则会首先尝试加载index.todo.vue文件不存在时再加载index.vue文件
VITE_BUILD_EXTENSION = todo

33
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,33 @@
# 工作流名称,可自定义
name: 自动部署
# 事件监听,决定什么时候触发该工作流内的任务
on:
# 在master分支推动到github时触发
push:
branches: [ master ]
# 任务集合,可包含多个任务
jobs:
# 任务名称
build:
# 运行的操作系统
runs-on: ubuntu-latest
# 步骤集合,可包含多个步骤
steps:
# 单个步骤没有名称直接使用一个action
- uses: actions/checkout@v2
# 单个步骤,带有名称,带有参数
- name: build and deploy
run: |
npm install
npm run build
cd dist
git config --global user.name "juetan"
git config --global user.email "810335188@qq.com"
git init
git add -A
git commit -m "Build through github action"
git push -f "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" master:gh-pages

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# 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
!.vscode/*.code-snippets
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
registry=https://registry.npmmirror.com/
public-hoist-pattern[]=@vue/runtime-core

98
.vscode/components.code-snippets vendored Normal file
View File

@ -0,0 +1,98 @@
{
"New useform": {
"prefix": "guseform",
"description": "useForm code",
"body": [
"const ${1:form} = useForm({",
" model: {},",
" items: [",
" {",
" field: '${2:name}',",
" label: '${3:label}',",
" type: '${4:input}',",
" },",
" ${0}",
" ],",
" submit: async ({ model }) => {",
" console.log(model);",
" },",
"});"
]
},
"New <Table /> Component": {
"scope": "vue,vue-html,html",
"prefix": "gtable",
"description": "Table Component Code",
"body": ["<Table ref=\"${1:table}\" v-bind=\"${2:table}\"></Table>"]
},
"New Table Column": {
"prefix": "gcolumn",
"description": "Table Column Code",
"body": ["{", " title: \"${1}\",", " dataIndex: \"${2}\",", " ${3}", "},", "${0}"]
},
"New Item": {
"prefix": "gitem",
"description": "创建新表单元素",
"body": ["{", " field: \"${1}\",", " label: \"${2}\",", " type: \"${3:input}\",", " ${4}", "},", "${0}"]
},
"New Crud Page": {
"prefix": "gusetable",
"isFileTemplate": true,
"body": [
"<template>",
" <BreadPage>",
" <Table ref=\"tableRef\" v-bind=\"table\"></Table>",
" </BreadPage>",
"</template>",
"",
"<script setup lang=\"tsx\">",
"const table = useTable({",
" data: async (model, paging) => {",
" ${1}",
" },",
" columns: [",
" {",
" type: 'index'",
" },",
" ${2}",
" ],",
" common: {",
" model: {",
" ${3}",
" },",
" items: [",
" ${4}",
" ],",
" modalProps: {",
" width: 772,",
" maskClosable: false,",
" },",
" formProps: {",
" layout: \"vertical\",",
" class: \"!grid grid-cols-2 gap-x-3\",",
" },",
" },",
" search: {",
" items: [",
" ${5}",
" ],",
" },",
" create: {",
" title: \"新增${6}\",",
" submit: ({ model }) => {",
" ${7}",
" },",
" },",
" modify: {",
" title: \"修改\",",
" submit: ({ model }) => {",
" ${8}",
" },",
" },",
"});",
"</script>",
"",
"<style lang=\"less\" scoped></style>"
]
}
}

11
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"xabikos.JavaScriptSnippets",
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"sdras.vue-vscode-snippets",
"antfu.iconify"
]
}

66
.vscode/template.code-snippets vendored Normal file
View File

@ -0,0 +1,66 @@
{
"New Route": {
"description": "生成一个Setup风格的SFC页面(带路由)",
"prefix": "groute",
"isFileTemplate": true,
"body": [
"<template>",
" <div>",
" Page",
" </div>",
"</template>",
"",
"<script setup lang=\"ts\">",
" ${0}",
"</script>",
"",
"<style lang=\"less\" scoped>",
" ",
"</style>",
"",
"<route lang=\"json\">",
"{",
" \"meta\": {",
" \"sort\": ${1:20010},",
" \"title\": \"${2:测试页面}\",",
" \"icon\": \"icon-park-outline-${3:home}\"",
" }",
"}",
"</route>"
]
},
"New Page": {
"description": "生成一个Setup风格的SFC页面",
"prefix": "gpage",
"isFileTemplate": true,
"body": [
"<template>",
" <div>",
" Page",
" </div>",
"</template>",
"",
"<script setup lang=\"ts\">",
" ${0}",
"</script>",
"",
"<style lang=\"less\" scoped>",
" ",
"</style>"
]
},
"New Route Meta": {
"prefix": "groutemeta",
"body": [
"<route lang=\"json\">",
"{",
" \"meta\": {",
" \"sort\": ${1:20010},",
" \"title\": \"${2:测试页面}\",",
" \"icon\": \"icon-park-outline-${3:home}\"",
" }",
"}",
"</route>"
]
}
}

264
README.md Normal file
View File

@ -0,0 +1,264 @@
## 介绍
基于vue3 + vite4 + typescript的B端管理系统起始模板提供自动导入/路由、轻量CRUD表格组件和API接口自动生成等功能。
## 功能
- 一个文件,自动生成路由/菜单/面包屑
- Typescript支持内置和扩展众多类型定义文档在手可触
- 根据openapi自动生成数据类型、请求函数
- 轻量化的封装表单、CRUD表格开箱即用
- 内置VITE插件输出版本/打包信息,支持根据不同后缀打包
- 轻量的字典常量定义助手函数
- 常用API/组件自动导入,同时带类型提示
- 图标/样式一个类名搞定
- 遵循Conventional Changelog规范 自动生成版本记录文档
- 内置常用VsCode代码片段和推荐扩展提升开发效率
## 快速开始
1. 确保本地安装有如下软件,推荐最新版本。
```
git
node
pnpm
```
备注pnpm在NodeJS v16+版本可通过 corepack enable 命令开启,低版本请通过 npm install pnpm 命令安装
2. 拉取模板
```
npx degit https://github.com/juetan/apptify-admin
```
3. 安装依赖
```
pnpm install
```
4. 启动项目默认端口3020。
```
pnpm dev
```
## 开发文档
本仓库仅是一个起始模板,具体项目请根据需求改造。
### 路由菜单
基于 [vite-plugin-pages](https://github.com/hannoeru/vite-plugin-pages) 插件。本项目使用`src/pages`作为路由目录最终生成的路由仅有2级主要是出于`<keepalive>`缓存的需要,其中:
| 说明 |
| --- |
| `src/pages`目录下以`_`开头的文件名/目录名为一级路由,如登陆页面。
| `src/pages`其他子目录或`.vue`文件为二级路由,如应用首页。
左侧菜单数据将根据上面的二级路由自动生成如需生成层级只需在对应目录下的index.vue文件中定义如下路由配置
```
<route lang="json">
{
"parentMeta": {
// 具体属性查阅 src/types/vue-router.d.ts
}
}
</route>
```
### 文件后缀
`scripts/vite/plugin.ts` 文件中内置了一个VITE插件主要用于输出编译信息以及根据不同文件后缀进行打包。在项目根目录下的`.env`配置文件中,可指定以下属性:
```
VITE_BUILD_TYPE = my
```
配置后,构建时将首先尝试加载`index.my.vue`文件,不存在时再加载`index.vue`。默认开发环境下为`dev`, 其他环境为`prod`,这在本地临时开发或根据微差异化打包时非常有用。
### 图标样式
基于 [UnoCSS]() 插件可使用类似TailwindCSS的原子样式快速开发同时默认安装`icon-park-outline`图标库只需引用类名即可得到SVG图标。这在路由菜单等需要动态渲染时非常有用同时所有样式类和图标类都是按需打包的示例
```html
<i class="text-sm icon-park-outline-home" />
```
### 接口请求
基于 [typescript-swagger-api]() 库根据openapi结构自动生成请求接口和数据类型。生成的内容位于`src/api/service`目录下,默认包含数据类型定义、请求客户端(默认axios)和请求基类三大块内容。如需自定义生成模板,可查阅`scripts/openapi`目录下的模板内容。
生成的接口类型包含完整的入参和出参类型提示。
此外,在`src/api/instance/useRequest.ts`中还定义了一个`useRequest`函数,可对请求状态进行管理,示例:
```typescript
const userState = useRequest(api.user.getUsers)
// 返回的数据
userState.data
// 是否请求中
userState.loading
// 请求异常
userState.error
```
### 字典枚举
字典枚举可能包含多种需求,例如根据值获取标签、生成下拉框选项、根据值获取其他内容(如颜色)等。在 `src/config/defineConstants.ts` 文件中,定义了一个简易的字典枚举值助手函数,解决以上问题,且提供类型提示,示例:
```typescript
const media = defineConstants([
{
label: '视频',
value: 1,
enumKey: 'VIDEO',
color: 'red'
},
{
label: '图片',
value: 2,
enumeKey: 'IMAGE',
color: 'blue'
}
])
// enumKey
media.IMAGE // 2
// 根据value值获取其他属性值, 第2个参数可选(默认label)
media.format(1, 'color') // red
// 获取某个属性值组成的数组
media.each('value') // [1, 2]
// 根据value值过滤数组omit同理
media.pick(1) // [{ label: '视频', value: 1, enumKey: 'VIDEO' }]
// 原始传入defineContants的数组可自定义操作
media.items // 可直接作为select的选项
```
### 增删改查
在`src/components`目录中,封装了`form`组件和`table`组件主要用于普通CRUD的实现这里演示基本的使用方法。
```html
<template>
<Table ref="tableRef" v-bind="table" />
</template>
<script>
import { Table, useTable } from '@/components'
const table = useTable({
// 数据源配置,可以是数组或返回对象的异步函数
data: async(search, paging) => {
return { data, total }
},
// 表格列配置
columns: [
{
title: '用户名',
dataIndex: 'name'
}
],
// 分页配置
pagination: {
showTotal: true,
},
// 查询配置类型为useForm的入参
search: {
items: [
{
field: 'username',
label: '用户名',
type: 'input'
},
]
},
// 新增表单弹窗的配置类型为useFormModal的入参
create: {
title: '新增用户',
items: [
{
field: 'username',
label: '用户名',
type: 'input'
},
],
submit: async({ model }) => {
return api.xx(model)
}
},
// 修改表单弹窗的配置类型为useFormModal的入参
modify: {
title: '修改用户',
items: [
{
field: 'username',
label: '用户名',
type: 'input'
},
],
submit: async({ model }) => {
return api.xx(model)
}
}
})
</script>
```
### 自动导入
基于 [unplugin-auto-import]() 和 [unplugin-vue-components]() 插件主要用于常用API的自动导入例如vue和vue-router等以及常用组件的导入例如arco-design等。示例
```html
<template>
<!-- ArcoDesign组件无需导入即可使用且有类型提示 -->
<a-table :data="data"></a-table>
</template>
<script lang="ts">
// vue的API无需导入即可使用且有类型提示
const data = ref([])
</script>
```
如需自定义其他API的自动导入请查阅`vite.config.ts`文件中的配置。
### 代码片段
基于 VsCode 的 snippet 功能,在`.vscode/components.code-snippets`文件中定义了常用组件和API模板的快捷生成。所有代码片段均以`g`(generate)开头对于快速CRUD或新建页面等非常有用示例
```ts
// 输入以下内容并回车
groutemeta
// 将生成如下内容
<route lang="json">
{
"meta": {
"order": 10020,
"titble": "测试页面",
"icon": "icon-park-outline-home"
}
}
</route>
```
### 版本记录
基于 [release-it]() 库,运行`pnpm release`命令时,将根据你的选择,执行以下操作:
- 提升package.json的version版本(遵循semver语义)
- 给git打版本标签例如 v1.0.1(同样遵循semver语义)
- 根据符合[Conventional Changelog]规范的git提交信息在`CHANGELOG.md`文件中生成版本记录。
- 自动推送到npm/github/gitlab中
如需自定义`CHANGELOG.md`的生成模板或进行其他自定义配置,请查阅`/scripts/release`目录下的内容。
### 状态管理
基于 [pinia]() 库,具体使用查阅官方文档即可。
### 工具类库
日常开发难免用到各种工具库但直接使用的话难免有不符合项目需求的时候。例如dayjs的本地化语言、相对时间插件等都要配置散落在项目的各个角落并不是个好习惯。
建议在`src/plugins`进行二次封装后,再在项目中使用,不仅统一调用还便于管理,方便后续的优化升级。
本项目内置开发中常用的类库,如下:
| 库 | 说明
| --- | ---
| [lodash-es]() | 常用函数集,例如深克隆、防抖、节流等
| [axios]() | HTTP请求库
| [dayjs]() | 日期时间处理库
| [numeral]() | 数值处理库,如数值转时间、数值转文件大小等
| [nprogress]() | 进度条
| [@vueuse/core]() | 基于Vue Composition API的工具库响应式存储数据、监听事件等
## 最后
如果你在使用过程中遇到问题请在issue中提问。

View File

@ -0,0 +1,13 @@
{
"hash": "3930064b",
"browserHash": "71254bdf",
"optimized": {
"vue": {
"src": "../../../../../../../node_modules/.pnpm/vue@3.3.4/node_modules/vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "9e6f557b",
"needsInterop": false
}
},
"chunks": {}
}

View File

@ -0,0 +1,3 @@
{
"type": "module"
}

10795
docs/.vitepress/cache/deps/vue.js vendored Normal file

File diff suppressed because it is too large Load Diff

7
docs/.vitepress/cache/deps/vue.js.map vendored Normal file

File diff suppressed because one or more lines are too long

152
docs/.vitepress/config.ts Normal file
View File

@ -0,0 +1,152 @@
import { defineConfig } from "vitepress";
/**
*
* @see https://vitepress.dev/reference/site-config
*/
export default defineConfig({
lang: "zh-CN",
title: "绝弹博客",
description: "一位前端开发者的博客",
/**
*
* @see https://vitepress.dev/reference/default-theme-config
*/
themeConfig: {
logo: "/juetan.jpg",
search: {
provider: "local",
options: {
translations: {
button: {
buttonText: "搜索",
buttonAriaLabel: "搜索",
},
modal: {
noResultsText: "没有找到结果",
resetButtonTitle: "重置搜索",
footer: {
selectText: "选择",
navigateText: "移动",
closeText: "关闭",
},
},
},
},
},
outline: {
label: "本篇目录",
},
nav: [
{
text: "首页",
link: "/",
},
{
text: "前端开发",
link: "/front-end/",
},
{
text: "后端开发",
items: [
{
text: "测试1",
link: "/test1",
},
{
text: "测试2",
link: "/test2",
},
],
},
{
text: '日常记录',
link: '/'
},
{
text: "开发工具",
link: "/dev-tools",
},
],
sidebar: {
"/front-end/": [
{
text: "基础知识",
items: [
{
text: "HTML中的标签有多少个?",
link: "/front-end/a",
},
{
text: "Runtime API示例",
link: "/front-end/b",
},
],
},
{
text: '工具类库',
items: [
{
text: 'Lodash在日常开发中有用的函数'
}
]
},
{
text: 'vue',
items: [
{
text: '如何将.vue文件编译成js文件?'
}
]
},
{
text: '浏览器',
items: [
{
text: '浏览器Console面板中有用的调试技巧',
},
{
text: '如何利用EJS模板引擎辅助生成代码?',
link: '/front-end/ejs-generate-code'
},
{
text: '项目中的字典常量应该如何维护?'
},
{
text: '从new xx()和new xx的区别聊聊JS中操作符的优先级问题',
link: '/front-end/js-operator-priority'
},
{
text: 'TailwindCSS中一些有意思的用法和实现'
},
{
text: '函数柯里化是什么如何实现它?'
},
{
text: '写一个VITE插件: 根据配置加载不同后缀的文件'
}
]
}
],
},
socialLinks: [
{
icon: "github",
link: "https://github.com/juetan",
},
],
docFooter: {
prev: "上一篇",
next: "下一篇",
},
footer: {
message: "© 2023 JueTan",
copyright: "绝弹博客 版权所有",
},
},
markdown: {
theme: "github-dark-dimmed",
lineNumbers: true,
},
});

View File

@ -0,0 +1,20 @@
import { h } from 'vue'
import Theme from 'vitepress/theme'
import './style.css'
import { EnhanceAppContext } from 'vitepress'
/**
*
* @see https://vitepress.dev/guide/custom-theme
*/
export default {
...Theme,
Layout: () => {
return h(Theme.Layout, null, {
// https://vitepress.dev/guide/extending-default-theme#layout-slots
})
},
enhanceApp({ app, router, siteData }: EnhanceAppContext) {
// ...
}
}

View File

@ -0,0 +1,251 @@
/**
* Customize default theme styling by overriding CSS variables:
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
*/
/**
* Colors
* -------------------------------------------------------------------------- */
:root {
--vp-c-brand: #09f;
--vp-c-brand-light: #0099ffe0;
--vp-c-brand-lighter: #0099ffa0;
--vp-c-brand-lightest: #0099ffc0;
--vp-c-brand-dark: #535bf2;
--vp-c-brand-darker: #454ce1;
--vp-c-brand-dimm: rgba(100, 108, 255, 0.08);
--vp-sidebar-bg-color: transparent;
--vp-nav-height: 56px;
}
/* :root {
--vp-c-brand: #646cff;
--vp-c-brand-light: #747bff;
--vp-c-brand-lighter: #9499ff;
--vp-c-brand-lightest: #bcc0ff;
--vp-c-brand-dark: #535bf2;
--vp-c-brand-darker: #454ce1;
--vp-c-brand-dimm: rgba(100, 108, 255, 0.08);
--vp-sidebar-bg-color: transparent;
} */
/**
* Component: Button
* -------------------------------------------------------------------------- */
:root {
--vp-button-brand-border: var(--vp-c-brand-light);
--vp-button-brand-text: var(--vp-c-white);
--vp-button-brand-bg: var(--vp-c-brand);
--vp-button-brand-hover-border: var(--vp-c-brand-light);
--vp-button-brand-hover-text: var(--vp-c-white);
--vp-button-brand-hover-bg: var(--vp-c-brand-light);
--vp-button-brand-active-border: var(--vp-c-brand-light);
--vp-button-brand-active-text: var(--vp-c-white);
--vp-button-brand-active-bg: var(--vp-button-brand-bg);
}
/**
* Component: Home
* -------------------------------------------------------------------------- */
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, #bd34fe 30%, #41d1ff);
--vp-home-hero-image-background-image: linear-gradient(-45deg, #bd34fe 50%, #47caff 50%);
--vp-home-hero-image-filter: blur(40px);
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(72px);
}
.VPContent .VPDoc {
padding: 20px 0 40px;
}
.VPDoc .container > .content {
background: #fff;
padding: 28px 24px;
box-shadow: 0 0 8px #f1f3f5;
border: 1px solid #f1f3f5;
border-radius: 4px;
}
}
/**
* Component: Custom Block
* -------------------------------------------------------------------------- */
:root {
--vp-custom-block-tip-border: var(--vp-c-brand);
--vp-custom-block-tip-text: var(--vp-c-brand-darker);
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
}
.dark {
--vp-custom-block-tip-border: var(--vp-c-brand);
--vp-custom-block-tip-text: var(--vp-c-brand-lightest);
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
}
/**
* Component: Algolia
* -------------------------------------------------------------------------- */
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand) !important;
}
.Layout {
background-color: #f1f3f5;
}
#VPContent .VPDoc.has-aside .content-container {
max-width: initial;
margin: 0;
}
.VPNav {
border-bottom: 1px solid #f1f3f5;
background: #fff;
}
.curtain,
.aside-curtain {
display: none;
}
#VPContent .aside {
padding-left: 20px;
}
@media (min-width: 640px) {
.vp-doc div[class*="language-"] {
border-radius: 2px;
}
}
#VPSidebarNav {
margin-top: 20px;
background: #fff;
padding: 16px;
}
#VPSidebarNav .group {
padding-top: 0;
}
#app .VPSidebar {
width: calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 20px);
padding-right: 24px;
}
#app .aside-container {
padding-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 20px);
}
#app .aside {
max-width: 228px;
}
#app .pager-link {
border-radius: 4px;
}
#app .VPNavBarTitle.has-sidebar .title {
border-bottom-color: transparent;
}
#app .VPMenu {
border-radius: 2px;
}
/**
* Component: Home
* -------------------------------------------------------------------------- */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
.content img {
border: 1px solid #e1f1f1;
border-radius: 2px;
}
.item {
width: 100%;
overflow: hidden;
}
.link {
width: 100%;
}
.text {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
body {
font-size: 14px;
}
.VPSidebarItem {
/* padding: 0 8px; */
}
.items > .VPSidebarItem.level-1 {
margin-top: 4px;
padding: 0 8px;
}
.items > .VPSidebarItem.level-1.is-active {
background: hsl(204 100% 95% / 1);
/* padding: 0 8px; */
border-radius: 2px;
}
.vp-doc tr:nth-child(2n) {
background-color: transparent;
}
.vp-doc h2 {
margin: 32px 0 16px;
border-top: none;
padding-top: 0;
border-bottom: 1px solid var(--vp-c-divider);
padding-bottom: 8px;
letter-spacing: -0.02em;
line-height: 32px;
font-size: 24px;
font-weight: 600;
}
.vp-doc a {
display: none;
}
.VPNavBarTitle .title {
font-weight: 400;
}

49
docs/api-examples.md Normal file
View File

@ -0,0 +1,49 @@
---
outline: deep
---
# Runtime API Examples
This page demonstrates usage of some of the runtime APIs provided by VitePress.
The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:
```md
<script setup>
import { useData } from 'vitepress'
const { theme, page, frontmatter } = useData()
</script>
## Results
### Theme Data
<pre>{{ theme }}</pre>
### Page Data
<pre>{{ page }}</pre>
### Page Frontmatter
<pre>{{ frontmatter }}</pre>
```
<script setup>
import { useData } from 'vitepress'
const { site, theme, page, frontmatter, localeIndex } = useData()
</script>
## 输出结果
### 主题数据
{{localeIndex}}
<pre>{{ theme }}</pre>
### 页面数据
<pre>{{ page }}</pre>
### 页面前置数据
<pre>{{ frontmatter }}</pre>
## 更多
Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).

View File

@ -0,0 +1,85 @@
# 日常开发中如何利用 EJS 模板引擎辅助生成代码?
在如今的前端开发中,[EJS](https://ejs.bootcss.com/) 已经是一个过时的开发方案,但其实在辅助开发方面还是用处的。话不多说,接下来以一个例子来感受下。
## 例子
在日常开发中,有时候会遇到后端有一些枚举字典的定义,前端也要进行相应的配置。例如在做权限功能时,后端给了我们下面的权限码数据:
```json
[
{
"id": 1,
"code": "access:readAbc",
"authName": "读取权限",
"level": 1
},
{
"id": 2,
"code": "access:writeAbc",
"authName": "写入权限"
}
]
```
我们需要把他做成枚举的形式, 也就是下面的形式:
```typescript
/**
* 权限码
*/
export const enum AuthEnum {
/**
* 读取权限
*/
ReadAbc = "access:readAbc",
/**
* 写入权限
*/
WriteAbc = "access:writeAbc",
}
```
那么如何从这项无聊的工作中解放出来呢,操作也比较简单。
1\. 首先打开一个能编译 EJS 的在线网站, 例如 [One Compiler](https://onecompiler.com/ejs)。
![One Compiler](./onecompiler-ejs.png)
2\. 根据目标数据结构整理下代码,点击`RUN`按钮即可。
```typescript
<%
let items = [
{
"id": 1,
"code": "access:readAbc",
"authName": "读取权限",
},
{
"id": 2,
"code": "access:writeAbc",
"authName": "写入权限",
},
]
items = items.map(item => {
let key = item.code.substring(7);
key = key.charAt(0).toUpperCase() + key.slice(1)
return { ...item, key }
})
-%>
/**
* 权限码
*/
export const enum DaEnum {
<% items.forEach((i, index) => { -%>
/**
* <%= i.authName %>
*/
<%= i.key %> = '<%= i.code %>',
<% }) -%>
}
```
## 结语
以上这是一个简单的例子但在命令行工具进行页面模板的生成方面可能用得比较多。另外EJS 的语法也不难,在一些重复性的代码生成方面还是不错的。

1
docs/front-end/index.md Normal file
View File

@ -0,0 +1 @@
前端

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -0,0 +1,112 @@
# 从new xx()和new xx的区别中整理JS中操作符的优先级
## 问题
最近在工作中写日期格式化时,遇到一个问题,先来看下面的代码:
```typescript
// 写法一
new Date().toISOString
// 写法二
new Date.toISOString;
```
执行如下:
![](./new-()and-new.png)
猜测是优先级的问题然后在MDN找到了[答案](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_precedence#%E6%B1%87%E6%80%BB%E8%A1%A8),它们确实是两个不同的优先级,如下图:
![](./js-diff-new()-and-new.png)
以上,方式一中的 `.` 优先级最高,但其左边需要先求值,因而先执行`new Date()`得到实例,再返回实例上的`toISOString`属性;而方式二中的 `. `优先级也是最高,但其左边不用求值,因而可以先执行`Date.toISOString`得到`toISOString`值(Date上不存在该属性因此该值为undefined),再尝试进行`new toISOString`操作时报错发生。
## 优先级
我印象中的运算符只有四十几个没想到在MDN里面找到的有六十几个。说实话上面的运算符以前真没注意过趁着空闲我决定将这些运算符整理下整理后总体分为下面的五大类
### 一级运算符
大部分是一元操作符。
| 运算符 | 类型 | 说明
| :--- | :--- | ---
| ( … ) | 分组 | 优先级最高的运算符
| … . … | 成员访问 | 静态访问
| [] | 需计算的成员访问 | 动态访问
| new xx() | new带参数列表 | 实例化
| () | 函数调用 | 函数调用
| ?. | 可选链Optional chaining | `a?.b`类似于`a === null \|\| a === void 0 ? void 0 : a.b;`
| new … | new无参数列表 | 实例化
| … ++ | 后置递增 | 先返回再+1例如 `let a = 1; const b = a++; // b: 1 a: 2`。比较常见的是在for循环中进行后置递增。
| … -- | 后置递减 | 同上
| ! … | 逻辑非 (!) |
| ~ … | 按位非 (~) |
| + … | 一元加法 (+) | 可用于把字符串转换为数值,例如`+'1'`将得到`1`。
| - … | 一元减法 (-) | 同上
| ++ … | 前置递增 | 与后置递增不同,先执行+1再返回
| -- … | 前置递减 | 同上
| typeof … | typeof | 返回值只有这几个string \| number \| boolean \| undefined \| null \| function \| object
| void … | void | 比较常见的是使用`void 0`代替undefined因为在以前undefined是可以作为变量名使用的。
| delete … | delete | 删除对象的属性
| await … | await | 等待某个promise执行成功
### 算符运算符
| 运算符 | 类型 | 说明
| :--- | :--- | ---
| … ** … | 幂 (**) |
| … * … | 乘法 (*) |
| … / … | 除法 (/) |
| … % … | 取余 (%) |
| … + … | 加法 (+) |
| … - … | 减法 (-) |
| … &lt;&lt; … | 按位左移 (&lt;&lt;) | 通常用于二进制数据的移位, 例如:`(4)<<1` `8``4``100`, `1000``8`
| … &gt;&gt; … | 按位右移 (&gt;&gt;) | 同上
| … &gt;&gt;&gt; … | 无符号右移 (&gt;&gt;&gt;) | 同上
| … &lt; … | 小于 (&lt;) | 对于数值,比较大小
| … &lt;= … | 小于等于 (&lt;=) |
| … &gt; … | 大于 (&gt;) |
| … &gt;= … | 大于等于 (&gt;=) |
### 比较运算符
| 运算符 | 类型 | 说明
| :--- | :--- | ---
| … in … | in | 判断某个属性是否存在于对象上,会顺着原型链进行查找,可以用`Object.prototype.hasOwnProperty.call(obj, 'xx')`进行检测自身的属性是否存在。
| … instanceof … | instanceof | 判断右边的对象,是否在左边对象的原型链上。
| … == … | 相等 (==) | 左右两边的值可能会先做隐式转换,再进行比较,例如: `'1' == 1 //true`
| … != … | 不相等 (!=) | 同上
| … === … | 一致/严格相等 (===) | 左右两边的值不做隐式转换,直接比较,例如:`'1' === 1 // false`
| … !== … | 不一致/严格不相等 (!==) | 同上
### 布尔运算符
| 运算符 | 类型 | 说明
| :--- | :--- | ---
| … &amp; … | 按位与 (&amp;) | 常用于对二进制数值进行操作
| … ^ … | 按位异或 (^) |
| … \| … | 按位或 (\|) |
| … &amp;&amp; … | 逻辑与 (&amp;&amp;) |
| … \|\| … | 逻辑或 (||) |
| … ?? … | 空值合并 (??) | 当左边的值不为undefined或null时返回左边否则返回右边 具体代码可能是这样的\n: `a !== null && a !== void 0 ? a : b;` |
### 赋值运算符
| 运算符 | 类型 | 说明
| :--- | :--- | ---
| … ? … : … | 条件(三元)运算符 | 可以在简单场景中代替if/else使用不过自从出了??运算符,这个运算符用的比较少
| … = … | 赋值 |
| … += … |
| … -= … |
| … **= … |
| … *= … |
| … /= … |
| … %= … |
| … &lt;&lt;= … |
| … &gt;&gt;= … |
| … &gt;&gt;&gt;= … |
| … &amp;= … |
| … ^= … |
| … |= … |
| … &amp;&amp;= … |
| … ||= … |
| … ??= … |
| … , … | 逗号 / 序列 | 优先级最低的运算符,由于其会返回最后一个值,在某些简短操作中也会用到,例如:`const map = items.reduce((m, i) => (m[i.id]=i,m), {})`
## 结语
优先级的重要性不言而喻,往后还是要多多温习。

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

19
docs/index.md Normal file
View File

@ -0,0 +1,19 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: "绝弹博客"
text: ""
tagline: 一位前端开发者的博客
actions:
- theme: brand
text: 首页
link: /markdown-examples
- theme: alt
text: 测试页面
link: /api-examples
image:
src: /assets/td.svg
alt: page
---

85
docs/markdown-examples.md Normal file
View File

@ -0,0 +1,85 @@
# Markdown语法示例
This page demonstrates some of the built-in markdown extensions provided by VitePress.
## Syntax Highlighting
VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:
**Input**
````
```js{4}
export default {
data () {
return {
msg: 'Highlighted!'
}
}
}
```
````
**Output**
```js{4}
export default {
data () {
return {
msg: 'Highlighted!'
}
}
}
```
## Custom Containers
**Input**
```md
::: info
This is an info box.
:::
::: tip
This is a tip.
:::
::: warning
This is a warning.
:::
::: danger
This is a dangerous warning.
:::
::: details
This is a details block.
:::
```
**Output**
::: info
This is an info box.
:::
::: tip
This is a tip.
:::
::: warning
This is a warning.
:::
::: danger
This is a dangerous warning.
:::
::: details
This is a details block.
:::
## More
Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 79 KiB

BIN
docs/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/public/juetan.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

60
index.html Normal file
View File

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>%VITE_APP_TITLE% - %VITE_APP_SUBTITLE%</title>
</head>
<body>
<div id="app" class="dark:bg-slate-900 dark:text-slate-200">
<style>
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
font-size: 14px;
font-family: Inter, "-apple-system", BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "noto sans",
"Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
#app {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loading-image {
width: 64px;
height: 64px;
}
.loading-title {
margin: 0;
margin-top: 20px;
font-size: 22px;
font-weight: 400;
line-height: 1;
}
.loading-tip {
margin-top: 12px;
line-height: 1;
}
</style>
<div class="loading">
<img src="/assets/loading.svg" alt="loading" class="loading-image" />
<h1 class="loading-title">欢迎访问%VITE_APP_TITLE%</h1>
<div class="loading-tip">正在加载中, 请稍后...</div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

55
package.json Normal file
View File

@ -0,0 +1,55 @@
{
"name": "common",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"new": "cd ./scripts/plop && plop",
"api": "tsx ./scripts/openapi/index.ts",
"release": "release-it --config ./scripts/release/index.cjs",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
},
"prettier": {
"tabWidth": 2,
"printWidth": 120
},
"devDependencies": {
"@arco-design/web-vue": "^2.44.4",
"@iconify-json/icon-park-outline": "^1.1.10",
"@release-it/conventional-changelog": "^5.1.1",
"@types/lodash-es": "^4.17.7",
"@types/nprogress": "^0.2.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@vueuse/core": "^9.13.0",
"axios": "^1.3.6",
"dayjs": "^1.11.7",
"less": "^4.1.3",
"lodash-es": "^4.17.21",
"nprogress": "^0.2.0",
"numeral": "^2.0.6",
"pinia": "^2.0.33",
"plop": "^3.1.2",
"release-it": "^15.10.1",
"swagger-typescript-api": "^12.0.4",
"tsx": "^3.12.6",
"typescript": "^4.6.4",
"unocss": "^0.49.4",
"unplugin-auto-import": "^0.13.0",
"unplugin-icons": "^0.15.2",
"unplugin-vue-components": "^0.23.0",
"vite": "^4.3.9",
"vite-plugin-mock": "^3.0.0",
"vite-plugin-pages": "^0.28.0",
"vite-plugin-style-import": "^2.0.0",
"vitepress": "1.0.0-beta.1",
"vue": "^3.3.4",
"vue-router": "^4.1.6",
"vue-tsc": "^1.0.9"
}
}

7552
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: rgb(255, 255, 255); display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<g transform="translate(50 50)">
<g>
<animateTransform attributeName="transform" type="rotate" values="0;45" keyTimes="0;1" dur="0.2s" repeatCount="indefinite"></animateTransform><path d="M29.491524206117255 -5.5 L37.491524206117255 -5.5 L37.491524206117255 5.5 L29.491524206117255 5.5 A30 30 0 0 1 24.742744050198738 16.964569457146712 L24.742744050198738 16.964569457146712 L30.399598299691117 22.621423706639092 L22.621423706639096 30.399598299691114 L16.964569457146716 24.742744050198734 A30 30 0 0 1 5.5 29.491524206117255 L5.5 29.491524206117255 L5.5 37.491524206117255 L-5.499999999999997 37.491524206117255 L-5.499999999999997 29.491524206117255 A30 30 0 0 1 -16.964569457146705 24.742744050198738 L-16.964569457146705 24.742744050198738 L-22.621423706639085 30.399598299691117 L-30.399598299691117 22.621423706639092 L-24.742744050198738 16.964569457146712 A30 30 0 0 1 -29.491524206117255 5.500000000000009 L-29.491524206117255 5.500000000000009 L-37.491524206117255 5.50000000000001 L-37.491524206117255 -5.500000000000001 L-29.491524206117255 -5.500000000000002 A30 30 0 0 1 -24.742744050198738 -16.964569457146705 L-24.742744050198738 -16.964569457146705 L-30.399598299691117 -22.621423706639085 L-22.621423706639092 -30.399598299691117 L-16.964569457146712 -24.742744050198738 A30 30 0 0 1 -5.500000000000011 -29.491524206117255 L-5.500000000000011 -29.491524206117255 L-5.500000000000012 -37.491524206117255 L5.499999999999998 -37.491524206117255 L5.5 -29.491524206117255 A30 30 0 0 1 16.964569457146702 -24.74274405019874 L16.964569457146702 -24.74274405019874 L22.62142370663908 -30.39959829969112 L30.399598299691117 -22.6214237066391 L24.742744050198738 -16.964569457146716 A30 30 0 0 1 29.491524206117255 -5.500000000000013 M0 -20A20 20 0 1 0 0 20 A20 20 0 1 0 0 -20" fill="#09f"></path></g></g>
<!-- [ldio] generated by https://loading.io/ --></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

63
scripts/openapi/index.ts Normal file
View File

@ -0,0 +1,63 @@
import path from "path";
import { generateApi } from "swagger-typescript-api";
import { loadEnv } from "vite";
import { fileURLToPath } from "url";
const __dirname = path.join(fileURLToPath(new URL(import.meta.url)), "..");
const env = loadEnv("development", process.cwd());
const run = async () => {
const output = await generateApi({
url: env.VITE_API_DOCS_URL,
templates: path.resolve(__dirname, "./template"),
output: path.resolve(process.cwd(), "src/api/service"),
name: "index.ts",
singleHttpClient: false,
httpClientType: "axios",
unwrapResponseData: false,
moduleNameIndex: 1,
moduleNameFirstTag: true,
cleanOutput: true,
// generateRouteTypes: true,
extractRequestParams: true,
modular: false,
prettier: {
printWidth: 120,
tabWidth: 2,
trailingComma: "all",
parser: "typescript",
},
});
// const { configuration, getTemplate, renderTemplate, createFile } = output
// const { config } = configuration
// const { templateInfos } = config
// const templateMap = templateInfos.reduce((acc, { fileName, name }) => ({
// ...acc,
// [name]: getTemplate({ fileName, name }),
// }),
// {});
// const files = [
// {
// path: config.output,
// fileName: 'dataContracts.ts',
// content: renderTemplate(templateMap.dataContracts, configuration),
// },
// {
// path: config.output,
// fileName: 'httpClient.ts',
// content: renderTemplate(templateMap.httpClient, configuration),
// },
// {
// path: config.output,
// fileName: 'apiClient.ts',
// content: renderTemplate(templateMap.api, configuration),
// }
// ]
// for (const file of files) {
// createFile(file)
// }
debugger
return output;
};
run();

View File

@ -0,0 +1,2 @@
# 修改
- procedure-call.ejs 添加return

View File

@ -0,0 +1,65 @@
<%
const { apiConfig, routes, utils, config } = it;
const { info, servers, externalDocs } = apiConfig;
const { _, require, formatDescription } = utils;
const server = (servers && servers[0]) || { url: "" };
const descriptionLines = _.compact([
`@title ${info.title || "No title"}`,
info.version && `@version ${info.version}`,
info.license && `@license ${_.compact([
info.license.name,
info.license.url && `(${info.license.url})`,
]).join(" ")}`,
info.termsOfService && `@termsOfService ${info.termsOfService}`,
server.url && `@baseUrl ${server.url}`,
externalDocs.url && `@externalDocs ${externalDocs.url}`,
info.contact && `@contact ${_.compact([
info.contact.name,
info.contact.email && `<${info.contact.email}>`,
info.contact.url && `(${info.contact.url})`,
]).join(" ")}`,
info.description && " ",
info.description && _.replace(formatDescription(info.description), /\n/g, "\n * "),
]);
%>
<% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import { AxiosRequestConfig, AxiosResponse } from "axios"; <% } %>
<% if (descriptionLines.length) { %>
/**
<% descriptionLines.forEach((descriptionLine) => { %>
* <%~ descriptionLine %>
<% }) %>
*/
<% } %>
export class <%~ config.apiClassName %><SecurityDataType extends unknown><% if (!config.singleHttpClient) { %> extends HttpClient<SecurityDataType> <% } %> {
<% if(config.singleHttpClient) { %>
http: HttpClient<SecurityDataType>;
constructor (http: HttpClient<SecurityDataType>) {
this.http = http;
}
<% } %>
<% routes.outOfModule && routes.outOfModule.forEach((route) => { %>
<%~ includeFile('./procedure-call.ejs', { ...it, route }) %>
<% }) %>
<% routes.combined && routes.combined.forEach(({ routes = [], moduleName }) => { %>
<%~ moduleName %> = {
<% routes.forEach((route) => { %>
<%~ includeFile('./procedure-call.ejs', { ...it, route }) %>
<% }) %>
}
<% }) %>
}

View File

@ -0,0 +1,37 @@
<%
const { data, utils } = it;
const { formatDescription, require, _ } = utils;
const stringify = (value) => _.isObject(value) ? JSON.stringify(value) : _.isString(value) ? `"${value}"` : value
const jsDocLines = _.compact([
data.title,
data.description && formatDescription(data.description),
!_.isUndefined(data.deprecated) && data.deprecated && '@deprecated',
!_.isUndefined(data.format) && `@format ${data.format}`,
!_.isUndefined(data.minimum) && `@min ${data.minimum}`,
!_.isUndefined(data.multipleOf) && `@multipleOf ${data.multipleOf}`,
!_.isUndefined(data.exclusiveMinimum) && `@exclusiveMin ${data.exclusiveMinimum}`,
!_.isUndefined(data.maximum) && `@max ${data.maximum}`,
!_.isUndefined(data.minLength) && `@minLength ${data.minLength}`,
!_.isUndefined(data.maxLength) && `@maxLength ${data.maxLength}`,
!_.isUndefined(data.exclusiveMaximum) && `@exclusiveMax ${data.exclusiveMaximum}`,
!_.isUndefined(data.maxItems) && `@maxItems ${data.maxItems}`,
!_.isUndefined(data.minItems) && `@minItems ${data.minItems}`,
!_.isUndefined(data.uniqueItems) && `@uniqueItems ${data.uniqueItems}`,
!_.isUndefined(data.default) && `@default ${stringify(data.default)}`,
!_.isUndefined(data.pattern) && `@pattern ${data.pattern}`,
!_.isUndefined(data.example) && `@example ${stringify(data.example)}`
]).join('\n').split('\n');
%>
<% if (jsDocLines.every(_.isEmpty)) { %>
<% } else if (jsDocLines.length === 1) { %>
/** <%~ jsDocLines[0] %> */
<% } else if (jsDocLines.length) { %>
/**
<% for (jsDocLine of jsDocLines) { %>
* <%~ jsDocLine %>
<% } %>
*/
<% } %>

View File

@ -0,0 +1,28 @@
<%
const { modelTypes, utils, config } = it;
const { formatDescription, require, _, Ts } = utils;
const dataContractTemplates = {
enum: (contract) => {
return `enum ${contract.name} {\r\n${contract.content} \r\n }`;
},
interface: (contract) => {
return `interface ${contract.name} {\r\n${contract.content}}`;
},
type: (contract) => {
return `type ${contract.name} = ${contract.content}`;
},
}
%>
<% if (config.internalTemplateOptions.addUtilRequiredKeysType) { %>
type <%~ config.Ts.CodeGenKeyword.UtilRequiredKeys %><T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
<% } %>
<% modelTypes.forEach((contract) => { %>
<%~ includeFile('@base/data-contract-jsdoc.ejs', { ...it, data: { ...contract, ...contract.typeData } }) %>
export <%~ (dataContractTemplates[contract.typeIdentifier] || dataContractTemplates.type)(contract) %>
<% }) %>

View File

@ -0,0 +1,12 @@
<%
const { contract, utils, config } = it;
const { formatDescription, require, _ } = utils;
const { name, $content } = contract;
%>
<% if (config.generateUnionEnums) { %>
export type <%~ name %> = <%~ _.map($content, ({ value }) => value).join(" | ") %>
<% } else { %>
export enum <%~ name %> {
<%~ _.map($content, ({ key, value }) => `${key} = ${value}`).join(",\n") %>
}
<% } %>

View File

@ -0,0 +1,3 @@
<% const { config } = it; %>
<% /* https://github.com/acacode/swagger-typescript-api/tree/next/templates/base/http-clients/ */ %>
<%~ includeFile(`./http-clients/${config.httpClientType}-http-client`, it) %>

View File

@ -0,0 +1,138 @@
<%
const { apiConfig, generateResponses, config } = it;
%>
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, ResponseType, HeadersDefaults } from "axios";
export type QueryParamsType = Record<string | number, any>;
export interface FullRequestParams extends Omit<AxiosRequestConfig, "data" | "params" | "url" | "responseType"> {
/** set11 parameter to `true` for call `securityWorker` for this request */
secure?: boolean;
/** request path */
path: string;
/** content type of request body */
type?: ContentType;
/** query params */
query?: QueryParamsType;
/** format of response (i.e. response.json() -> format: "json") */
format?: ResponseType;
/** request body */
body?: unknown;
}
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;
export interface ApiConfig<SecurityDataType = unknown> extends Omit<AxiosRequestConfig, "data" | "cancelToken"> {
securityWorker?: (securityData: SecurityDataType | null) => Promise<AxiosRequestConfig | void> | AxiosRequestConfig | void;
secure?: boolean;
format?: ResponseType;
}
export enum ContentType {
Json = "application/json",
FormData = "multipart/form-data",
UrlEncoded = "application/x-www-form-urlencoded",
Text = "text/plain",
}
export class HttpClient<SecurityDataType = unknown> {
public instance: AxiosInstance;
private securityData: SecurityDataType | null = null;
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
private secure?: boolean;
private format?: ResponseType;
constructor({ securityWorker, secure, format, ...axiosConfig }: ApiConfig<SecurityDataType> = {}) {
this.instance = axios.create({ ...axiosConfig, baseURL: axiosConfig.baseURL || "<%~ apiConfig.baseUrl %>" })
this.secure = secure;
this.format = format;
this.securityWorker = securityWorker;
}
public setSecurityData = (data: SecurityDataType | null) => {
this.securityData = data
}
protected mergeRequestParams(params1: AxiosRequestConfig, params2?: AxiosRequestConfig): AxiosRequestConfig {
const method = params1.method || (params2 && params2.method)
return {
...this.instance.defaults,
...params1,
...(params2 || {}),
headers: {
...((method && this.instance.defaults.headers[method.toLowerCase() as keyof HeadersDefaults]) || {}),
...(params1.headers || {}),
...((params2 && params2.headers) || {}),
},
};
}
protected stringifyFormItem(formItem: unknown) {
if (typeof formItem === "object" && formItem !== null) {
return JSON.stringify(formItem);
} else {
return `${formItem}`;
}
}
protected createFormData(input: Record<string, unknown>): FormData {
return Object.keys(input || {}).reduce((formData, key) => {
const property = input[key];
const propertyContent: any[] = (property instanceof Array) ? property : [property]
for (const formItem of propertyContent) {
const isFileType = formItem instanceof Blob || formItem instanceof File;
formData.append(
key,
isFileType ? formItem : this.stringifyFormItem(formItem)
);
}
return formData;
}, new FormData());
}
public request = async <T = any, _E = any>({
secure,
path,
type,
query,
format,
body,
...params
<% if (config.unwrapResponseData) { %>
}: FullRequestParams): Promise<T> => {
<% } else { %>
}: FullRequestParams): Promise<AxiosResponse<T>> => {
<% } %>
const secureParams = ((typeof secure === 'boolean' ? secure : this.secure) && this.securityWorker && (await this.securityWorker(this.securityData))) || {};
const requestParams = this.mergeRequestParams(params, secureParams);
const responseFormat = (format || this.format) || undefined;
if (type === ContentType.FormData && body && body !== null && typeof body === "object") {
body = this.createFormData(body as Record<string, unknown>);
}
if (type === ContentType.Text && body && body !== null && typeof body !== "string") {
body = JSON.stringify(body);
}
return this.instance.request({
...requestParams,
headers: {
...(requestParams.headers || {}),
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
},
params: query,
responseType: responseFormat,
data: body,
url: path,
<% if (config.unwrapResponseData) { %>
}).then(response => response.data);
<% } else { %>
});
<% } %>
};
}

View File

@ -0,0 +1,224 @@
<%
const { apiConfig, generateResponses, config } = it;
%>
export type QueryParamsType = Record<string | number, any>;
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
export interface FullRequestParams extends Omit<RequestInit, "body"> {
/** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean;
/** request path */
path: string;
/** content type of request body */
type?: ContentType;
/** query params */
query?: QueryParamsType;
/** format of response (i.e. response.json() -> format: "json") */
format?: ResponseFormat;
/** request body */
body?: unknown;
/** base url */
baseUrl?: string;
/** request cancellation token */
cancelToken?: CancelToken;
}
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">
export interface ApiConfig<SecurityDataType = unknown> {
baseUrl?: string;
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
securityWorker?: (securityData: SecurityDataType | null) => Promise<RequestParams | void> | RequestParams | void;
customFetch?: typeof fetch;
}
export interface HttpResponse<D extends unknown, E extends unknown = unknown> extends Response {
data: D;
error: E;
}
type CancelToken = Symbol | string | number;
export enum ContentType {
Json = "application/json",
FormData = "multipart/form-data",
UrlEncoded = "application/x-www-form-urlencoded",
Text = "text/plain",
}
export class HttpClient<SecurityDataType = unknown> {
public baseUrl: string = "<%~ apiConfig.baseUrl %>";
private securityData: SecurityDataType | null = null;
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
private abortControllers = new Map<CancelToken, AbortController>();
private customFetch = (...fetchParams: Parameters<typeof fetch>) => fetch(...fetchParams);
private baseApiParams: RequestParams = {
credentials: 'same-origin',
headers: {},
redirect: 'follow',
referrerPolicy: 'no-referrer',
}
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
Object.assign(this, apiConfig);
}
public setSecurityData = (data: SecurityDataType | null) => {
this.securityData = data;
}
protected encodeQueryParam(key: string, value: any) {
const encodedKey = encodeURIComponent(key);
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
}
protected addQueryParam(query: QueryParamsType, key: string) {
return this.encodeQueryParam(key, query[key]);
}
protected addArrayQueryParam(query: QueryParamsType, key: string) {
const value = query[key];
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
}
protected toQueryString(rawQuery?: QueryParamsType): string {
const query = rawQuery || {};
const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]);
return keys
.map((key) =>
Array.isArray(query[key])
? this.addArrayQueryParam(query, key)
: this.addQueryParam(query, key),
)
.join("&");
}
protected addQueryParams(rawQuery?: QueryParamsType): string {
const queryString = this.toQueryString(rawQuery);
return queryString ? `?${queryString}` : "";
}
private contentFormatters: Record<ContentType, (input: any) => any> = {
[ContentType.Json]: (input:any) => input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input,
[ContentType.Text]: (input:any) => input !== null && typeof input !== "string" ? JSON.stringify(input) : input,
[ContentType.FormData]: (input: any) =>
Object.keys(input || {}).reduce((formData, key) => {
const property = input[key];
formData.append(
key,
property instanceof Blob ?
property :
typeof property === "object" && property !== null ?
JSON.stringify(property) :
`${property}`
);
return formData;
}, new FormData()),
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
}
protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
return {
...this.baseApiParams,
...params1,
...(params2 || {}),
headers: {
...(this.baseApiParams.headers || {}),
...(params1.headers || {}),
...((params2 && params2.headers) || {}),
},
};
}
protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => {
if (this.abortControllers.has(cancelToken)) {
const abortController = this.abortControllers.get(cancelToken);
if (abortController) {
return abortController.signal;
}
return void 0;
}
const abortController = new AbortController();
this.abortControllers.set(cancelToken, abortController);
return abortController.signal;
}
public abortRequest = (cancelToken: CancelToken) => {
const abortController = this.abortControllers.get(cancelToken)
if (abortController) {
abortController.abort();
this.abortControllers.delete(cancelToken);
}
}
public request = async <T = any, E = any>({
body,
secure,
path,
type,
query,
format,
baseUrl,
cancelToken,
...params
<% if (config.unwrapResponseData) { %>
}: FullRequestParams): Promise<T> => {
<% } else { %>
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
<% } %>
const secureParams = ((typeof secure === 'boolean' ? secure : this.baseApiParams.secure) && this.securityWorker && await this.securityWorker(this.securityData)) || {};
const requestParams = this.mergeRequestParams(params, secureParams);
const queryString = query && this.toQueryString(query);
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
const responseFormat = format || requestParams.format;
return this.customFetch(
`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`,
{
...requestParams,
headers: {
...(requestParams.headers || {}),
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
},
signal: cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal,
body: typeof body === "undefined" || body === null ? null : payloadFormatter(body),
}
).then(async (response) => {
const r = response as HttpResponse<T, E>;
r.data = (null as unknown) as T;
r.error = (null as unknown) as E;
const data = !responseFormat ? r : await response[responseFormat]()
.then((data) => {
if (r.ok) {
r.data = data;
} else {
r.error = data;
}
return r;
})
.catch((e) => {
r.error = e;
return r;
});
if (cancelToken) {
this.abortControllers.delete(cancelToken);
}
<% if (!config.disableThrowOnError) { %>
if (!response.ok) throw data;
<% } %>
<% if (config.unwrapResponseData) { %>
return data.data;
<% } else { %>
return data;
<% } %>
});
};
}

View File

@ -0,0 +1,10 @@
<%
const { contract, utils } = it;
const { formatDescription, require, _ } = utils;
%>
export interface <%~ contract.name %> {
<% _.forEach(contract.$content, (field) => { %>
<%~ includeFile('@base/object-field-jsdoc.ejs', { ...it, field }) %>
<%~ field.name %><%~ field.isRequired ? '' : '?' %>: <%~ field.value %><%~ field.isNullable ? ' | null' : ''%>;
<% }) %>
}

View File

@ -0,0 +1,28 @@
<%
const { field, utils } = it;
const { formatDescription, require, _ } = utils;
const comments = _.uniq(
_.compact([
field.title,
field.description,
field.deprecated && ` * @deprecated`,
!_.isUndefined(field.format) && `@format ${field.format}`,
!_.isUndefined(field.minimum) && `@min ${field.minimum}`,
!_.isUndefined(field.maximum) && `@max ${field.maximum}`,
!_.isUndefined(field.pattern) && `@pattern ${field.pattern}`,
!_.isUndefined(field.example) &&
`@example ${_.isObject(field.example) ? JSON.stringify(field.example) : field.example}`,
]).reduce((acc, comment) => [...acc, ...comment.split(/\n/g)], []),
);
%>
<% if (comments.length === 1) { %>
/** <%~ comments[0] %> */
<% } else if (comments.length) { %>
/**
<% comments.forEach(comment => { %>
* <%~ comment %>
<% }) %>
*/
<% } %>

View File

@ -0,0 +1,101 @@
<%
const { utils, route, config } = it;
const { requestBodyInfo, responseBodyInfo, specificArgNameResolver } = route;
const { _, getInlineParseContent, getParseContent, parseSchema, getComponentByRef, require } = utils;
const { parameters, path, method, payload, query, formData, security, requestParams } = route.request;
const { type, errorType, contentTypes } = route.response;
const { HTTP_CLIENT, RESERVED_REQ_PARAMS_ARG_NAMES } = config.constants;
const routeDocs = includeFile("./route-docs", { config, route, utils });
const queryName = (query && query.name) || "query";
const pathParams = _.values(parameters);
const pathParamsNames = _.map(pathParams, "name");
const isFetchTemplate = config.httpClientType === HTTP_CLIENT.FETCH;
const requestConfigParam = {
name: specificArgNameResolver.resolve(RESERVED_REQ_PARAMS_ARG_NAMES),
optional: true,
type: "RequestParams",
defaultValue: "{}",
}
const argToTmpl = ({ name, optional, type, defaultValue }) => `${name}${!defaultValue && optional ? '?' : ''}: ${type}${defaultValue ? ` = ${defaultValue}` : ''}`;
const rawWrapperArgs = config.extractRequestParams ?
_.compact([
requestParams && {
name: pathParams.length ? `{ ${_.join(pathParamsNames, ", ")}, ...${queryName} }` : queryName,
optional: false,
type: getInlineParseContent(requestParams),
},
...(!requestParams ? pathParams : []),
payload,
requestConfigParam,
]) :
_.compact([
...pathParams,
query,
payload,
requestConfigParam,
])
const wrapperArgs = _
// Sort by optionality
.sortBy(rawWrapperArgs, [o => o.optional])
.map(argToTmpl)
.join(', ')
// RequestParams["type"]
const requestContentKind = {
"JSON": "ContentType.Json",
"URL_ENCODED": "ContentType.UrlEncoded",
"FORM_DATA": "ContentType.FormData",
"TEXT": "ContentType.Text",
}
// RequestParams["format"]
const responseContentKind = {
"JSON": '"json"',
"IMAGE": '"blob"',
"FORM_DATA": isFetchTemplate ? '"formData"' : '"document"'
}
const bodyTmpl = _.get(payload, "name") || null;
const queryTmpl = (query != null && queryName) || null;
const bodyContentKindTmpl = requestContentKind[requestBodyInfo.contentKind] || null;
const responseFormatTmpl = responseContentKind[responseBodyInfo.success && responseBodyInfo.success.schema && responseBodyInfo.success.schema.contentKind] || null;
const securityTmpl = security ? 'true' : null;
const describeReturnType = () => {
if (!config.toJS) return "";
switch(config.httpClientType) {
case HTTP_CLIENT.AXIOS: {
return `Promise<AxiosResponse<${type}>>`
}
default: {
return `Promise<HttpResponse<${type}, ${errorType}>`
}
}
}
%>
/**
<%~ routeDocs.description %>
*<% /* Here you can add some other JSDoc tags */ %>
<%~ routeDocs.lines %>
*/
<%~ route.routeName.usage %><%~ route.namespace ? ': ' : ' = ' %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> => {
return <%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>, <%~ errorType %>>({
path: `<%~ path %>`,
method: '<%~ _.upperCase(method) %>',
<%~ queryTmpl ? `query: ${queryTmpl},` : '' %>
<%~ bodyTmpl ? `body: ${bodyTmpl},` : '' %>
<%~ securityTmpl ? `secure: ${securityTmpl},` : '' %>
<%~ bodyContentKindTmpl ? `type: ${bodyContentKindTmpl},` : '' %>
<%~ responseFormatTmpl ? `format: ${responseFormatTmpl},` : '' %>
...<%~ _.get(requestConfigParam, "name") %>,
})
}<%~ route.namespace ? ',' : '' %>

View File

@ -0,0 +1,30 @@
<%
const { config, route, utils } = it;
const { _, formatDescription, fmtToJSDocLine, pascalCase, require } = utils;
const { raw, request, routeName } = route;
const jsDocDescription = raw.description ?
` * @description ${formatDescription(raw.description, true)}` :
fmtToJSDocLine('No description', { eol: false });
const jsDocLines = _.compact([
_.size(raw.tags) && ` * @tags ${raw.tags.join(", ")}`,
` * @name ${pascalCase(routeName.usage)}`,
raw.summary && ` * @summary ${raw.summary}`,
` * @request ${_.upperCase(request.method)}:${raw.route}`,
raw.deprecated && ` * @deprecated`,
routeName.duplicate && ` * @originalName ${routeName.original}`,
routeName.duplicate && ` * @duplicate`,
request.security && ` * @secure`,
...(config.generateResponses && raw.responsesTypes.length
? raw.responsesTypes.map(
({ type, status, description, isSuccess }) =>
` * @response \`${status}\` \`${_.replace(_.replace(type, /\/\*/g, "\\*"), /\*\//g, "*\\")}\` ${description}`,
)
: []),
]).map(str => str.trimEnd()).join("\n");
return {
description: jsDocDescription,
lines: jsDocLines,
}
%>

View File

@ -0,0 +1,43 @@
<%
const { routeInfo, utils } = it;
const {
operationId,
method,
route,
moduleName,
responsesTypes,
description,
tags,
summary,
pathArgs,
} = routeInfo;
const { _, fmtToJSDocLine, require } = utils;
const methodAliases = {
get: (pathName, hasPathInserts) =>
_.camelCase(`${pathName}_${hasPathInserts ? "detail" : "list"}`),
post: (pathName, hasPathInserts) => _.camelCase(`${pathName}_create`),
put: (pathName, hasPathInserts) => _.camelCase(`${pathName}_update`),
patch: (pathName, hasPathInserts) => _.camelCase(`${pathName}_partial_update`),
delete: (pathName, hasPathInserts) => _.camelCase(`${pathName}_delete`),
};
const createCustomOperationId = (method, route, moduleName) => {
const hasPathInserts = /\{(\w){1,}\}/g.test(route);
const splitedRouteBySlash = _.compact(_.replace(route, /\{(\w){1,}\}/g, "").split("/"));
const routeParts = (splitedRouteBySlash.length > 1
? splitedRouteBySlash.splice(1)
: splitedRouteBySlash
).join("_");
return routeParts.length > 3 && methodAliases[method]
? methodAliases[method](routeParts, hasPathInserts)
: _.camelCase(_.lowerCase(method) + "_" + [moduleName].join("_")) || "index";
};
if (operationId)
return _.camelCase(operationId);
if (route === "/")
return _.camelCase(`${_.lowerCase(method)}Root`);
return createCustomOperationId(method, route, moduleName);
%>

View File

@ -0,0 +1,22 @@
<%
const { route, utils, config } = it;
const { _, pascalCase, require } = utils;
const { query, payload, pathParams, headers } = route.request;
const routeDocs = includeFile("@base/route-docs", { config, route, utils });
const routeNamespace = pascalCase(route.routeName.usage);
%>
/**
<%~ routeDocs.description %>
<%~ routeDocs.lines %>
*/
export namespace <%~ routeNamespace %> {
export type RequestParams = <%~ (pathParams && pathParams.type) || '{}' %>;
export type RequestQuery = <%~ (query && query.type) || '{}' %>;
export type RequestBody = <%~ (payload && payload.type) || 'never' %>;
export type RequestHeaders = <%~ (headers && headers.type) || '{}' %>;
export type ResponseBody = <%~ route.response.type %>;
}

View File

@ -0,0 +1,28 @@
<%
const { utils, config, routes, modelTypes } = it;
const { _, pascalCase } = utils;
const dataContracts = config.modular ? _.map(modelTypes, "name") : [];
%>
<% if (dataContracts.length) { %>
import { <%~ dataContracts.join(", ") %> } from "./<%~ config.fileNames.dataContracts %>"
<% } %>
<%
/* TODO: outOfModule, combined should be attributes of route, which will allow to avoid duplication of code */
%>
<% routes.outOfModule && routes.outOfModule.forEach(({ routes = [] }) => { %>
<% routes.forEach((route) => { %>
<%~ includeFile('@base/route-type.ejs', { ...it, route }) %>
<% }) %>
<% }) %>
<% routes.combined && routes.combined.forEach(({ routes = [], moduleName }) => { %>
export namespace <%~ pascalCase(moduleName) %> {
<% routes.forEach((route) => { %>
<%~ includeFile('@base/route-type.ejs', { ...it, route }) %>
<% }) %>
}
<% }) %>

View File

@ -0,0 +1,15 @@
<%
const { contract, utils } = it;
const { formatDescription, require, _ } = utils;
%>
<% if (contract.$content.length) { %>
export type <%~ contract.name %> = {
<% _.forEach(contract.$content, (field) => { %>
<%~ includeFile('@base/object-field-jsdoc.ejs', { ...it, field }) %>
<%~ field.field %>;
<% }) %>
}<%~ utils.isNeedToAddNull(contract) ? ' | null' : ''%>
<% } else { %>
export type <%~ contract.name %> = Record<string, any>;
<% } %>

58
scripts/plop/plopfile.js Normal file
View File

@ -0,0 +1,58 @@
/**
* 模板生成器
* @param {import('plop').NodePlopAPI} plop
*/
export default function (plop) {
plop.setGenerator('route', {
description: '创建一个路由',
prompts: [
{
type: 'input',
name: 'name',
message: '请输入路由名称',
validate: (value) => {
if (!value) {
return '请输入路由名称';
}
return true;
},
},
],
actions: [
{
type: 'add',
path: '../../src/pages/{{name}}.vue',
templateFile: 'template-page.hbs',
},
{
type: 'add',
path: '../../src/pages/{{name}}/index.vue',
templateFile: 'template-page.hbs',
},
],
});
plop.setGenerator('page', {
description: '创建一个页面',
prompts: [
{
type: 'input',
name: 'name',
message: '请输入页面名称',
validate: (value) => {
if (!value) {
return '请输入页面名称';
}
return true;
},
},
],
actions: [
{
type: 'add',
path: '../../src/pages/{{name}}.vue',
templateFile: 'template-page.hbs',
},
],
});
}

View File

@ -0,0 +1,19 @@
<template>
<div class="p-4">
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>
<route lang="json">
{
"meta": {
"sort": 101,
"title": "{{name}}",
"icon": "icon-park-outline-home"
}
}
</route>

65
scripts/release/index.cjs Normal file
View File

@ -0,0 +1,65 @@
const fs = require("fs");
const path = require("path");
const loadTemplate = (name) => {
const filePath = path.join(__dirname, `template-${name}.hbs`);
return fs.readFileSync(filePath, "utf8");
};
module.exports = {
git: {
commitMessage: "chore(release): v${version}",
},
npm: {
publish: false,
},
github: {
release: false,
},
gitlab: {
release: false,
},
plugins: {
"@release-it/conventional-changelog": {
ignoreRecommendedBump: true,
infile: "CHANGELOG.md",
header: "# 版本记录",
preset: {
name: "conventionalcommits",
types: [
{
type: "feat",
section: "✨功能新增",
},
{
type: "impr",
section: "⚡️改进优化",
},
{
type: "fix",
section: "🐛问题修复",
},
],
},
context: {
host: "https://github.com",
owner: "juetan",
repository: "template-vue",
},
gitRawCommitsOpts: {
format:
"%B%n-hash-%n%H%n-shortHash-%n%h%n-gitTags-%n%d%n-committerDate-%n%ci%n-author-%n%an%n-email-%n%ae%n-date-%n%ci",
},
writerOpts: {
commitsSort: false,
commitGroupsSort: (a, b) => {
const order = ["✨功能新增", "⚡️改进优化", "🐛问题修复"];
return order.indexOf(a.title) - order.indexOf(b.title);
},
commitPartial: loadTemplate("commit"),
// headerPartial: loadTemplate('header'),
mainTemplate: loadTemplate("main"),
},
},
},
};

View File

@ -0,0 +1,53 @@
* [{{type}}] {{subject}}
{{~!-- commit link --}}
{{~#if @root.linkReferences}} ([{{shortHash}}](
{{~#if @root.repository}}
{{~#if @root.host}}
{{~@root.host}}/
{{~/if}}
{{~#if @root.owner}}
{{~@root.owner}}/
{{~/if}}
{{~@root.repository}}
{{~else}}
{{~@root.repoUrl}}
{{~/if}}/
{{~@root.commit}}/{{hash}}))
{{~else if hash}} {{hash}}{{~/if}}
{{~!-- commit references --}}
{{~#if references~}}
, closes
{{~#each references}} {{#if @root.linkReferences~}}
[
{{~#if this.owner}}
{{~this.owner}}/
{{~/if}}
{{~this.repository}}#{{this.issue}}](
{{~#if @root.repository}}
{{~#if @root.host}}
{{~@root.host}}/
{{~/if}}
{{~#if this.repository}}
{{~#if this.owner}}
{{~this.owner}}/
{{~/if}}
{{~this.repository}}
{{~else}}
{{~#if @root.owner}}
{{~@root.owner}}/
{{~/if}}
{{~@root.repository}}
{{~/if}}
{{~else}}
{{~@root.repoUrl}}
{{~/if}}/
{{~@root.issue}}/{{this.issue}})
{{~else}}
{{~#if this.owner}}
{{~this.owner}}/
{{~/if}}
{{~this.repository}}#{{this.issue}}
{{~/if}}{{/each}}
{{~/if}}

View File

@ -0,0 +1,9 @@
{{> header}}
{{#each commitGroups}}
{{#each commits}}
{{> commit root=@root}}
{{/each}}
{{/each}}
{{> footer}}

115
scripts/vite/plugin.ts Normal file
View File

@ -0,0 +1,115 @@
import { spawn } from "child_process";
import fs from "fs";
import { Plugin, ResolvedConfig } from "vite";
import pkg from "../../package.json";
/**
* logo
* @description APPTIFY
*/
const LOGO = `
________ ______ ______ _________ ________ ______ __ __
/_______/\\\\ /_____/\\\\ /_____/\\\\ /________/\\\\/_______/\\\\/_____/\\\\ /_/\\\\/_/\\\\
\\\\::: _ \\\\ \\\\\\\\:::_ \\\\ \\\\\\\\:::_ \\\\ \\\\\\\\__.::.__\\\\/\\\\__.::._\\\\/\\\\::::_\\\\/_\\\\ \\\\ \\\\ \\\\ \\\\
\\\\::(_) \\\\ \\\\\\\\:(_) \\\\ \\\\\\\\:(_) \\\\ \\\\ \\\\::\\\\ \\\\ \\\\::\\\\ \\\\ \\\\:\\\\/___/\\\\\\\\:\\\\_\\\\ \\\\ \\\\
\\\\:: __ \\\\ \\\\\\\\: ___\\\\/ \\\\: ___\\\\/ \\\\::\\\\ \\\\ _\\\\::\\\\ \\\\__\\\\:::._\\\\/ \\\\::::_\\\\/
\\\\:.\\\\ \\\\ \\\\ \\\\\\\\ \\\\ \\\\ \\\\ \\\\ \\\\ \\\\::\\\\ \\\\ /__\\\\::\\\\__/\\\\\\\\:\\\\ \\\\ \\\\::\\\\ \\\\
\\\\__\\\\/\\\\__\\\\/ \\\\_\\\\/ \\\\_\\\\/ \\\\__\\\\/ \\\\________\\\\/ \\\\_\\\\/ \\\\__\\\\/
`;
/**
* shell
* @param cmd
* @returns Promise<string | void>
*/
const exec = (cmd: string) => {
return new Promise<string | void>((resolve) => {
if (!cmd) {
return resolve();
}
const child = spawn(cmd, [], { shell: true });
child.stdout.once("data", (data) => {
resolve(data.toString().replace(/"|\n/g, ""));
});
child.stderr.once("data", () => {
resolve();
});
});
};
/**
*
* @returns Promise<string>
*/
const getBuildInfo = async () => {
const hash = await exec("git log --format=%h -n 1");
const time = new Date().toLocaleString("zh-Hans-CN");
const latestTag = await exec("git describe --tags --abbrev=0");
const commits = await exec(`git rev-list --count ${latestTag}..HEAD`);
const version = commits ? `${latestTag}.${commits}` : `v${pkg.version}`;
const content = ` 欢迎访问!版本: ${version} 标识: ${hash} 构建时间: ${time}`;
const style = `"color: #09f; font-weight: 900;", "font-size: 12px; color: #09f; font-family: ''"`;
const script = `console.log(\`%c${LOGO} \n%c${content}\n\`, ${style});\n`;
return script;
};
/**
*
* @returns Plugin
*/
export default function plugin(): Plugin {
let config: ResolvedConfig;
let extension: string;
return {
name: "vite:customizer",
enforce: "pre",
configResolved(resolvedConfig) {
config = resolvedConfig;
const defaultExt = config.mode === "development" ? "dev" : "prod";
extension = config.env.VITE_BUILD_EXTENTION || defaultExt;
},
async transformIndexHtml(html) {
const script = await getBuildInfo();
const replacedHtml = html.replace(/__((\w|_|-)+)__/g, (match, p1) => {
return config.env[`VITE_${p1}`] || "";
});
return {
html: replacedHtml,
tags: [
{
tag: "script",
injectTo: "body",
children: script,
},
],
};
},
async resolveId(id, importer, options) {
if (!extension || !id.startsWith("/src")) {
return;
}
const resolution = await this.resolve(id, importer, { skipSelf: true, ...options });
const targetPath = resolution?.id.replace(/\.([^.]*?)$/, `.${extension}.$1`);
if (targetPath && fs.existsSync(targetPath)) {
return targetPath;
}
},
load(id) {
if (!extension || !id.includes("src")) {
return;
}
if (id.includes("?")) {
return;
}
const targetPath = id.replace(/\.([^.]*?)$/, `.${extension}.$1`);
if (targetPath && fs.existsSync(targetPath)) {
return fs.readFileSync(targetPath, "utf-8");
}
},
};
}

13
src/App.vue Normal file
View File

@ -0,0 +1,13 @@
<template>
<a-config-provider>
<router-view v-slot="{ Component }">
<page-403 v-if="Math.random() > 0.99"></page-403>
<component v-else :is="Component"></component>
</router-view>
</a-config-provider>
</template>
<script setup lang="ts">
</script>
<style scoped></style>

2
src/api/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from "./instance";
export * from "./service";

17
src/api/instance/axios.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
import "axios";
import { IToastOptions } from "@/components";
declare module "axios" {
interface AxiosRequestConfig {
/**
* toast config
* @default false
*/
toast?: boolean | string | IToastOptions;
/**
* close toast(internal)
* @private
*/
_closeToast?: () => void;
}
}

View File

@ -0,0 +1,3 @@
export * from "./instance";
export * from "./useRequest";

View File

@ -0,0 +1,85 @@
import { Api } from "../service";
import { toast, IToastOptions } from "@/components";
import { useUserStore, store } from "@/store";
const userStore = useUserStore(store);
/**
* ,
*/
class Service extends Api<unknown> {
github = {
/**
*
*/
getRepoInfo: async () => {
const info: Record<string, any> = await this.request({
baseURL: "https://api.github.com",
path: "/repos/juetan/apptify-admin",
method: "GET",
});
return info;
},
};
}
/**
* api
* @see src/api/instance/instance.ts
*/
const api = new Service({
baseURL: import.meta.env.VITE_API_BASE_URL,
});
/**
*
*/
api.instance.interceptors.request.use(
(config) => {
if (userStore.accessToken) {
config.headers.Authorization = `Bearer ${userStore.accessToken}`;
}
if (config.toast) {
let options: IToastOptions = {};
if (typeof config.toast === "string") {
options = {
message: config.toast,
};
}
if (typeof config.toast === "object") {
options = config.toast;
}
config._closeToast = toast(options);
}
return config;
},
(error) => {
error.config?._closeToast?.();
return Promise.reject(error);
}
);
/**
*
*/
api.instance.interceptors.response.use(
(res) => {
res.config?._closeToast?.();
if (res.data?.code && res.data.code !== 2000) {
return Promise.reject(res);
}
return res;
},
(error) => {
error.config?._closeToast?.();
if (error.request) {
console.log("request error", error.request);
}
if (error.response) {
console.log("response error", error.response);
}
return Promise.reject(error);
}
);
export { api };

View File

@ -0,0 +1,179 @@
type PromiseFn = (...args: any[]) => Promise<any>;
type Options<T extends PromiseFn = PromiseFn> = {
/**
* loading
*/
toast?: boolean | string;
/**
*
*/
initialParams?: boolean | Parameters<T>;
/**
*
*/
initialData?: Partial<Awaited<ReturnType<T>>>;
/**
*
*/
retry?: number;
/**
* (ms)
*/
retryDelay?: number;
/**
* (ms)
*/
interval?: number;
/**
*
*/
onBefore?: (args: Parameters<T>) => void;
/**
*
*/
onSuccess?: (data: Awaited<ReturnType<T>>) => void;
/**
*
*/
onError?: (error: unknown) => void;
/**
*
*/
onFinally?: () => void;
};
type State<T extends PromiseFn = PromiseFn, D = Awaited<ReturnType<T>>> = {
/**
*
*/
data: D | undefined;
/**
*
*/
error: unknown;
/**
*
*/
loading: boolean;
/**
*
*/
send: (...args: Parameters<T>) => Promise<[unknown, undefined] | [undefined, D]>;
/**
*
*/
cancel: () => void;
};
const log = (...args: any[]) => {
if (process.env.NODE_ENV === "development") {
console.log(...args);
}
};
/**
*
* @see src/api/instance/useRequest.ts
*/
export function useRequest<T extends PromiseFn>(fn: T, options: Options<T> = {}) {
const {
initialParams,
retry,
retryDelay = 0,
interval,
initialData,
onBefore,
onSuccess,
onError,
onFinally,
} = options;
const state = reactive<State<T>>({
data: initialData,
error: null,
loading: false,
send: null,
cancel: null,
} as any);
const inner = {
canceled: false,
retryCount: 0,
retryTimer: 0 as any,
intervalTimer: 0 as any,
latestParams: (initialParams || []) as any,
clearAllTimer: () => {
inner.retryTimer && clearTimeout(inner.retryTimer);
inner.intervalTimer && clearTimeout(inner.intervalTimer);
},
};
const _send: any = async (...args: Parameters<T>) => {
let data;
let error;
inner.retryCount && log(`retry: ${inner.retryCount}`);
try {
state.loading = true;
onBefore?.(args);
const res = await fn(...args);
inner.retryCount = 0;
if (!inner.canceled) {
onSuccess?.(res.data);
data = res.data;
}
} catch (err) {
if (!inner.canceled) {
error = err;
onError?.(err);
if (retry && retry > 0 && inner.retryCount < retry) {
inner.retryCount++;
inner.retryTimer = setTimeout(() => {
_send(...args);
}, retryDelay);
}
}
} finally {
log("finally");
state.loading = false;
state.error = error;
if (!error) {
state.data = data;
}
if (!inner.canceled) {
onFinally?.();
if (!inner.retryCount && interval && interval > 0) {
inner.intervalTimer = setTimeout(() => {
_send(...args);
}, interval);
}
}
}
return [error, data];
};
state.cancel = () => {
inner.canceled = true;
inner.clearAllTimer();
};
state.send = (...args: Parameters<T>) => {
inner.canceled = false;
inner.retryCount = 0;
inner.latestParams = args;
inner.clearAllTimer();
return _send(...args);
};
onMounted(() => {
if (initialParams) {
state.send(...(Array.isArray(initialParams) ? initialParams : ([] as any)));
}
});
onUnmounted(() => {
state.cancel();
});
return state;
}

622
src/api/service/index.ts Normal file
View File

@ -0,0 +1,622 @@
/* eslint-disable */
/* tslint:disable */
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
export interface ApiResponse {
/** @format int32 */
code?: number;
type?: string;
message?: string;
}
export interface Category {
/** @format int64 */
id?: number;
name?: string;
}
export interface Pet {
/** @format int64 */
id?: number;
category?: Category;
/** @example "doggie" */
name: string;
photoUrls: string[];
tags?: Tag[];
/** pet status in the store */
status?: "available" | "pending" | "sold";
}
export interface Tag {
/** @format int64 */
id?: number;
name?: string;
}
export interface Order {
/** @format int64 */
id?: number;
/** @format int64 */
petId?: number;
/** @format int32 */
quantity?: number;
/** @format date-time */
shipDate?: string;
/** Order Status */
status?: "placed" | "approved" | "delivered";
complete?: boolean;
}
export interface User {
/** @format int64 */
id?: number;
username?: string;
firstName?: string;
lastName?: string;
email?: string;
password?: string;
phone?: string;
/**
* User Status
* @format int32
*/
userStatus?: number;
}
export interface FindPetsByStatusParams {
/** Status values that need to be considered for filter */
status: ("available" | "pending" | "sold")[];
}
export interface FindPetsByTagsParams {
/** Tags to filter by */
tags: string[];
}
export interface LoginUserParams {
/** The user name for login */
username: string;
/** The password for login in clear text */
password: string;
}
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, HeadersDefaults, ResponseType } from "axios";
export type QueryParamsType = Record<string | number, any>;
export interface FullRequestParams extends Omit<AxiosRequestConfig, "data" | "params" | "url" | "responseType"> {
/** set11 parameter to `true` for call `securityWorker` for this request */
secure?: boolean;
/** request path */
path: string;
/** content type of request body */
type?: ContentType;
/** query params */
query?: QueryParamsType;
/** format of response (i.e. response.json() -> format: "json") */
format?: ResponseType;
/** request body */
body?: unknown;
}
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;
export interface ApiConfig<SecurityDataType = unknown> extends Omit<AxiosRequestConfig, "data" | "cancelToken"> {
securityWorker?: (
securityData: SecurityDataType | null,
) => Promise<AxiosRequestConfig | void> | AxiosRequestConfig | void;
secure?: boolean;
format?: ResponseType;
}
export enum ContentType {
Json = "application/json",
FormData = "multipart/form-data",
UrlEncoded = "application/x-www-form-urlencoded",
Text = "text/plain",
}
export class HttpClient<SecurityDataType = unknown> {
public instance: AxiosInstance;
private securityData: SecurityDataType | null = null;
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
private secure?: boolean;
private format?: ResponseType;
constructor({ securityWorker, secure, format, ...axiosConfig }: ApiConfig<SecurityDataType> = {}) {
this.instance = axios.create({ ...axiosConfig, baseURL: axiosConfig.baseURL || "https://petstore.swagger.io/v2" });
this.secure = secure;
this.format = format;
this.securityWorker = securityWorker;
}
public setSecurityData = (data: SecurityDataType | null) => {
this.securityData = data;
};
protected mergeRequestParams(params1: AxiosRequestConfig, params2?: AxiosRequestConfig): AxiosRequestConfig {
const method = params1.method || (params2 && params2.method);
return {
...this.instance.defaults,
...params1,
...(params2 || {}),
headers: {
...((method && this.instance.defaults.headers[method.toLowerCase() as keyof HeadersDefaults]) || {}),
...(params1.headers || {}),
...((params2 && params2.headers) || {}),
},
};
}
protected stringifyFormItem(formItem: unknown) {
if (typeof formItem === "object" && formItem !== null) {
return JSON.stringify(formItem);
} else {
return `${formItem}`;
}
}
protected createFormData(input: Record<string, unknown>): FormData {
return Object.keys(input || {}).reduce((formData, key) => {
const property = input[key];
const propertyContent: any[] = property instanceof Array ? property : [property];
for (const formItem of propertyContent) {
const isFileType = formItem instanceof Blob || formItem instanceof File;
formData.append(key, isFileType ? formItem : this.stringifyFormItem(formItem));
}
return formData;
}, new FormData());
}
public request = async <T = any, _E = any>({
secure,
path,
type,
query,
format,
body,
...params
}: FullRequestParams): Promise<AxiosResponse<T>> => {
const secureParams =
((typeof secure === "boolean" ? secure : this.secure) &&
this.securityWorker &&
(await this.securityWorker(this.securityData))) ||
{};
const requestParams = this.mergeRequestParams(params, secureParams);
const responseFormat = format || this.format || undefined;
if (type === ContentType.FormData && body && body !== null && typeof body === "object") {
body = this.createFormData(body as Record<string, unknown>);
}
if (type === ContentType.Text && body && body !== null && typeof body !== "string") {
body = JSON.stringify(body);
}
return this.instance.request({
...requestParams,
headers: {
...(requestParams.headers || {}),
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
},
params: query,
responseType: responseFormat,
data: body,
url: path,
});
};
}
/**
* @title Swagger Petstore
* @version 1.0.6
* @license Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html)
* @termsOfService http://swagger.io/terms/
* @baseUrl https://petstore.swagger.io/v2
* @externalDocs http://swagger.io
* @contact <apiteam@swagger.io>
*
* This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.
*/
export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDataType> {
pet = {
/**
* No description
*
* @tags pet
* @name UploadFile
* @summary uploads an image
* @request POST:/pet/{petId}/uploadImage
* @secure
*/
uploadFile: (
petId: number,
data: {
/** Additional data to pass to server */
additionalMetadata?: string;
/** file to upload */
file?: File;
},
params: RequestParams = {},
) => {
return this.request<ApiResponse, any>({
path: `/pet/${petId}/uploadImage`,
method: "POST",
body: data,
secure: true,
type: ContentType.FormData,
format: "json",
...params,
});
},
/**
* No description
*
* @tags pet
* @name AddPet
* @summary Add a new pet to the store
* @request POST:/pet
* @secure
*/
addPet: (body: Pet, params: RequestParams = {}) => {
return this.request<any, void>({
path: `/pet`,
method: "POST",
body: body,
secure: true,
type: ContentType.Json,
...params,
});
},
/**
* No description
*
* @tags pet
* @name UpdatePet
* @summary Update an existing pet
* @request PUT:/pet
* @secure
*/
updatePet: (body: Pet, params: RequestParams = {}) => {
return this.request<any, void>({
path: `/pet`,
method: "PUT",
body: body,
secure: true,
type: ContentType.Json,
...params,
});
},
/**
* @description Multiple status values can be provided with comma separated strings
*
* @tags pet
* @name FindPetsByStatus
* @summary Finds Pets by status
* @request GET:/pet/findByStatus
* @secure
*/
findPetsByStatus: (query: FindPetsByStatusParams, params: RequestParams = {}) => {
return this.request<Pet[], void>({
path: `/pet/findByStatus`,
method: "GET",
query: query,
secure: true,
format: "json",
...params,
});
},
/**
* @description Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
*
* @tags pet
* @name FindPetsByTags
* @summary Finds Pets by tags
* @request GET:/pet/findByTags
* @deprecated
* @secure
*/
findPetsByTags: (query: FindPetsByTagsParams, params: RequestParams = {}) => {
return this.request<Pet[], void>({
path: `/pet/findByTags`,
method: "GET",
query: query,
secure: true,
format: "json",
...params,
});
},
/**
* @description Returns a single pet
*
* @tags pet
* @name GetPetById
* @summary Find pet by ID
* @request GET:/pet/{petId}
* @secure
*/
getPetById: (petId: number, params: RequestParams = {}) => {
return this.request<Pet, void>({
path: `/pet/${petId}`,
method: "GET",
secure: true,
format: "json",
...params,
});
},
/**
* No description
*
* @tags pet
* @name UpdatePetWithForm
* @summary Updates a pet in the store with form data
* @request POST:/pet/{petId}
* @secure
*/
updatePetWithForm: (
petId: number,
data: {
/** Updated name of the pet */
name?: string;
/** Updated status of the pet */
status?: string;
},
params: RequestParams = {},
) => {
return this.request<any, void>({
path: `/pet/${petId}`,
method: "POST",
body: data,
secure: true,
type: ContentType.FormData,
...params,
});
},
/**
* No description
*
* @tags pet
* @name DeletePet
* @summary Deletes a pet
* @request DELETE:/pet/{petId}
* @secure
*/
deletePet: (petId: number, params: RequestParams = {}) => {
return this.request<any, void>({
path: `/pet/${petId}`,
method: "DELETE",
secure: true,
...params,
});
},
};
store = {
/**
* No description
*
* @tags store
* @name PlaceOrder
* @summary Place an order for a pet
* @request POST:/store/order
*/
placeOrder: (body: Order, params: RequestParams = {}) => {
return this.request<Order, void>({
path: `/store/order`,
method: "POST",
body: body,
type: ContentType.Json,
format: "json",
...params,
});
},
/**
* @description For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions
*
* @tags store
* @name GetOrderById
* @summary Find purchase order by ID
* @request GET:/store/order/{orderId}
*/
getOrderById: (orderId: number, params: RequestParams = {}) => {
return this.request<Order, void>({
path: `/store/order/${orderId}`,
method: "GET",
format: "json",
...params,
});
},
/**
* @description For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors
*
* @tags store
* @name DeleteOrder
* @summary Delete purchase order by ID
* @request DELETE:/store/order/{orderId}
*/
deleteOrder: (orderId: number, params: RequestParams = {}) => {
return this.request<any, void>({
path: `/store/order/${orderId}`,
method: "DELETE",
...params,
});
},
/**
* @description Returns a map of status codes to quantities
*
* @tags store
* @name GetInventory
* @summary Returns pet inventories by status
* @request GET:/store/inventory
* @secure
*/
getInventory: (params: RequestParams = {}) => {
return this.request<Record<string, number>, any>({
path: `/store/inventory`,
method: "GET",
secure: true,
format: "json",
...params,
});
},
};
user = {
/**
* No description
*
* @tags user
* @name CreateUsersWithArrayInput
* @summary Creates list of users with given input array
* @request POST:/user/createWithArray
*/
createUsersWithArrayInput: (body: User[], params: RequestParams = {}) => {
return this.request<any, void>({
path: `/user/createWithArray`,
method: "POST",
body: body,
type: ContentType.Json,
...params,
});
},
/**
* No description
*
* @tags user
* @name CreateUsersWithListInput
* @summary Creates list of users with given input array
* @request POST:/user/createWithList
*/
createUsersWithListInput: (body: User[], params: RequestParams = {}) => {
return this.request<any, void>({
path: `/user/createWithList`,
method: "POST",
body: body,
type: ContentType.Json,
...params,
});
},
/**
* No description
*
* @tags user
* @name GetUserByName
* @summary Get user by user name
* @request GET:/user/{username}
*/
getUserByName: (username: string, params: RequestParams = {}) => {
return this.request<User, void>({
path: `/user/${username}`,
method: "GET",
format: "json",
...params,
});
},
/**
* @description This can only be done by the logged in user.
*
* @tags user
* @name UpdateUser
* @summary Updated user
* @request PUT:/user/{username}
*/
updateUser: (username: string, body: User, params: RequestParams = {}) => {
return this.request<any, void>({
path: `/user/${username}`,
method: "PUT",
body: body,
type: ContentType.Json,
...params,
});
},
/**
* @description This can only be done by the logged in user.
*
* @tags user
* @name DeleteUser
* @summary Delete user
* @request DELETE:/user/{username}
*/
deleteUser: (username: string, params: RequestParams = {}) => {
return this.request<any, void>({
path: `/user/${username}`,
method: "DELETE",
...params,
});
},
/**
* No description
*
* @tags user
* @name LoginUser
* @summary Logs user into the system
* @request GET:/user/login
*/
loginUser: (query: LoginUserParams, params: RequestParams = {}) => {
return this.request<string, void>({
path: `/user/login`,
method: "GET",
query: query,
format: "json",
...params,
});
},
/**
* No description
*
* @tags user
* @name LogoutUser
* @summary Logs out current logged in user session
* @request GET:/user/logout
*/
logoutUser: (params: RequestParams = {}) => {
return this.request<any, void>({
path: `/user/logout`,
method: "GET",
...params,
});
},
/**
* @description This can only be done by the logged in user.
*
* @tags user
* @name CreateUser
* @summary Create user
* @request POST:/user
*/
createUser: (body: User, params: RequestParams = {}) => {
return this.request<any, void>({
path: `/user`,
method: "POST",
body: body,
type: ContentType.Json,
...params,
});
},
};
}

245
src/assets/403.svg Normal file
View File

@ -0,0 +1,245 @@
<svg viewBox="0 0 400 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m70.66 196.612 3.04 13.52-13.23-4.13 10.19-9.39Z"
fill="#5578EC"
/>
<path
d="m73.7 210.132-3.04-13.52-10.19 9.39 13.23 4.13Z"
stroke="#3351E5"
stroke-width="2.126"
/>
<path
d="M65.84 203.277c-11.39-10.1-17.84-21.64-17.84-33.89 0-39.79 68.07-72.04 152.03-72.04 83.97 0 152.03 32.25 152.03 72.04 0 12.82-7.07 24.87-19.47 35.3"
stroke="#31333A"
stroke-width="2.126"
/>
<path
d="M125.452 252.76c-6.94-1.86-13.54-3.96-19.74-6.29-15.97-5.99-29.33-13.47-39.16-21.99-11.83-10.25-18.55-22.01-18.55-34.51 0-39.79 68.07-72.04 152.03-72.04 83.97 0 152.03 32.25 152.03 72.04 0 39.79-68.06 72.04-152.03 72.04-16.94 0-33.23-1.31-48.45-3.74"
stroke="#31333A"
stroke-width="2.126"
/>
<path
d="M238.644 239.084c-12.33 1.53-25.27 2.34-38.61 2.34-50.25 0-94.8-11.55-122.48-29.35"
stroke="#31333A"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M249.193 232.08c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5Z"
fill="#31333A"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M102.154 197.846v-82.26c0-4.11 6.14-7.44 13.72-7.44h148.67c7.58 0 13.73 3.33 13.73 7.44v82.26c0 4.11-6.15 7.44-13.73 7.44h-148.67c-7.58 0-13.72-3.33-13.72-7.44Z"
fill="#fff"
/>
<path
d="M102.154 115.586v82.26c0 4.11 6.14 7.44 13.72 7.44h148.67c7.58 0 13.73-3.33 13.73-7.44v-82.26c0-4.11-6.15-7.44-13.73-7.44h-148.67c-7.58 0-13.72 3.33-13.72 7.44Z"
stroke="#31333A"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M112.314 97.49h155.81c5.6 0 10.15 4.55 10.15 10.16v14.24h-176.12v-14.24c0-5.61 4.55-10.16 10.16-10.16Z"
fill="#5578EC"
/>
<path
d="M268.124 97.49h-155.81c-5.61 0-10.16 4.55-10.16 10.16v14.24h176.12v-14.24c0-5.61-4.55-10.16-10.15-10.16Z"
stroke="#3351E5"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M265.712 195.453c1.24 5.51 6.72 8.98 12.23 7.75l16.36-3.68c5.51-1.23 8.98-6.72 7.74-12.22l-2.79-12.47c-2.19-9.76-11.92-15.92-21.69-13.73l-.92.21c-9.76 2.19-15.92 11.92-13.73 21.68l2.8 12.46Zm28.167-6.323-2.8-12.46c-1.18-5.25-6.42-8.57-11.68-7.39l-.92.2c-5.25 1.18-8.57 6.42-7.39 11.68l2.8 12.46c.22.98 1.23 1.63 2.22 1.41l16.36-3.68c.98-.22 1.63-1.23 1.41-2.22Z"
fill="#686A6D"
/>
<path
d="M265.712 195.453c1.24 5.51 6.72 8.98 12.23 7.75l16.36-3.68c5.51-1.23 8.98-6.72 7.74-12.22l-2.79-12.47c-2.19-9.76-11.92-15.92-21.69-13.73l-.92.21c-9.76 2.19-15.92 11.92-13.73 21.68l2.8 12.46Zm28.167-6.323-2.8-12.46c-1.18-5.25-6.42-8.57-11.68-7.39l-.92.2c-5.25 1.18-8.57 6.42-7.39 11.68l2.8 12.46c.22.98 1.23 1.63 2.22 1.41l16.36-3.68c.98-.22 1.63-1.23 1.41-2.22Z"
stroke="#31333A"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m285.415 228.924 13.124-2.947c10.888-2.444 17.729-13.245 15.284-24.134l-3.446-15.348a4.468 4.468 0 0 0-5.34-3.382l-43.82 9.838a4.468 4.468 0 0 0-3.382 5.34l3.446 15.348c2.445 10.889 13.245 17.729 24.134 15.285Z"
fill="#5578EC"
/>
<path
d="m310.672 182.937-52.546 11.797 8.385 37.348 52.546-11.797-8.385-37.348Z"
stroke="#3351E5"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M286.765 194.724c-3.82 0-6.92 3.1-6.92 6.92s3.1 6.92 6.92 6.92 6.92-3.1 6.92-6.92-3.1-6.92-6.92-6.92Z"
fill="#31333A"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m288.891 203.001 3.21 10.45c.32 1.41-.57 2.82-1.98 3.13-1.42.32-2.82-.57-3.14-1.98l-1.56-10.82 3.47-.78Z"
fill="#31333A"
/>
<path
d="m144.481 167.653-1.15 2.01c-.34.58-.68 1.2-.64 1.87.05.67.67 1.34 1.32 1.16"
stroke="#31333A"
stroke-width=".425"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M186.839 142.49h67.25v-9.57h-67.25v9.57Z"
fill="#AFAEB4"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M186.839 155.631h67.25v-6.3h-67.25v6.3ZM186.839 168.529h67.25v-6.3h-67.25v6.3ZM186.839 181.426h47.55v-6.3h-47.55v6.3Z"
fill="#D7D9DD"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M201.174 116.591h-21.91c-2.79 0-5.05-2.26-5.05-5.05v-15.43h32v15.43c0 2.79-2.26 5.05-5.04 5.05Z"
fill="#3351E5"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M184.268 75.95h11.75c.76 0 1.5-.3 2.04-.84s.84-1.28.84-2.04v-6.19c0-.76-.3-1.5-.84-2.04s-1.28-.84-2.04-.84h-11.75c-.76 0-1.5.3-2.04.84s-.84 1.28-.84 2.04v6.19c0 .76.3 1.5.84 2.04s1.28.84 2.04.84Z"
fill="#686A6D"
/>
<path
d="M199.958 65.064h-17.507v9.822h17.507v-9.822Z"
stroke="#31333A"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M181.337 111.919h17.62a2.6 2.6 0 0 0 2.59-2.6v-33.31a2.6 2.6 0 0 0-2.59-2.6h-17.62a2.595 2.595 0 0 0-2.6 2.6v33.31a2.595 2.595 0 0 0 2.6 2.6Z"
fill="#DCDDDD"
/>
<path
d="M202.613 74.473H179.8v36.381h22.813V74.473Z"
stroke="#31333A"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M143.589 126.485c-17.43 0-31.56 14.13-31.56 31.56 0 17.42 14.13 31.55 31.56 31.55 17.42 0 31.55-14.13 31.55-31.55 0-17.43-14.13-31.56-31.55-31.56Z"
fill="#D7D9DD"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M183.536 100.461a6.61 6.61 0 0 1 6.61-6.61v13.22a6.61 6.61 0 0 1-6.61-6.61Z"
fill="#5578EC"
/>
<path
d="M190.146 93.85a6.61 6.61 0 0 0 0 13.221v-13.22Z"
stroke="#3351E5"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M190.144 93.85a6.61 6.61 0 0 1 0 13.221v-13.22Z"
fill="#5578EC"
/>
<path
d="M196.754 100.461a6.61 6.61 0 0 0-6.61-6.61v13.22a6.61 6.61 0 0 0 6.61-6.61Z"
stroke="#3351E5"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M160.252 171.477c-3.64 4.61-9.57 7.62-16.26 7.62-1.78 0-3.51-.22-5.16-.62-.26-.06-.5-.13-.75-.2-.02 0-.04-.01-.06-.01a21.012 21.012 0 0 1-11.09-7.89c-.04-.05-.07-.1-.11-.16-1.84-2.78-2.9-6.06-2.9-9.55 0-1.21.13-2.39.37-3.54 2.45.71 5 1.07 7.56 1.06 5.54 0 10.57-1.62 14.3-4.25 1.92-1.32 3.54-3.03 4.76-5.02.01-.01.01-.02.02-.04.21-.31.63-.38.94-.17 7.38 5.02 10.73 14.17 8.38 22.77Z"
fill="#fff"
/>
<path
d="M143.992 179.097c6.69 0 12.62-3.01 16.26-7.62 2.35-8.6-1-17.75-8.38-22.77a.674.674 0 0 0-.94.17c-.01.02-.01.03-.02.04-1.22 1.99-2.84 3.7-4.76 5.02-3.73 2.63-8.76 4.25-14.3 4.25-2.56.01-5.11-.35-7.56-1.06-.24 1.15-.37 2.33-.37 3.54 0 3.49 1.06 6.77 2.9 9.55.04.06.07.11.11.16 2.68 3.73 6.56 6.55 11.09 7.89.02 0 .04.01.06.01.25.07.49.14.75.2 1.65.4 3.38.62 5.16.62Z"
stroke="#31333A"
stroke-width="1.417"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M125.008 149.562c4.87-10.06 16.71-14.61 27.06-10.41 7.9 3.2 13.08 10.87 13.1 19.4 0 4.95-1.76 9.74-4.98 13.51 2.42-8.46-.72-17.51-7.85-22.66-.02-.02-.04-.03-.06-.04-.55-.36-1.28-.2-1.64.36-1.19 1.8-2.71 3.36-4.49 4.58-3.73 2.63-8.76 4.25-14.3 4.25-1.03 0-2.07-.06-3.1-.18-.95 2.63-1.44 5.4-1.43 8.2-.01 1.77.19 3.53.57 5.25a20.777 20.777 0 0 1-5.13-14.84c-.49-.17-.98-.36-1.45-.57a25.9 25.9 0 0 1-1.67-.82c-.47-.26-.84-.67-1.05-1.16-.55-1.24.02-2.69 1.26-3.23 1.66-.74 3.39-1.29 5.16-1.64Z"
fill="#31333A"
/>
<path
d="M152.068 139.152c-10.35-4.2-22.19.35-27.06 10.41-1.77.35-3.5.9-5.16 1.64a2.445 2.445 0 0 0-1.26 3.23c.21.49.58.9 1.05 1.16.54.29 1.09.56 1.67.82.47.21.96.4 1.45.57a20.777 20.777 0 0 0 5.13 14.84 23.72 23.72 0 0 1-.57-5.25c-.01-2.8.48-5.57 1.43-8.2 1.03.12 2.07.18 3.1.18 5.54 0 10.57-1.62 14.3-4.25 1.78-1.22 3.3-2.78 4.49-4.58.36-.56 1.09-.72 1.64-.36.02.01.04.02.06.04a20.861 20.861 0 0 1 7.85 22.66c3.22-3.77 4.98-8.56 4.98-13.51-.02-8.53-5.2-16.2-13.1-19.4Z"
stroke="#31333A"
stroke-width=".709"
/>
<path
d="M138.049 166.896c0-1-.8-1.8-1.79-1.8-1 0-1.8.8-1.8 1.8M153.378 166.896c0-1-.8-1.8-1.79-1.8-1 0-1.8.8-1.8 1.8"
stroke="#31333A"
stroke-width="1.417"
/>
<path
d="M143.992 142.231c-11.08 0-20.07 8.25-20.07 18.43s8.99 18.43 20.07 18.43c11.09 0 20.08-8.25 20.08-18.43s-8.99-18.43-20.08-18.43Z"
stroke="#31333A"
stroke-width="1.417"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M123.049 154.826a3.726 3.726 0 0 0 0 7.45 3.73 3.73 0 0 0 3.73-3.73c0-2.05-1.67-3.72-3.73-3.72Z"
fill="#31333A"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M143.995 175.298c1.56 0 2.83-1.1 2.83-2.47h-5.66c0 1.37 1.27 2.47 2.83 2.47Z"
fill="#fff"
/>
<path
d="M143.995 175.298c1.56 0 2.83-1.1 2.83-2.47h-5.66c0 1.37 1.27 2.47 2.83 2.47Z"
stroke="#31333A"
stroke-width="1.5"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m316.12 134.275-7.16 1.19c-1.12.19-1.97-1-1.44-2l3.42-6.41c.21-.4.21-.87 0-1.28l-3.34-6.44c-.52-1.01.34-2.18 1.46-1.99l7.15 1.27c.44.08.9-.07 1.22-.39l5.09-5.17c.8-.81 2.18-.35 2.34.78l1.01 7.18c.06.45.34.84.74 1.04l6.49 3.25c1.02.51 1.01 1.97-.01 2.47l-6.52 3.17c-.41.2-.7.59-.76 1.03l-1.09 7.18c-.17 1.13-1.55 1.57-2.35.75l-5.03-5.22c-.32-.33-.77-.48-1.22-.41Z"
fill="#fff"
/>
<path
d="m308.96 135.465 7.16-1.19c.45-.07.9.08 1.22.41l5.03 5.22c.8.82 2.18.38 2.35-.75l1.09-7.18c.06-.44.35-.83.76-1.03l6.52-3.17c1.02-.5 1.03-1.96.01-2.47l-6.49-3.25c-.4-.2-.68-.59-.74-1.04l-1.01-7.18c-.16-1.13-1.54-1.59-2.34-.78l-5.09 5.17c-.32.32-.78.47-1.22.39l-7.15-1.27c-1.12-.19-1.98.98-1.46 1.99l3.34 6.44c.21.41.21.88 0 1.28l-3.42 6.41c-.53 1 .32 2.19 1.44 2Z"
stroke="#31333A"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M138.052 250.496c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5Z"
fill="#fff"
/>
<path
d="M138.052 250.496c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5Z"
stroke="#31333A"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M153.384 131.631c-3.95 0-7.15 3.2-7.15 7.15 0 3.94 3.2 7.14 7.15 7.14 3.94 0 7.14-3.2 7.14-7.14 0-3.95-3.2-7.15-7.14-7.15Z"
fill="#31333A"
/>
<path
d="m81.614 106.969 8.38 5.73M92.944 89.777l6.25 8M112.25 79.528l.51 10.14"
stroke="#31333A"
stroke-width="2"
/>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

239
src/assets/404.svg Normal file
View File

@ -0,0 +1,239 @@
<svg
viewBox="0 0 400 300"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-[280px]"
>
<path
d="M55.92 169.384c-1.7-2.38-3.14-4.82-4.3-7.31-2.37-5.06-3.62-10.31-3.62-15.7 0-36.68 57.86-66.96 132.69-71.46M297.606 91.128c33.29 13.22 54.46 33.06 54.46 55.25 0 10.3-4.57 20.1-12.79 28.97M196.192 74.356c1.28-.01 2.56-.02 3.84-.02 24.71 0 48.05 2.79 68.67 7.75"
stroke="#31333A"
stroke-width="2.126"
/>
<path
d="M315.532 118.444c12.36 7.6 21.59 16.4 26.77 25.97M201.24 110.279c83.97 0 152.03 32.25 152.03 72.04 0 39.79-68.06 72.04-152.03 72.04-35.63 0-68.41-5.81-94.32-15.54-15.97-5.99-29.33-13.47-39.16-21.99-11.83-10.25-18.55-22.01-18.55-34.51 0-22.37 21.52-42.36 55.28-55.57 2.1-.83 4.25-1.62 6.44-2.39"
stroke="#31333A"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M126.986 201.18h128.76c6.56 0 11.88-5.33 11.88-11.89V57.88c0-6.56-5.32-11.88-11.88-11.88h-128.76c-3.15 0-6.18 1.25-8.41 3.48a11.899 11.899 0 0 0-3.48 8.41v131.4c0 3.15 1.25 6.18 3.48 8.41 2.23 2.23 5.26 3.48 8.41 3.48Z"
fill="#DCDDDD"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M126.975 46h128.77c6.56 0 11.89 5.32 11.89 11.89v9.24h-152.54v-9.24c0-6.57 5.32-11.89 11.88-11.89Z"
fill="#999"
/>
<path
d="M136.064 99.007c12.54-2.76 26.01-4.74 40.13-5.79M64.414 131.78c9.47-8.83 22.67-16.61 38.62-22.88"
stroke="#31333A"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M62.193 127.069c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5Z"
fill="#31333A"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M147.693 73.57c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5Z"
fill="#fff"
/>
<path
d="M147.693 73.57c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5Z"
stroke="#31333A"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m89.926 158.793 6.93 12h-13.86l6.93-12Z"
fill="#5578EC"
/>
<path
d="m96.856 170.793-6.93-12-6.93 12h13.86Z"
stroke="#3351E5"
stroke-width="2.126"
/>
<path
d="M259.143 237.095c-15.48 2.52-41.5 4.94-58.78 4.94-18.54 0-39.27-2.06-55.7-4.94M112.663 228.745c-2.15-.73-4.25-1.48-6.32-2.25-.42-.16-.84-.32-1.25-.48-15.42-5.9-28.34-13.21-37.91-21.51M332.263 202.087c-8.42 7.24-19.07 13.93-31.48 19.79-1.75.83-3.54 1.64-5.36 2.43"
stroke="#31333A"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m137.509 224.27-2.07 4.53c-.13.28-.11.61.05.88l2.56 4.28c.4.67-.14 1.51-.92 1.42l-4.95-.57a.94.94 0 0 0-.82.32l-3.28 3.76c-.51.59-1.48.33-1.63-.44l-.99-4.88a.945.945 0 0 0-.56-.68l-4.59-1.96a.944.944 0 0 1-.09-1.69l4.34-2.45a.97.97 0 0 0 .48-.74l.44-4.97c.07-.78 1-1.14 1.58-.61l3.67 3.37c.23.21.55.3.85.23l4.86-1.12c.77-.17 1.4.6 1.07 1.32Z"
fill="#fff"
/>
<path
d="m135.439 228.8 2.07-4.53a.945.945 0 0 0-1.07-1.32l-4.86 1.12a.95.95 0 0 1-.85-.23l-3.67-3.37a.944.944 0 0 0-1.58.61l-.44 4.97a.97.97 0 0 1-.48.74l-4.34 2.45a.944.944 0 0 0 .09 1.69l4.59 1.96c.29.12.49.37.56.68l.99 4.88c.15.77 1.12 1.03 1.63.44l3.28-3.76c.2-.24.51-.35.82-.32l4.95.57c.78.09 1.32-.75.92-1.42l-2.56-4.28a.947.947 0 0 1-.05-.88Z"
stroke="#31333A"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m335.832 196.284 8.478-1.554-1.554-8.479-8.479 1.555 1.555 8.478Z"
fill="#5578EC"
/>
<path
d="m343.99 187.106-8.475 1.553 1.17 6.384 8.475-1.554-1.17-6.383Z"
stroke="#3351E5"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M136.863 209.542h128.76c6.56 0 11.88-5.33 11.88-11.89V66.242c0-6.56-5.32-11.88-11.88-11.88h-128.76c-3.15 0-6.18 1.25-8.41 3.48a11.899 11.899 0 0 0-3.48 8.41v131.4c0 3.15 1.25 6.18 3.48 8.41 2.23 2.23 5.26 3.48 8.41 3.48Z"
fill="#fff"
/>
<path
d="M278.57 55.425H126.036v153.05H278.57V55.425Z"
stroke="#31333A"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M136.853 54.362h128.77c6.56 0 11.88 5.32 11.88 11.89v9.24h-152.53v-9.24c0-6.57 5.32-11.89 11.88-11.89Z"
fill="#5578EC"
/>
<path
d="M265.623 54.362h-128.77c-6.56 0-11.88 5.32-11.88 11.89v9.24h152.53v-9.24c0-6.57-5.32-11.89-11.88-11.89Z"
stroke="#3351E5"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M132.492 64.922a5.695 5.695 0 1 1 11.39.01 5.695 5.695 0 0 1-11.39-.01ZM148.833 64.922a5.695 5.695 0 1 1 11.39.01 5.695 5.695 0 0 1-11.39-.01ZM165.176 64.922a5.695 5.695 0 1 1 11.39.011 5.695 5.695 0 0 1-11.39-.01Z"
fill="#fff"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M145.772 162.308h110.93v-6.85h-110.93v6.85ZM145.772 173.204h110.93v-6.85h-110.93v6.85ZM145.772 184.1h62.4v-6.85h-62.4v6.85Z"
fill="#C6C6C5"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M212.883 107.575c.27-.3.74-.32 1.05-.05a23.046 23.046 0 0 1 5.85 26.13c-4.66 4.52-11.57 6.95-18.89 5.98-1.96-.26-3.82-.75-5.57-1.43-.26-.11-.53-.21-.79-.33-.02 0-.04-.01-.06-.02a23.175 23.175 0 0 1-10.98-10.25c-.04-.06-.06-.12-.1-.18-1.61-3.32-2.29-7.05-1.79-10.88.18-1.32.49-2.6.93-3.81 2.58 1.13 5.31 1.89 8.11 2.25 6.06.81 11.8-.23 16.26-2.56a18.35 18.35 0 0 0 5.94-4.81l.04-.04Z"
fill="#fff"
/>
<path
d="M213.933 107.525a.752.752 0 0 0-1.05.05l-.04.04c-1.62 2-3.64 3.64-5.94 4.81-4.46 2.33-10.2 3.37-16.26 2.56-2.8-.36-5.53-1.12-8.11-2.25a18.45 18.45 0 0 0-.93 3.81c-.5 3.83.18 7.56 1.79 10.88.04.06.06.12.1.18 2.39 4.48 6.22 8.12 10.98 10.25.02.01.04.02.06.02.26.12.53.22.79.33 1.75.68 3.61 1.17 5.57 1.43 7.32.97 14.23-1.46 18.89-5.98a23.046 23.046 0 0 0-5.85-26.13Z"
stroke="#31333A"
stroke-width=".992"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M214.277 108.303c-.55-.47-1.38-.4-1.84.15-1.57 1.8-3.46 3.28-5.58 4.37-4.47 2.34-10.21 3.37-16.27 2.56-1.13-.15-2.25-.36-3.36-.64a26.29 26.29 0 0 0-2.77 8.76c-.26 1.93-.3 3.89-.13 5.83a22.908 22.908 0 0 1-3.65-15.77c.05-.41.12-.81.19-1.21-.51-.27-1.02-.55-1.5-.84-.59-.36-1.16-.74-1.71-1.14-.48-.35-.82-.85-.98-1.42a2.71 2.71 0 0 1 1.85-3.36c1.92-.56 3.89-.91 5.89-1.04 6.78-10.3 20.4-13.55 31.11-7.45a23.125 23.125 0 0 1 11.51 23.13c-.71 5.42-3.34 10.4-7.4 14.06 3.86-8.9 1.75-19.27-5.3-25.94a.265.265 0 0 0-.06-.05Z"
fill="#31333A"
/>
<path
d="M212.437 108.453c.46-.55 1.29-.62 1.84-.15.02.01.04.03.06.05 7.05 6.67 9.16 17.04 5.3 25.94 4.06-3.66 6.69-8.64 7.4-14.06a23.125 23.125 0 0 0-11.51-23.13c-10.71-6.1-24.33-2.85-31.11 7.45-2 .13-3.97.48-5.89 1.04a2.71 2.71 0 0 0-1.85 3.36c.16.57.5 1.07.98 1.42.55.4 1.12.78 1.71 1.14.48.29.99.57 1.5.84-.07.4-.14.8-.19 1.21-.74 5.52.55 11.13 3.65 15.77-.17-1.94-.13-3.9.13-5.83.4-3.06 1.34-6.02 2.77-8.76 1.11.28 2.23.49 3.36.64 6.06.81 11.8-.22 16.27-2.56 2.12-1.09 4.01-2.57 5.58-4.37Z"
stroke="#31333A"
/>
<path
d="m194.061 126.161 3.34-3.6M193.931 122.69l3.6 3.34M208.133 126.305l3.34-3.6M208.002 122.835l3.6 3.34M181.607 116.548c-1.479 11.132 7.154 21.47 19.288 23.082 12.123 1.611 23.157-6.112 24.636-17.245 1.481-11.142-7.153-21.479-19.276-23.09-12.133-1.613-23.167 6.111-24.648 17.253Z"
stroke="#31333A"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m169.931 103.186-9.09-.09v.07l-16.32 21.59.49 4.83 15.56.16-.07 7.12 9.08.09.07-7.12 3.96.04.06-6.07-3.95-.04.21-20.58Zm-9.192 9.971-.16-.02-1.01 1.73-7.24 8.72 8.31.08.1-10.51ZM247.096 136.867l9.09.09.07-7.12 3.95.04.07-6.07-3.95-.04.21-20.58-9.09-.09v.07l-16.32 21.59.49 4.83 15.56.16-.08 7.12Zm.248-23.7-.16-.03-1.01 1.73-7.24 8.72 8.3.08.11-10.5Z"
fill="#31333A"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M264.771 220.051c-5.87 3.29-13 4.45-20.09 2.69-14.71-3.64-23.72-18.37-20.12-32.9 3.6-14.54 18.43-23.37 33.14-19.73 14.71 3.64 23.71 18.37 20.12 32.9-1.03 4.18-3 7.88-5.61 10.95l16.29 18.45.81-3.28 2.35 1.24-1.08 4.39-7.35 5.5-18.46-20.21Z"
fill="#3351E5"
/>
<path
d="M244.681 222.741c7.09 1.76 14.22.6 20.09-2.69l18.46 20.21 7.35-5.5 1.08-4.39-2.35-1.24-.81 3.28-16.29-18.45c2.61-3.07 4.58-6.77 5.61-10.95 3.59-14.53-5.41-29.26-20.12-32.9-14.71-3.64-29.54 5.19-33.14 19.73-3.6 14.53 5.41 29.26 20.12 32.9Z"
stroke="#3351E5"
stroke-width=".992"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M277.111 200.513c-.8 3.23-2.25 6.15-4.17 8.66l18.72 21.2-7.34 5.5-18.61-20.37c-5.79 3.37-13.02 4.57-20.26 2.78-14.19-3.51-23.02-17.16-19.72-30.48 3.29-13.33 17.47-21.28 31.66-17.77 14.18 3.51 23.01 17.16 19.72 30.48Z"
fill="#5578EC"
/>
<path
d="M272.941 209.173c1.92-2.51 3.37-5.43 4.17-8.66 3.29-13.32-5.54-26.97-19.72-30.48-14.19-3.51-28.37 4.44-31.66 17.77-3.3 13.32 5.53 26.97 19.72 30.48 7.24 1.79 14.47.59 20.26-2.78l18.61 20.37 7.34-5.5-18.72-21.2Z"
stroke="#3351E5"
stroke-width=".992"
/>
<path
d="M258.139 169.99c-.06-.01-.11-.03-.16-.04-11.16-2.76-22.31 1.57-28.17 10.03a26.8 26.8 0 0 0-4.66 9.77c-3.6 14.54 5.41 29.27 20.12 32.91 7.09 1.75 14.22.6 20.09-2.69l18.46 20.21 7.35-5.5 1.08-4.39-17.21-19.48c1.5-2.37 2.66-5.01 3.37-7.88 3.6-14.53-5.41-29.27-20.12-32.9a.833.833 0 0 0-.15-.04Z"
stroke="#31333A"
stroke-width="2"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M232.624 189.502c-2.44 9.862 3.998 19.943 14.375 22.51 10.387 2.57 20.782-3.347 23.222-13.21 2.44-9.862-4-19.934-14.387-22.503-10.377-2.567-20.77 3.34-23.21 13.203Z"
fill="#fff"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M235.672 205.485c-3.227-4.276-4.582-9.782-3.237-15.222 2.44-9.863 12.833-15.77 23.21-13.203 6.397 1.582 11.297 6.01 13.547 11.482-2.625-3.478-6.491-6.141-11.152-7.294-10.378-2.567-20.77 3.34-23.21 13.203-.938 3.792-.564 7.617.842 11.034Z"
fill="#E7E7E7"
/>
<path
d="M232.624 189.502c-2.44 9.862 3.998 19.943 14.375 22.51 10.387 2.57 20.782-3.347 23.222-13.21 2.44-9.862-4-19.934-14.387-22.503-10.377-2.567-20.77 3.34-23.21 13.203Z"
stroke="#31333A"
stroke-width="2"
/>
<path
d="M250.303 204.983s-4.05.08-6.64-1.6c-2.59-1.67-2.95-3.98-2.95-3.98"
stroke="#31333A"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m287.587 249.187-16.93-18.84c-1.36-1.52-.21-4.8 2.59-7.31l3.38-3.03c2.79-2.52 6.17-3.32 7.54-1.8l16.93 18.84c1.37 1.53.21 4.8-2.59 7.31l-3.38 3.04c-2.79 2.51-6.17 3.31-7.54 1.79Z"
fill="#DCDDDD"
/>
<path
d="m270.657 230.347 16.93 18.84c1.37 1.52 4.75.72 7.54-1.79l3.38-3.04c2.8-2.51 3.96-5.78 2.59-7.31l-16.93-18.84c-1.37-1.52-4.75-.72-7.54 1.8l-3.38 3.03c-2.8 2.51-3.95 5.79-2.59 7.31Z"
stroke="#31333A"
stroke-width="2.126"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m298.505 244.358-3.37 3.03c-2.8 2.52-6.18 3.32-7.54 1.8l-1.38-1.54 13.51-12.13 1.37 1.53c1.37 1.52.21 4.8-2.59 7.31Z"
fill="#0D1D38"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m287.587 249.186-16.93-18.84c-1.36-1.52-.21-4.8 2.59-7.31l.85-.76 21.88 24.36-.85.76c-2.79 2.51-6.17 3.31-7.54 1.79Z"
fill="#DCDDDD"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M281.843 220.296c.82-.74 1.81-.97 2.21-.53l15.48 17.24c.4.44.06 1.4-.76 2.13-.81.73-1.8.97-2.2.52l-15.48-17.23c-.4-.44-.06-1.4.75-2.13Z"
fill="#fff"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M180.431 108.9a3.55 3.55 0 1 0 .002 7.102 3.55 3.55 0 0 0-.002-7.102Z"
fill="#31333A"
/>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

1
src/assets/td.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 79 KiB

167
src/assets/wave.svg Normal file
View File

@ -0,0 +1,167 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="1920" height="960" preserveAspectRatio="none" viewBox="0 0 1920 960">
<g mask="url(&quot;#SvgjsMask1032&quot;)" fill="none">
<use xlink:href="#SvgjsSymbol1039" x="0" y="0"></use>
<use xlink:href="#SvgjsSymbol1039" x="0" y="720"></use>
<use xlink:href="#SvgjsSymbol1039" x="720" y="0"></use>
<use xlink:href="#SvgjsSymbol1039" x="720" y="720"></use>
<use xlink:href="#SvgjsSymbol1039" x="1440" y="0"></use>
<use xlink:href="#SvgjsSymbol1039" x="1440" y="720"></use>
</g>
<defs>
<mask id="SvgjsMask1032">
<rect width="1920" height="960" fill="#ffffff"></rect>
</mask>
<path d="M-1 0 a1 1 0 1 0 2 0 a1 1 0 1 0 -2 0z" id="SvgjsPath1037"></path>
<path d="M-3 0 a3 3 0 1 0 6 0 a3 3 0 1 0 -6 0z" id="SvgjsPath1036"></path>
<path d="M-5 0 a5 5 0 1 0 10 0 a5 5 0 1 0 -10 0z" id="SvgjsPath1033"></path>
<path d="M2 -2 L-2 2z" id="SvgjsPath1034"></path>
<path d="M6 -6 L-6 6z" id="SvgjsPath1035"></path>
<path d="M30 -30 L-30 30z" id="SvgjsPath1038"></path>
</defs>
<symbol id="SvgjsSymbol1039">
<use xlink:href="#SvgjsPath1033" x="30" y="30" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="30" y="90" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="30" y="150" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="30" y="210" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1036" x="30" y="270" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="30" y="330" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="30" y="390" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1037" x="30" y="450" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1036" x="30" y="510" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="30" y="570" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="30" y="630" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1036" x="30" y="690" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1036" x="90" y="30" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="90" y="90" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="90" y="150" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="90" y="210" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="90" y="270" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="90" y="330" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="90" y="390" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="90" y="450" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="90" y="510" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="90" y="570" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="90" y="630" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1036" x="90" y="690" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="150" y="30" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="150" y="90" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="150" y="150" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="150" y="210" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="150" y="270" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="150" y="330" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="150" y="390" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="150" y="450" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="150" y="510" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1037" x="150" y="570" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="150" y="630" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1037" x="150" y="690" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="210" y="30" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="210" y="90" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1036" x="210" y="150" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1037" x="210" y="210" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="210" y="270" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="210" y="330" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="210" y="390" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="210" y="450" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1037" x="210" y="510" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="210" y="570" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="210" y="630" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1037" x="210" y="690" stroke="#1c538e"></use>
<!-- <use xlink:href="#SvgjsPath1038" x="270" y="30" stroke="#1c538e" ></use> -->
<use xlink:href="#SvgjsPath1035" x="270" y="90" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="270" y="150" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1037" x="270" y="210" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1037" x="270" y="270" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="270" y="330" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="270" y="390" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="270" y="450" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="270" y="510" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="270" y="570" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="270" y="630" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="270" y="690" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1037" x="330" y="30" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="330" y="90" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="330" y="150" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1037" x="330" y="210" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1037" x="330" y="270" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1036" x="330" y="330" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="330" y="390" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1036" x="330" y="450" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="330" y="510" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="330" y="570" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="330" y="630" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="330" y="690" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="390" y="30" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="390" y="90" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="390" y="150" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="390" y="210" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="390" y="270" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="390" y="330" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="390" y="390" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="390" y="450" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="390" y="510" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="390" y="570" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="390" y="630" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="390" y="690" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="450" y="30" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="450" y="90" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="450" y="150" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="450" y="210" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1037" x="450" y="270" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="450" y="330" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="450" y="390" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="450" y="450" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="450" y="510" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="450" y="570" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="450" y="630" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="450" y="690" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="510" y="30" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="510" y="90" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="510" y="150" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="510" y="210" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="510" y="270" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1036" x="510" y="330" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="510" y="390" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="510" y="450" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="510" y="510" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="510" y="570" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="510" y="630" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="510" y="690" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="570" y="30" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="570" y="90" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="570" y="150" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="570" y="210" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="570" y="270" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1036" x="570" y="330" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="570" y="390" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="570" y="450" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="570" y="510" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1038" x="570" y="570" stroke="#1c538e" stroke-width="3"></use>
<use xlink:href="#SvgjsPath1037" x="570" y="630" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="570" y="690" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="630" y="30" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="630" y="90" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="630" y="150" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="630" y="210" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="630" y="270" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1037" x="630" y="330" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1037" x="630" y="390" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1036" x="630" y="450" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="630" y="510" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="630" y="570" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1038" x="630" y="630" stroke="#1c538e" stroke-width="3"></use>
<use xlink:href="#SvgjsPath1035" x="630" y="690" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="690" y="30" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="690" y="90" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="690" y="150" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="690" y="210" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1038" x="690" y="270" stroke="#1c538e" stroke-width="3"></use>
<use xlink:href="#SvgjsPath1035" x="690" y="330" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="690" y="390" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1033" x="690" y="450" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1036" x="690" y="510" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1034" x="690" y="570" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1036" x="690" y="630" stroke="#1c538e"></use>
<use xlink:href="#SvgjsPath1035" x="690" y="690" stroke="#1c538e"></use>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,43 @@
<template>
<div class="mt-4 mx-5">
<a-breadcrumb>
<a-breadcrumb-item>
<i class="icon-park-outline-all-application text-gray-400"></i>
</a-breadcrumb-item>
<a-breadcrumb-item v-for="(item, index) in items" :key="item">
<span
:class="
index !== items.length - 1 ? 'text-gray-400' : 'text-gray-500'
"
>
{{ item }}
</span>
</a-breadcrumb-item>
</a-breadcrumb>
</div>
</template>
<script setup lang="ts">
import { MenuItem, menus } from "@/router";
const route = useRoute();
const getPaths = (items: MenuItem[], path: string, paths: string[] = []) => {
const item = items.find((i) => {
if (i.id.endsWith("index")) {
return i.id.includes(path);
}
return path.includes(i.id) && path.includes(i.path);
});
if (item) {
paths.push(item.title as string);
if (item.children?.length) {
getPaths(item.children, path, paths);
}
}
return paths;
};
const items = computed(() => getPaths(menus, route.path));
</script>
<style scoped></style>

View File

@ -0,0 +1,14 @@
<template>
<div>
<BreadCrumb></BreadCrumb>
<div class="mx-4 mt-4 p-4 bg-white">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import BreadCrumb from "./bread-crumb.vue";
</script>
<style scoped></style>

View File

@ -0,0 +1,42 @@
<template>
<div class="w-full h-full flex justify-center items-center p-4">
<div class="flex gap-2 items-center">
<div>
<img src="@/assets/403.svg" alt="forbiden" class="w-[320px]" />
</div>
<div>
<h2 class="text-3xl m-0 font-bold">403</h2>
<p class="mt-2">您的权限不足如需访问请联系管理员分配权限!</p>
<div class="space-x-3 mt-6">
<a-button type="primary" @click="router.back()">
<template #icon>
<i class="icon-park-outline-back"></i>
</template>
返回
</a-button>
<a-button type="outline" @click="router.push('/')">
<template #icon>
<i class="icon-park-outline-home"></i>
</template>
首页
</a-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const router = useRouter();
</script>
<style scoped></style>
<route lang="json">
{
"meta": {
"title": "404",
"icon": "icon-park-outline-home"
}
}
</route>

View File

@ -0,0 +1,237 @@
### 介绍
基于`Arco-Design`组件封装的表单,旨在通过较少的配置提升开发效率,将一些通过的状态管理内置,使得开发者只需关注核心内容即可快速开发通用型表单。
本表单适用于通用型表单,对于自定义要求较高的需求,可能不太适合。
### 功能
- 配置化编写代码保证UI一致性提供开发效率。
- 提供typesciprt类型提示
- 表单项和校验规则之间可联动、可动态显示/隐藏
- 内置常用校验规则,开箱即用
- 支持组件参数透传,让每个组件都能自定义。
### 基本功能
基本用法:
```tsx
<template>
<Form v-bind="form" />
</template>
<script setup lang="ts">
import { Form, useForm } from '@/components'
const form = useForm({
model: {
id: undefined
},
items: [
{
field: 'username',
label: '用户名称',
type: 'input'
}
],
submit: async ({ model, items }) => {
await new Promise(res => setTimeout(res, 2000));
return { message: '操作成功!' }
},
formProps: {},
})
</script>
```
以上, 只有四个参数,只需定义关注的内容,剩下的内容如内部状态等, 由表单管理。
| 参数 | 说明 | 类型 |
| :--- | :--- | :--- |
| model | 表单数据(可选),默认从`items`每一项的`field`和`initialValue`生成,如果存在同名属性,将与其合并。 | `Record<string, any>` |
| items | 表单项,具体用法看下文。| `FormItem[]` |
| submit | 提交表单的函数,可为同步/异步函数。当有返回值且返回值为包含`message`的对象时,将弹出成功提示。| `({ model, items }) => Promise<any>` |
| formProps | 传递给`AForm`组件的参数(可选),具体可参考`Arco-Design`的`Form`组件,部分参数不可用,如`model`等。 | `FormInstance['$props']` |
### 表单数据
`model`表示当前表单的数据,当使用`useForm`时,将从`items`中每一项的`field`和`initialValue`生成。如果`model`中的属性与`field`值同名,且`initialValue`值不为空,则原`model`中的同名属性值将被覆盖。
对于日期范围框、级联选择器等值为数组的组件,提供有一份便捷的语法,请看如下示例:
```typescript
const form = useForm({
items: [
{
field: `startDate:endDate`,
label: '日期范围',
type: 'dateRange',
},
{
field: 'provice:city:town',
label: '省市区',
type: 'cascader',
options: []
}
]
})
```
以上,`field`可通过`:`分隔的语法,指定提交表单时,将数组值划分到指定的属性上,最终提交的数据如下:
```typescript
{
startDate: '',
endDate: '',
province: '',
city: '',
town: ''
}
```
### 表单项
用法示例:
```typescript
const form = useForm({
items: [
{
field: 'username',
initialValue: 'apptify',
label: '用户名称',
type: 'input',
itemProps: {},
nodeProps: {},
visible: ({ model, item, items }) => true,
disable: ({ model, item, items }) => true,
required: true,
rules: ['email'],
options: ({ model }) => api.xx(model.id)
component: ({ model, item, items }) => <div> </div>,
help: string |
}
]
})
```
用法说明:
| 参数 | 说明 | 类型 | 默认值 |
| :--- | :--- | :--- | :--- |
| field | 字段名,将合并合并到`model`中,默认值为`undefined`,可通过`initalValue`指定初始值 | string | - |
| initialValue | 初始值, 作为默认初始值以及通过`formRef.reset`重置表单数据时的值 | any | undefined |
| label | 标签名,可为字符串或函数, 作用同`AFormItem`的`label`参数 | string \| ({ model,item }) => JSX.Element | - |
| type | 输入控件的类型,具体可参考下文 | NodeType | 'input' |
| visible | 动态控制该表单项是否显示 | boolean \| ({ model,item }) => boolean | - |
| disable | 动态控制该表单项是否禁止,作用同`FormItem`的`disabled`属性 | boolean \| ({ model, item }) => boolean | - |
| required | 是否必填,作用同`AFormItem`的`required`属性 | boolean | - |
| rules | 校验规则,内置常用规则,并支持动态生效,详见下文 | RuleType[] | - |
| options | 作用域`select`等多选项组件,支持动态获取 | (Option[]) \| ({ model, item }) => Option[] | - |
| itemProps | 传递给`AFormItem`组件的参数,部分参数不可用,如上面的`field`等参数 | FormItemInstance['$props'] | - |
| nodeProps | 传递给`type`属性对应组件的参数,如当`type`为`input`时, `nodeProps`类型为`Input`组件的props。 | NodeProps | - |
### 控件类型
表单项的`type`指定表单控件的类型,当输入具体的值时,`nodeProps`会提供对应的参数类型提示。内置有常见的组件,且带有默认的参数,具体默认参数可在`src/components/form/form-node.tsx`中查看:
| 类型 | 说明 |
| :--- | :--- |
| input | 同 [Input](https://arco.design/vue/component/input) 组件
| number | 同 [InputNumber](https://arco.design/vue/component/input-number) 组件
| password | 同 [InputPassword](https://arco.design/vue/component/input#password) 组件
| select | 同 [Select](https://arco.design/vue/component/select) 组件
| time | 同 [TimePicker](https://arco.design/vue/component/time-picker) 组件
| date | 同 [DatePicker](https://arco.design/vue/component/date-picker) 组件
| dateRange | 同 [RangePicker](https://arco.design/vue/component/date-picker#range) 组件
| textarea | 同 [Textarea](https://arco.design/vue/component/textarea) 组件
| cascader | 同 [Cascader](https://arco.design/vue/component/cascader) 组件
| checkbox | 同 [Checkbox](https://arco.design/vue/component/checkbox) 组件
| radio | 同 [Radio](https://arco.design/vue/component/radio) 组件
| slider | 同 [Slider](https://arco.design/vue/component/slider) 组件
| submit | 提交表单按钮,应只有一个。
| custom | 自定义组件,通过表单项的`component`属性定义需返回一个JSX元素。
对于`select`、`checkbox`、`radio`和`cascader`类型,其`options`参数不通过`nodeProps`传递,而是写在表单项的`options`属性。该属性支持数组和函数类型,当为数组类型时将直接传递给控件,当为函数时可动态请求,返回值需为数组类型。
以上描述,示例如下:
```typescript
const form = useForm({
items: [
{
field: 'gender',
label: '性别',
type: 'select',
options: [
{
label: '男',
value: 1,
},
{
label: '女',
value: 2,
}
]
},
{
field: 'departmentId',
label: '部门',
type: 'cascader',
options: async ({ model, item }) => {
const res = await api.getDepartments(model.xx);
return res.data;
}
}
]
})
```
### 表单校验
跟表单校验相关的属性有2个`required`(必填)和`rules`属性,其中`rules`内置常见的校验规则,参考如下:
| 校验规则 | 说明 |
| :--- | :--- |
| string | 格式为字符串 |
| number | 格式为数字 |
| passwod | 格式为密码类型,即至少包含大写字母、小写字母、数字和特殊字符。|
| required | 该项必填 |
| email | 格式为邮箱类型,例如: xx@abc.com |
| url | 格式为URL类型, 例如: https://abc.com |
| ip | 格式为IP类型, 例如: 101.10.10.302 |
| phone | 格式为11位手机号例如: 15912345678 |
| idcard | 格式为18位身份证号例如: 12345619991205131x |
| alphabet | 格式为26字母例如apptify |
当以上规则不满足需求时,可通过对象自定义校验规则,具体语法可参考`AFormItem`的 [FieldRule](https://arco.design/vue/component/form#FieldRule) 文档。在其基础上,可添加一个`disable`函数,用于动态禁止/允许当前校验规则。
用法示例:
```typescript
const form = useForm({
items: [
{
required: true,
rules: [
'email',
{
match: /\d{2,3}/,
message: '请输入2~3位数字',
disable: ({ model, item, items }) => !model.username
}
],
}
]
})
```
### 提交表单
`submit`为提交表单的函数,通常返回一个`promise`,当该函数抛出异常,则默认为提交失败。该函数有一个可选的返回值,如果返回值为包含`message`的对象时,将弹出一个包含`message`值的成功提示。
示例如下:
```typescript
const form = useForm({
submit: async ({ model, items }) => {
const res = await api.xx(model);
return { message: res.msg }
}
})
```
### 常见问题
- Q为什么不是模板形式
- A配置式更易于描述逻辑模板介入和引入的组件比较多且对于做typescript类型提示不是很方便。
- Q为什么不是JSON形式
- A对于自定义组件支持、联动等不是非常友好尽管可以通过解析字符串执行等方式实现对typescript提示也不是很友好。
### 最后
尽管看起来是低代码,但其实我更倾向于是业务组件。

View File

@ -0,0 +1,225 @@
import { FormItem as BaseFormItem, FieldRule, FormItemInstance, SelectOptionData } from "@arco-design/web-vue";
import { NodeType, NodeUnion, nodeMap } from "./form-node";
const defineRuleMap = <T extends Record<string, FieldRule>>(ruleMap: T) => ruleMap;
const ruleMap = defineRuleMap({
required: {
required: true,
message: "该项不能为空",
},
string: {
type: "string",
message: "请输入字符串",
},
number: {
type: "number",
message: "请输入数字",
},
email: {
type: "email",
message: "邮箱格式错误,示例: xx@abc.com",
},
url: {
type: "url",
message: "URL格式错误, 示例: www.abc.com",
},
ip: {
type: "ip",
message: "IP格式错误, 示例: 101.10.10.30",
},
phone: {
match: /^(?:(?:\+|00)86)?1\d{10}$/,
message: "手机格式错误, 示例(11位): 15912345678",
},
idcard: {
match: /^[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/,
message: "身份证格式错误, 长度为15或18位",
},
alphabet: {
match: /^[a-zA-Z]\w{4,15}$/,
message: "请输入英文字母, 长度为4~15位",
},
password: {
match: /^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/,
message: "至少包含大写字母、小写字母、数字和特殊字符",
},
});
export type FieldStringRule = keyof typeof ruleMap;
export type FieldObjectRule = FieldRule & {
disable?: (arg: { item: IFormItem; model: Record<string, any> }) => boolean;
};
export type FieldRuleType = FieldStringRule | FieldObjectRule;
/**
*
*/
export const FormItem = (props: any, { emit }: any) => {
const { item } = props;
const args = {
...props,
field: item.field,
};
const rules = computed(() => {
const result = [];
if (item.required) {
result.push(ruleMap.required);
}
item.rules?.forEach((rule: any) => {
if (typeof rule === "string") {
result.push(ruleMap[rule as FieldStringRule]);
return;
}
if (!rule.disable) {
result.push(rule);
return;
}
if (!rule.disable({ model: props.model, item, items: props.items })) {
result.push(rule);
}
});
return result;
});
const disabled = computed(() => {
if (item.disable === undefined) {
return false;
}
if (typeof item.disable === "function") {
return item.disable(args);
}
return item.disable;
});
if (item.visible && !item.visible(args)) {
return null;
}
return (
<BaseFormItem rules={rules.value} disabled={disabled.value} field={item.field} {...item.itemProps}>
{{
default: () => {
if (item.component) {
return <item.component {...item.nodeProps} />;
}
const comp = nodeMap[item.type as NodeType]?.component;
if (!comp) {
return null;
}
if (item.type === "submit") {
return <comp loading={props.loading} onSubmit={() => emit("submit")} onCancel={emit("cancel")} />;
}
return <comp v-model={props.model[item.field]} {...item.nodeProps} />;
},
label: item.label && (() => (typeof item.label === "string" ? item.label : item.label?.(args))),
help: item.help && (() => (typeof item.help === "string" ? item.help : item.help?.(args))),
extra: item.extra && (() => (typeof item.extra === "string" ? item.extra : item.extra?.(args))),
}}
</BaseFormItem>
);
};
type FormItemBase = {
/**
*
* @example
* ```typescript
* // 1. 以:分隔的字段名,将用作数组值解构。例如:
* {
* field: 'v1:v2',
* type: 'dateRange',
* }
* // 将得到
* {
* v1: '2021-01-01',
* v2: '2021-01-02',
* }
* ```
*/
field: string;
/**
*
* @description undefinedmodel
*/
initialValue?: any;
/**
*
* @description FormItemlabel
*/
label?: string | ((item: IFormItem, model: Record<string, any>) => any);
/**
* `FormItem`
* @description fieldlabelrequiredrulesdisabled
*/
itemProps?: Partial<Omit<FormItemInstance["$props"], "field" | "label" | "required" | "rules" | "disabled">>;
/**
*
* @description false
*/
required?: boolean;
/**
*
* @description ()
* @example
* ```typescript
* rules: [
* 'idcard', // 内置的身份证号校验规则
* {
* match: /\d+/,
* message: '请输入数字',
* },
* ]
*```
* @see https://arco.design/vue/component/form#FieldRule
*/
rules?: FieldRuleType[];
/**
*
* @description
*/
visible?: (arg: { item: IFormItem; model: Record<string, any> }) => boolean;
/**
*
* @description
*/
disable?: (arg: { item: IFormItem; model: Record<string, any> }) => boolean;
/**
*
* @description ,
*/
options?: SelectOptionData[] | ((arg: { item: IFormItem; model: Record<string, any> }) => Promise<any>);
/**
*
* @description
*/
component?: (args: { item: IFormItem; model: Record<string, any>; field: string }) => any;
/**
*
* @description FormItemhelp
* @see https://arco.design/vue/component/form#form-item%20Slots
*/
help?: string | ((args: { item: IFormItem; model: Record<string, any> }) => any);
/**
*
* @description FormItemextra
* @see https://arco.design/vue/component/form#form-item%20Slots
*/
extra?: string | ((args: { item: IFormItem; model: Record<string, any> }) => any);
};
export type IFormItem = FormItemBase & NodeUnion;

View File

@ -0,0 +1,183 @@
import { Button, ButtonInstance, FormInstance, Message, Modal } from "@arco-design/web-vue";
import { assign, cloneDeep, omit } from "lodash-es";
import { PropType, defineComponent } from "vue";
import { Form } from "./form";
import { IFormItem } from "./form-item";
/**
*
*/
export const FormModal = defineComponent({
name: "FormModal",
inheritAttrs: false,
props: {
/**
*
* @default '新建'
*/
title: {
type: [String, Function] as PropType<
string | ((args: { model: Record<string, any>; items: IFormItem[] }) => string)
>,
default: "新建",
},
/**
*
*/
trigger: {
type: [Boolean, Object] as PropType<boolean | (ButtonInstance["$props"] & { text: string })>,
default: true,
},
/**
* Modalprops
*/
modalProps: {
type: Object as PropType<Omit<InstanceType<typeof Modal>["$props"], "visible" | "title" | "onBeforeOk">>,
},
/**
*
*/
model: {
type: Object as PropType<Record<string, any>>,
required: true,
},
/**
*
*/
items: {
type: Array as PropType<IFormItem[]>,
required: true,
},
/**
*
* @description `{ message }`
*/
submit: {
type: Function as PropType<(arg: { model: Record<string, any>; items: IFormItem[] }) => any | Promise<any>>,
default: () => true,
},
/**
* Formprops
*/
formProps: {
type: Object as PropType<Omit<FormInstance["$props"], "model">>,
},
},
emits: ["close", "submited"],
setup(props, { slots, emit, attrs }) {
const origin = cloneDeep(props.model);
const formRef = ref<InstanceType<typeof Form>>();
const loading = ref(false);
const visible = ref(false);
const open = async (data: Record<string, any> = {}) => {
visible.value = true;
await nextTick();
for (const key in data) {
props.model[key] = data[key];
}
};
const onBeforeOk = async () => {
if (typeof attrs.onBeforeOk === "function") {
const isOk = await attrs.onBeforeOk();
if (!isOk) return false;
}
const errors = await formRef.value?.formRef?.validate();
if (errors) {
return false;
}
try {
const model = formRef.value?.getModel() || {};
const res = await props.submit?.({ items: props.items, model });
res?.message && Message.success(`提示: ${res.message}`);
emit("submited", res);
} catch (error: any) {
error.message && Message.error(`提示: ${error.message}`);
return false;
}
return true;
};
const onClose = () => {
visible.value = false;
assign(props.model, origin);
emit("close");
};
const modalTitle = computed(() => {
if (typeof props.title === "string") {
return props.title;
}
if (typeof props.title === "function") {
return props.title({ model: props.model, items: props.items });
}
});
const modalTrigger = computed(() => {
if (!props.trigger) {
return null;
}
let content;
if (typeof props.trigger === "boolean") {
content = (
<Button type="primary">
{{
default: () => "新建",
icon: () => <i class="icon-park-outline-plus" />,
}}
</Button>
);
}
if (typeof props.trigger === "object") {
content = (
<Button type="primary" {...omit(props.trigger, "text")}>
{props.trigger?.text || "新建"}
</Button>
);
}
if (slots.trigger) {
content = slots.trigger({ model: props.model, items: props.items });
}
return <span onClick={() => open()}>{content}</span>;
});
return {
origin,
formRef,
loading,
visible,
modalTitle,
modalTrigger,
open,
onClose,
onBeforeOk,
};
},
render() {
return (
<>
{this.modalTrigger}
<Modal
{...this.modalProps}
v-model:visible={this.visible}
onBeforeOk={this.onBeforeOk}
onClose={this.onClose}
title={this.modalTitle}
>
{this.visible && (
<Form ref={(el: any) => (this.formRef = el)} {...this.formProps} model={this.model} items={this.items}>
{{ ...this.$slots }}
</Form>
)}
</Modal>
</>
);
},
});
export type FormModalInstance = InstanceType<typeof FormModal>;
export type FormModalProps = FormModalInstance["$props"];
export default FormModal;

View File

@ -0,0 +1,223 @@
import {
Button,
Cascader,
CheckboxGroup,
DatePicker,
Input,
InputNumber,
InputPassword,
RadioGroup,
RangePicker,
Select,
Slider,
Textarea,
TimePicker,
} from "@arco-design/web-vue";
const initOptions = ({ item, model }: any) => {
if (Array.isArray(item.options)) {
item.nodeProps.options = item.options;
return;
}
if (typeof item.options !== "function") {
return;
}
item.nodeProps.options = reactive([]);
const fetchData = item.options;
item._updateOptions = async () => {
let data = await fetchData({ item, model });
if (Array.isArray(data?.data)) {
data = data.data.map((i: any) => ({ label: i.name, value: i.id }));
}
if (Array.isArray(data)) {
item.nodeProps.options.splice(0);
item.nodeProps.options.push(...data);
}
};
item._updateOptions();
};
const defineNodeMap = <T extends { [key: string]: { component: any, nodeProps: any } }>(map: T) => {
return map
}
/**
*
*/
export const nodeMap = {
/**
*
*/
input: {
component: Input,
nodeProps: {
placeholder: "请输入",
allowClear: true,
} as InstanceType<typeof Input>["$props"],
},
/**
*
*/
textarea: {
component: Textarea,
nodeProps: {
placeholder: "请输入",
allowClear: true,
} as InstanceType<typeof Textarea>["$props"],
},
/**
*
*/
number: {
component: InputNumber,
nodeProps: {
placeholder: "请输入",
defaultValue: 0,
allowClear: true,
} as InstanceType<typeof InputNumber>["$props"],
},
/**
*
*/
password: {
component: InputPassword,
nodeProps: {
placeholder: "请输入",
} as InstanceType<typeof InputPassword>["$props"],
},
/**
*
*/
select: {
component: Select,
nodeProps: {
placeholder: "请选择",
allowClear: true,
allowSearch: true,
options: [{}],
} as InstanceType<typeof Select>["$props"],
init: initOptions,
},
/**
*
*/
cascader: {
component: Cascader,
nodeProps: {
placeholder: "请选择",
allowClear: true,
expandTrigger: "hover",
} as InstanceType<typeof Cascader>["$props"],
init: initOptions,
},
/**
*
*/
time: {
component: TimePicker,
nodeProps: {
allowClear: true,
} as InstanceType<typeof TimePicker>["$props"],
},
/**
*
*/
date: {
component: DatePicker,
nodeProps: {
allowClear: true,
} as InstanceType<typeof DatePicker>["$props"],
},
/**
*
*/
dateRange: {
component: RangePicker,
nodeProps: {
allowClear: true,
} as InstanceType<typeof RangePicker>["$props"],
},
/**
*
*/
checkbox: {
component: CheckboxGroup,
nodeProps: {
allowClear: true,
} as InstanceType<typeof CheckboxGroup>["$props"],
init: initOptions,
},
/**
*
*/
radio: {
component: RadioGroup,
nodeProps: {
allowClear: true,
} as InstanceType<typeof RadioGroup>["$props"],
init: initOptions,
},
/**
*
*/
slider: {
component: Slider,
nodeProps: {
allowClear: true,
} as InstanceType<typeof Slider>["$props"],
},
/**
*
*/
submit: {
component: (props: any, { emit }: any) => {
const state = inject("tableInstance");
console.log("st", state);
return (
<>
<Button type="primary" loading={props.loading} onClick={() => emit("submit")} class="mr-3">
</Button>
{/* <Button loading={props.loading} onClick={() => emit("cancel")}>
</Button> */}
</>
);
},
nodeProps: {},
},
/**
*
*/
custom: {
nodeProps: {},
component: () => null,
},
};
/**
*
*/
export type NodeMap = typeof nodeMap;
/**
*
*/
export type NodeType = keyof NodeMap;
/**
* `FormItem`
* @description typenodeProps
*/
export type NodeUnion = {
[key in NodeType]: {
/**
* `input`
*/
type: key;
/**
* `type`
*/
nodeProps?: NodeMap[key]["nodeProps"];
};
}[NodeType];

View File

@ -0,0 +1,133 @@
import { Form as BaseForm, FormInstance as BaseFormInstance, Message } from "@arco-design/web-vue";
import { assign, cloneDeep, defaultsDeep } from "lodash-es";
import { PropType } from "vue";
import { FormItem, IFormItem } from "./form-item";
import { NodeType, nodeMap } from "./form-node";
/**
*
*/
export const Form = defineComponent({
name: "Form",
props: {
/**
*
*/
model: {
type: Object as PropType<Record<string, any>>,
default: () => reactive({}),
},
/**
*
*/
items: {
type: Array as PropType<IFormItem[]>,
default: () => [],
},
/**
*
*/
submit: {
type: Function as PropType<(arg: { model: Record<string, any>; items: IFormItem[] }) => Promise<any>>,
},
/**
* Form
*/
formProps: {
type: Object as PropType<Omit<BaseFormInstance["$props"], "model">>,
},
},
setup(props) {
const model = cloneDeep(props.model);
const formRef = ref<InstanceType<typeof BaseForm>>();
const loading = ref(false);
props.items.forEach((item: any) => {
const node = nodeMap[item.type as NodeType];
defaultsDeep(item, { nodeProps: node?.nodeProps ?? {} });
(node as any).init?.({ item, model: props.model });
});
const getItem = (field: string) => {
return props.items.find((item) => item.field === field);
};
const getModel = () => {
const model: Record<string, any> = {};
for (const key of Object.keys(props.model)) {
if (/[^:]+:[^:]+/.test(key)) {
const keys = key.split(":");
const vals = cloneDeep(props.model[key] || []);
for (const k of keys) {
model[k] = vals.shift();
}
} else {
model[key] = cloneDeep(props.model[key]);
}
}
return model;
};
const submitForm = async () => {
if (await formRef.value?.validate()) {
return;
}
const model: Record<string, any> = getModel();
try {
loading.value = true;
const res = await props.submit?.({ model, items: props.items });
res.message && Message.success(`提示: ${res.message}`);
} finally {
loading.value = false;
}
};
const resetModel = () => {
assign(props.model, model);
};
const setModel = (model: Record<string, any>) => {
for (const key of Object.keys(props.model)) {
if (/.+:.+/.test(key)) {
const [key1, key2] = key.split(":");
props.model[key] = [model[key1], model[key2]];
} else {
props.model[key] = model[key];
}
}
};
return {
formRef,
loading,
getItem,
submitForm,
resetModel,
setModel,
getModel,
};
},
render() {
(this.items as any).instance = this;
const props = {
items: this.items,
model: this.model,
slots: this.$slots,
};
return (
<BaseForm ref="formRef" layout="vertical" model={this.model} {...this.$attrs} {...this.formProps}>
{this.items.map((item) => (
<FormItem loading={this.loading} onSubmit={this.submitForm} item={item} {...props}></FormItem>
))}
</BaseForm>
);
},
});
export type FormInstance = InstanceType<typeof Form>;
export type FormProps = FormInstance["$props"];
export type FormDefinedProps = Pick<FormProps, "model" | "items" | "submit" | "formProps">;

View File

@ -0,0 +1,4 @@
export * from "./form";
export * from "./use-form";
export * from "./form-modal";
export * from "./use-form-modal";

View File

@ -0,0 +1,22 @@
import { Modal } from "@arco-design/web-vue";
import { assign } from "lodash-es";
import { reactive } from "vue";
import { useForm } from "./use-form";
import { FormModalProps } from "./form-modal";
const defaults: Partial<InstanceType<typeof Modal>> = {
width: 1080,
titleAlign: "start",
};
/**
* FormModal
* @see src/components/form/use-form-modal.tsx
*/
export const useFormModal = (options: FormModalProps): FormModalProps & { model: Record<string, any> } => {
const { model, items } = options || {};
const form = useForm({ model, items });
return reactive(assign({ modalProps: { ...defaults } }, { ...options, ...form }));
};

View File

@ -0,0 +1,62 @@
import { FormInstance } from "@arco-design/web-vue";
import { IFormItem } from "./form-item";
export type Options = {
/**
*
*/
model?: Record<string, any>;
/**
*
*/
items: IFormItem[];
/**
*
*/
submit?: (arg: { model: Record<string, any>; items: IFormItem[] }) => Promise<any>;
/**
*
*/
formProps?: Partial<FormInstance["$props"]>;
};
/**
*
* @see src/components/form/use-form.tsx
*/
export const useForm = (options: Options) => {
const { model = { id: undefined } } = options;
const items: IFormItem[] = [];
options.items.forEach((item) => {
if (!item.nodeProps) {
item.nodeProps = {} as any;
}
if (/(.+)\?(.+)/.test(item.field)) {
const [field, condition] = item.field.split("?");
model[field] = item.initialValue ?? model[item.field];
const params = new URLSearchParams(condition);
for (const [key, value] of params.entries()) {
model[key] = value;
}
}
model[item.field] = model[item.field] ?? item.initialValue;
const _item = { ...item };
items.push(_item);
});
if (options.submit) {
const submit = items.find((item) => item.type === "submit");
if (!submit) {
items.push({
field: "id",
type: "submit",
itemProps: {
hideLabel: true,
},
});
}
}
return reactive({ ...options, model, items }) as any;
};

3
src/components/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from "./form";
export * from "./table";
export * from "./toast";

View File

@ -0,0 +1,156 @@
### 基本用法
```typescript
<template>
<Table v-bind="table" />
</template>
<script setup lang="ts">
import { Table, useTable } from '@/components'
const table = useTable({
data: (search, paging) => {
return {
data: [
{
username: '用户A'
}
],
meta: {
total: 30
}
};
},
columns: [
{
title: "用户名称",
dataIndex: "username",
},
],
pagination: {
pageSize: 10,
showTotal: true
},
search: {
items: [
{
field: "username",
label: "用户名称",
type: "input",
},
],
},
common: {
items: [
{
field: "username",
label: "用户名称",
type: "input",
},
],
},
create: {
title: "新建用户",
submit: async ({ model }) => {
return api.xx(model);
},
},
modify: {
title: "修改用户",
submit: async ({ model }) => {
return api.xx(model);
},
},
});
</script>
```
以上就是一个CRUD表格的简单用法。参数描述
| 参数 | 说明 | 类型 |
| :--- | :--- | :--- |
| data | 表格数据,可为数组或函数(发起HTTP请求) | BaseData[] | ((search, paging) => Promise<any>) |
| columns | 表格列,参见 [TableColumnData](https://arco.design/vue/component/table#TableColumnData) 文档,增加和扩展部分属性,详见下文。 | TableColumnData[] |
| pagination | 分页参数,参见 [Pagination](https://arco.design/vue/component/pagination) 文档,默认 15/每页。| Pagination |
| search | 搜索表单的配置,参见 [Form]() 说明,其中 `submit` 参数不可用 | FormProps |
| common | 新增和修改表单弹窗的公用参数,参见 [FormModal]() 说明。 | FormModalProps |
| create | 新增表单弹窗的参数,参见 [FormModal]() 说明, 将与`common`参数合并。 | FormModalProps |
| modify | 修改表单弹窗的参数,参见 [FormModal]() 说明, 将与`common`参数合并。 | FormModalProps |
| tableProps | 传递给`Table`组件的参数,参见 [Table](https://arco.design/vue/component/table) 文档,其中`columns`参数不可用。| TableProps |
### 表格数据
`data`定义表格数据,可以是数组或函数。
- 当是数组时,直接用作数据源。
- 当是函数时,传入查询参数和分页参数,可返回数组或对象,返回数组作用同上,返回对象时需遵循`{ data: [], meta: { total: number } }`格式,用于分页处理。
用法示例:
```typescript
const table = useTable({
data: async (search, paging) {
const { page, size: pageSize } = paging
const res = await api.xx({ ...search, page, pageSize });
return {
data: res.data,
meta: {
total: res.total
}
}
}
})
```
### 表格列
`columns`定义表格列,并在原本基础上增加默认值并扩展部分属性。增加和扩展的属性如下:
| 参数 | 说明 | 类型 |
| :--- | :--- | :--- |
| type | 特殊类型, 目前支持`index`(表示行数)、`button`(行操作按钮) | 'index' | 'button' |
| buttons | 当`type`为`button`时的按钮数组,如果子项是对象则为`Button`组件的参数,如果为函数则为自定义渲染函数。 | Button[]
### 表格分页
`pagination`定义分页行为,具体参数可参考 [Pagination](https://arco.design/vue/component/pagination) 文档。当`data`为数组时,将作为数据源进行分页;当`data`为函数且返回值为对象时,则根据`total`值进行分页。
### 搜索表单
参阅
### 公共参数
参数为`FormModal`的参数,主要作为新增和修改的公共参数。在大多数情况,新增和修改的配置大多是相似的,没必要写两份,把相同的参数写在这里即可,不同的参数在`create`和`modify`中单独配置。
注意,这里的`items`也可以被搜索表单复用,搜索表单可通过`extends: <field>`继承`common.items`中对应的字段配置。使用示例如下:
```typescript
const table = useTable({
common: {
items: [
{
field: 'username',
label: '用户名称',
type: 'input',
required: true,
}
]
},
search: {
items: [
{
extend: 'usernam',
required: false,
}
]
}
})
```
### 新增弹窗
`create`为新增表单弹窗的参数,即`useFormModal`对应的参数。参阅。当指定该参数时,会在表格左上添加新建按钮,如需自定义按钮样式或自定义渲染,可通过`create.trigger`参数配置。
### 修改弹窗
`modify`为新增表单弹窗的参数,即`useFormModal`对应的参数。参阅。当指定该参数时,会在表格行添加修改按钮。
### 表格参数
`tableProps`为传递给`Table`组件的额外参数,其中部分参数不可用,如`data`和`columns`等。此外,部分参数有默认值,具体参数可查看`src/components/table/table.config.ts`文件。
### 插槽
- `Table`组件的插槽可正常使用
- `action`插槽用作表格左上方的操作区。
## 问题
- 问题:日期范围框值为数组,处理不方便
- 解决:字段名使用`v1:v2`格式,提交时会生成`{ v1: '00:00:01', v2: '00:00:02' }`数据
- 问题:搜索表单、新增表单和修改表单通常用到同一表单项,如何避免重复定义
- 解决:表单项使用`{ extends: <field-name> }`会在`common.items`中寻找相同的项,并合并值。

View File

@ -0,0 +1,2 @@
export * from "./table";
export * from "./use-table";

View File

@ -0,0 +1,71 @@
import { Button } from "@arco-design/web-vue";
import { IconRefresh, IconSearch } from "@arco-design/web-vue/es/icon";
/**
*
*/
export const TABLE_SEARCH_DEFAULTS = {
labelAlign: "left",
autoLabelWidth: true,
model: {},
};
/**
*
*/
export const TABLE_COLUMN_DEFAULTS = {
ellipsis: true,
tooltip: true,
render: ({ record, column }: any) => record[column.dataIndex] || "-",
};
/**
*
*/
export const TABLE_ACTION_DEFAULTS = {
buttonProps: {
type: "primary",
},
};
/**
*
*/
export const TABLE_DELTE_DEFAULTS = {
title: "删除确认",
content: "确认删除当前数据吗?",
modalClass: "text-center",
hideCancel: false,
maskClosable: false,
};
export const TALBE_INDEX_DEFAULTS = {
title: "#",
width: 60,
align: "center",
render: ({ rowIndex }: any) => rowIndex + 1,
};
export const searchItem = {
field: "id",
type: "custom",
itemProps: {
class: "table-search-item col-start-4 !mr-0 grid grid-cols-[0_1fr]",
hideLabel: true,
},
component: () => {
const tableRef = inject<any>("ref:table");
return (
<div class="w-full flex gap-x-2 justify-end">
{(tableRef.search?.items?.length || 0) > 3 && (
<Button disabled={tableRef?.loading.value} onClick={() => tableRef?.reloadData()}>
{{ icon: () => <IconRefresh></IconRefresh>, default: () => "重置" }}
</Button>
)}
<Button type="primary" loading={tableRef?.loading.value} onClick={() => tableRef?.loadData()}>
{{ icon: () => <IconSearch></IconSearch>, default: () => "查询" }}
</Button>
</div>
);
},
};

View File

@ -0,0 +1,241 @@
import {
TableColumnData as BaseColumn,
TableData as BaseData,
Table as BaseTable,
Divider,
} from "@arco-design/web-vue";
import { PropType, computed, defineComponent, reactive, ref, watch } from "vue";
import { Form, FormInstance, FormModal, FormModalInstance, FormModalProps, FormProps } from "../form";
/**
*
* @see src/components/table/table.tsx
*/
export const Table = defineComponent({
name: "Table",
props: {
/**
*
*/
data: {
type: [Array, Function] as PropType<
BaseData[] | ((search: Record<string, any>, paging: { page: number; size: number }) => Promise<any>)
>,
},
/**
*
*/
columns: {
type: Array as PropType<BaseColumn[]>,
default: () => [],
},
/**
*
*/
pagination: {
type: Object as PropType<any>,
default: () => reactive({ current: 1, pageSize: 10, total: 300, showTotal: true }),
},
/**
*
*/
search: {
type: Object as PropType<FormProps>,
},
/**
*
*/
create: {
type: Object as PropType<FormModalProps>,
},
/**
*
*/
modify: {
type: Object as PropType<FormModalProps>,
},
/**
*
*/
detail: {
type: Object as PropType<any>,
},
/**
* Table
*/
tableProps: {
type: Object as PropType<InstanceType<typeof BaseTable>["$props"]>,
},
},
setup(props) {
const loading = ref(false);
const searchRef = ref<FormInstance>();
const createRef = ref<FormModalInstance>();
const modifyRef = ref<FormModalInstance>();
const renderData = ref<BaseData[]>([]);
const inlineSearch = computed(() => (props.search?.items?.length || 0) < 4);
Object.assign(props.columns, { getInstance: () => getCurrentInstance() });
const getPaging = (pagination: Partial<any>) => {
const { current: page, pageSize: size } = { ...props.pagination, ...pagination } as any;
return { page, size };
};
const loadData = async (pagination: Partial<any> = {}) => {
if (!props.data) {
return;
}
if (Array.isArray(props.data)) {
if (!props.search?.model) {
return;
}
const filters = Object.entries(props.search?.model || {});
const data = props.data?.filter((item) => {
return filters.every(([key, value]) => {
if (typeof value === "string") {
return item[key].includes(value);
}
return item[key] === value;
});
});
renderData.value = data || [];
props.pagination.total = renderData.value.length;
props.pagination.current = 1;
return;
}
if (typeof props.data !== "function") {
return;
}
const model = searchRef.value?.getModel() || {};
const paging = getPaging(pagination);
try {
loading.value = true;
const resData = await props.data(model, paging);
const { data = [], meta = {} } = resData || {};
const { page: pageNum, total } = meta;
renderData.value = data;
Object.assign(props.pagination, { current: pageNum, total });
} catch (error) {
console.log("table error", error);
} finally {
loading.value = false;
}
};
const reloadData = () => {
loadData({ current: 1, pageSize: 10 });
};
const openModifyModal = (data: any) => {
modifyRef.value?.open(data.record);
};
const onPageChange = (current: number) => {
loadData({ current });
};
const onCreateOk = () => {
reloadData();
};
const onModifyOk = () => {
reloadData();
};
watch(
() => props.data,
(data) => {
if (!Array.isArray(data)) {
return;
}
renderData.value = data;
props.pagination.total = data.length;
props.pagination.current = 1;
},
{
immediate: true,
}
);
onMounted(() => {
loadData();
});
const state = {
loading,
searchRef,
createRef,
modifyRef,
renderData,
inlineSearch,
loadData,
reloadData,
openModifyModal,
onPageChange,
onCreateOk,
onModifyOk,
};
provide("ref:table", { ...state, ...props });
return state;
},
render() {
(this.columns as any).instance = this;
return (
<div class="bh-table w-full">
{!this.inlineSearch && (
<div class="">
<Form ref={(el: any) => (this.searchRef = el)} class="grid grid-cols-4 gap-x-4" {...this.search}></Form>
</div>
)}
{!this.inlineSearch && <Divider class="mt-0 border-gray-200" />}
<div class={`mb-2 flex justify-between ${!this.inlineSearch && "mt-2"}`}>
<div class="flex-1 flex gap-2">
{this.create && (
<FormModal
ref={(el: any) => (this.createRef = el)}
onOk={this.onCreateOk}
{...(this.create as any)}
></FormModal>
)}
{this.modify && (
<FormModal
ref={(el: any) => (this.modifyRef = el)}
onOk={this.onModifyOk}
trigger={false}
{...(this.modify as any)}
></FormModal>
)}
{this.$slots.action?.()}
</div>
<div>
{this.inlineSearch && (
<Form
ref={(el: any) => (this.searchRef = el)}
{...{ ...this.search, formProps: { layout: "inline" } }}
></Form>
)}
</div>
</div>
<div>
<BaseTable
row-key="id"
bordered={false}
{...this.tableProps}
loading={this.loading}
pagination={this.pagination}
data={this.renderData}
columns={this.columns}
onPageChange={this.onPageChange}
></BaseTable>
</div>
</div>
);
},
});
export type TableInstance = InstanceType<typeof Table>;
export type TableProps = TableInstance["$props"];

View File

@ -0,0 +1,116 @@
import { Link, TableColumnData, TableData } from "@arco-design/web-vue";
import { FormModalProps, FormProps } from "../form";
import { IFormItem } from "../form/form-item";
import { TableProps } from "./table";
interface UseColumnRenderOptions {
/**
*
*/
record: TableData;
/**
*
*/
column: TableColumnData;
/**
*
*/
rowIndex: number;
}
export interface TableColumnButton {
/**
* button text
*/
text?: string;
/**
* button type
*/
type?: "delete" | "modify";
/**
* onClick callback
*/
onClick?: (data: UseColumnRenderOptions, openModify?: (model: Record<string, any>) => void) => void;
/**
* disable button dynamicly
*/
disabled?: (data: UseColumnRenderOptions) => boolean;
/**
* show or hide button dynamicly
*/
visible?: (data: UseColumnRenderOptions) => boolean;
/**
* props for `Button`
*/
buttonProps?: Partial<Omit<InstanceType<typeof Link>["$props"], "onClick" | "disabled">>;
}
export interface UseTableColumn extends TableColumnData {
/**
* column type
*/
type?: "index" | "button";
/**
* only for `type: "button"`
*/
buttons?: TableColumnButton[];
}
type ExtendableFormItem = (
| string
| ({
/**
* common.itemsfield
*/
extend: string;
} & Partial<IFormItem>)
| IFormItem
)[];
export interface UseTableOptions extends Omit<TableProps, "search" | "create" | "modify" | "columns"> {
/**
* columns config, extends from `TableColumnData`
* @see https://arco.design/web-vue/components/table/#tablecolumn
*/
columns: UseTableColumn[];
/**
* search form config
* @see FormProps
*/
search?: Partial<{
[k in keyof FormProps]: k extends "items" ? ExtendableFormItem : FormProps[k];
}>;
/**
* common props for `create` and `modify` modal
* @see FormModalProps
*/
common?: Partial<FormModalProps>;
/**
*
*/
create?: Partial<
{
[k in keyof FormModalProps]: k extends "items"
? (string | (IFormItem & { extend: string }))[]
: FormModalProps[k];
} & { extend: boolean }
>;
/**
*
*/
modify?: Partial<
{ [k in keyof FormModalProps]: k extends "items" ? (string | IFormItem)[] : FormModalProps[k] } & {
extend: boolean;
}
>;
/**
*
*/
detail?: any;
}

View File

@ -0,0 +1,144 @@
import { Link, Message, Modal, TableColumnData } from "@arco-design/web-vue";
import { defaultsDeep, isArray, isFunction, mergeWith, omit } from "lodash-es";
import { reactive } from "vue";
import { TableInstance } from "./table";
import {
TABLE_ACTION_DEFAULTS,
TABLE_COLUMN_DEFAULTS,
TABLE_DELTE_DEFAULTS,
TALBE_INDEX_DEFAULTS,
searchItem,
} from "./table.config";
import { UseTableOptions } from "./use-interface";
const merge = (...args: any[]) => {
return mergeWith({}, ...args, (obj: any, src: any) => {
if (Array.isArray(obj) && Array.isArray(src)) {
return obj.concat(src);
}
return undefined;
});
};
const has = (obj: any, key: string) => Object.prototype.hasOwnProperty.call(obj, key);
const propTruly = (obj: any, key: string) => !has(obj, key) || !!obj[key];
/**
* 便Table
* @see src/components/table/use-table.tsx
*/
export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions)): any => {
const options: UseTableOptions = typeof optionsOrFn === "function" ? optionsOrFn() : optionsOrFn;
const columns: TableColumnData[] = [];
const getTable = (): TableInstance => (columns as any).instance;
options.columns.forEach((column) => {
// 序号
if (column.type === "index") {
defaultsDeep(column, TALBE_INDEX_DEFAULTS);
}
// 操作
if (column.type === "button" && isArray(column.buttons)) {
if (options.detail) {
column.buttons.unshift({ text: "详情", onClick: (data) => {} });
}
if (options.modify) {
const modifyAction = column.buttons.find((i) => i.type === "modify");
if (modifyAction) {
const { onClick } = modifyAction;
modifyAction.onClick = (columnData) => {
const fn = (data: Record<string, any>) => getTable()?.openModifyModal(data);
if (isFunction(onClick)) {
onClick(columnData, fn);
} else {
fn(columnData);
}
};
} else {
column.buttons.unshift({
text: "修改",
onClick: (data) => getTable()?.openModifyModal(data),
});
}
}
column.buttons = column.buttons?.map((action) => {
let onClick = action?.onClick;
if (action.type === "delete") {
onClick = (data) => {
Modal.warning({
...TABLE_DELTE_DEFAULTS,
onOk: async () => {
const resData: any = await action?.onClick?.(data);
resData.msg && Message.success(resData?.msg || "");
getTable()?.loadData();
},
});
};
}
return { ...TABLE_ACTION_DEFAULTS, ...action, onClick } as any;
});
column.render = (columnData) =>
column.buttons?.map((action) => {
const onClick = () => action.onClick?.(columnData);
const omitKeys = ["text", "render", "api", "action", "onClick", "disabled"];
const disabled = () => action.disabled?.(columnData);
if (action.visible && !action.visible(columnData)) {
return null;
}
return (
<Link onClick={onClick} disabled={disabled()} {...omit(action as any, omitKeys)}>
{action.text}
</Link>
);
});
}
columns.push({ ...TABLE_COLUMN_DEFAULTS, ...column });
});
const itemsMap = options.common?.items?.reduce((map, item) => {
map[item.field] = item;
return map;
}, {} as any);
/**
*
*/
if (options.search && options.search.items) {
const searchItems: any[] = [];
options.search.items.forEach((item) => {
if (typeof item === "string") {
if (!itemsMap[item]) {
throw new Error(`search item ${item} not found in common items`);
}
searchItems.push(itemsMap[item]);
return;
}
if ("extend" in item && item.extend && itemsMap[item.extend]) {
searchItems.push(merge({}, itemsMap[item.extend], item));
return;
}
searchItems.push(item);
});
searchItems.push(searchItem);
options.search.items = searchItems;
}
if (options.create && propTruly(options.create, "extend")) {
options.create = merge(options.common, options.create);
}
if (options.modify && propTruly(options.modify, "extend")) {
options.modify = merge(options.common, options.modify);
}
return reactive({ ...options, columns });
};

View File

@ -0,0 +1 @@
export * from "./toast";

View File

@ -0,0 +1,42 @@
import { createVNode, render } from "vue";
import Toast from "./toast.vue";
export interface IToastOptions {
/**
*
* @default '正在操作中,请稍等...'
*/
message?: string;
/**
*
* @default 'icon-park-outline-loading-one'
*/
icon?: string;
/**
*
* @default true
*/
mask?: boolean;
/**
* ()
* @default false
*/
cover?: boolean;
}
export const toast = (messageOrOptions?: string | IToastOptions) => {
if (typeof messageOrOptions === "string") {
messageOrOptions = {
message: messageOrOptions,
};
}
const container = document.createElement("div");
const vnode = createVNode(Toast, messageOrOptions as any);
render(vnode, container);
document.body.appendChild(container);
const close = () => {
render(null, container);
document.body.removeChild(container);
};
return close;
};

View File

@ -0,0 +1,80 @@
<template>
<div class="toast">
<div class="toast-content">
<i :class="[icon, iconRotate && 'rotate']"></i>
<span>{{ message }}</span>
</div>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: "toast",
});
const props = defineProps({
message: {
type: String,
default: "正在操作中,请稍等...",
},
icon: {
type: String,
default: "icon-park-outline-loading-one",
},
iconRotate: {
type: Boolean,
default: true,
},
mask: {
type: Boolean,
default: false,
},
cover: {
type: Boolean,
default: true,
},
});
const style = computed(() => {
return {
pointerEvents: props.cover ? "initial" : "none",
backgroundColor: props.mask ? "rgba(0, 0, 0, 0.2)" : "transparent",
};
});
</script>
<style>
.toast {
position: fixed;
top: 0;
right: 0;
z-index: 9999;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
pointer-events: v-bind("style.pointerEvents");
background-color: v-bind("style.backgroundColor");
}
.toast-content {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border-radius: 3px;
background-color: rgba(0, 0, 0, 0.8);
color: #fff;
}
.rotate {
animation: rotate 2s linear infinite;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@ -0,0 +1,127 @@
type ArrayMap<
A extends readonly Item[],
K extends keyof A[number],
B extends string[] = []
> = A["length"] extends B["length"] ? B : ArrayMap<A, K, [...B, A[B["length"]][K]]>;
type ArrayFind<A extends readonly Item[], V extends A[number]["value"]> = A extends readonly [
infer I extends Item,
...infer R extends Item[]
]
? I["value"] extends V
? I["label"]
: ArrayFind<R, V>
: "nev1er";
type MergeIntersection<A> = A extends infer T ? { [Key in keyof T]: T[Key] } : never;
interface Item {
label: string;
value: any;
enumKey: string;
[key: string]: any;
}
type ConstantType<T extends readonly Item[]> = MergeIntersection<
{
/**
*
*/
[K in T[number] as K["enumKey"]]: K["value"];
} & {
/**
*
*/
items: T;
/**
*
*/
map: {
[k in T[number] as k["value"]]: k;
};
/**
*
*/
values: ArrayMap<T, "value">;
/**
*
*/
pick(...values: T[number]["value"][]): Item[];
/**
*
*/
omit(...values: T[number]["value"][]): Item[];
/**
*
*/
each<K extends keyof T[number]>(key: K): T[number][K][];
/**
*
* @param value value
* @param key label
*/
format<K extends T[number]["value"]>(value: K, key?: keyof T[number]): ArrayFind<T, K>;
}
>;
/**
*
*/
class Constanter {
// @ts-ignore
items: Item[];
pick(...values: any[]) {
return this.items.filter((item) => values.includes(item.value));
}
omit(...values: any[]) {
return this.items.filter((item) => !values.includes(item.value));
}
each(key: string) {
return this.items.map((item) => item[key]);
}
format(value: any, key: string = "label") {
return this.items.find((item) => item.value === value)?.[key];
}
}
/**
*
*/
export function defineConstants<T extends readonly Item[]>(items: T): ConstantType<T> {
const constants: any = {
items,
map: {},
values: [],
};
for (const item of items) {
constants[item.enumKey] = item.value;
constants.map[item.value] = item;
constants.values.push(item.value);
}
return Object.setPrototypeOf(constants, Constanter.prototype);
}
// const media = defineConstants([
// {
// label: "视频",
// value: 1,
// enumKey: "VIDEO",
// },
// {
// label: "图片",
// value: 2,
// enumKey: "IMAGE",
// },
// {
// label: "文本",
// value: 3,
// enumKey: "TEXT",
// },
// ] as const);
// console.log("media", media, media.VIDEO, media.IMAGE, media.TEXT);
// console.log("media pick", media.pick(media.VIDEO));
// console.log("media omit", media.omit(media.TEXT));
// console.log("media each", media.each("label"));
// console.log("media format", media.format(2));
// console.log("media maps", media.map);

16
src/main.ts Normal file
View File

@ -0,0 +1,16 @@
import { createApp } from "vue";
import App from "./App.vue";
import { router } from "./router";
import { store } from "./store";
import { style } from "./style";
const run = async () => {
const app = createApp(App);
app.use(store);
app.use(style);
app.use(router);
await router.isReady();
app.mount("#app");
};
run();

View File

@ -0,0 +1,43 @@
<template>
<div class="w-full h-full flex justify-center items-center p-4">
<div class="flex flex-col items-center">
<div>
<img src="@/assets/404.svg" alt="404" class="w-[320px]" />
</div>
<div class="text-center">
<!-- <h2 class="text-3xl m-0 font-bold">页面不存在</h2> -->
<p class="mt-0">抱歉页面未找到你访问的地址不存在!</p>
<div class="space-x-3 mt-5">
<a-button type="primary" @click="router.back()">
<template #icon>
<i class="icon-park-outline-back"></i>
</template>
返回上页
</a-button>
<a-button type="outline" @click="router.push('/')">
<template #icon>
<i class="icon-park-outline-home"></i>
</template>
返回首页
</a-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
const router = useRouter();
</script>
<style scoped></style>
<route lang="json">
{
"meta": {
"title": "404",
"icon": "icon-park-outline-home"
}
}
</route>

View File

@ -0,0 +1,60 @@
<script lang="tsx">
import { MenuItem, menus } from "@/router";
export default defineComponent({
name: "LayoutMenu",
setup() {
const selectedKeys = ref<string[]>([]);
const route = useRoute();
watch(
() => route.path,
() => {
selectedKeys.value = route.matched.map((i) => i.path);
},
{ immediate: true }
);
return { selectedKeys };
},
methods: {
goto(route: MenuItem) {
if (route.external) {
window.open(route.path, "_blank");
return;
}
this.$router.push(route);
},
renderItem(routes: MenuItem[], isTop = false) {
return routes.map((route) => {
const icon = route.icon && isTop ? () => <i class={route.icon} /> : null;
const node = route.children?.length ? (
<a-sub-menu key={route.path} v-slots={{ icon, title: () => route.title }}>
{this.renderItem(route?.children)}
</a-sub-menu>
) : (
<a-menu-item key={route.path} v-slots={{ icon }} onClick={() => this.goto(route)}>
{route.title}
</a-menu-item>
);
return node;
});
},
},
render() {
return (
<a-menu
style={{ width: "100%", height: "100%" }}
breakpoint="xl"
selectedKeys={this.selectedKeys}
autoOpenSelected={true}
>
{this.renderItem(menus, true)}
</a-menu>
);
},
});
</script>

202
src/pages/_app/index.vue Normal file
View File

@ -0,0 +1,202 @@
<template>
<a-layout class="layout">
<a-layout-header
class="h-13 overflow-hidden flex justify-between items-center gap-4 px-4 border-b border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700"
>
<div class="h-13 flex items-center border-b border-slate-200 dark:border-slate-800">
<router-link to="/" class="ml-1 flex items-center gap-2 text-slate-700">
<img src="/favicon.ico" alt="" width="20" height="20" />
<h1 class="text-lg leading-[19px] dark:text-white m-0 p-0">
{{ appStore.title }}
</h1>
</router-link>
</div>
<div class="flex items-center gap-4">
<a-tooltip v-for="btn in buttons" :key="btn.icon" :content="btn.tooltip">
<a-button shape="round" @click="btn.onClick">
<template #icon>
<i :class="btn.icon"></i>
</template>
</a-button>
</a-tooltip>
<a-dropdown>
<span class="cursor-pointer">
<a-avatar :size="28">
<img :src="userStore.avatar" :alt="userStore.name">
</a-avatar>
<span class="mx-2">
{{ userStore.name }}
</span>
<i class="icon-park-outline-down"></i>
</span>
<template #content>
<a-doption v-for="item in userButtons" :key="item.text" @click="item.onClick">
<template #icon>
<i :class="item.icon"></i>
</template>
{{ item.text }}
</a-doption>
</template>
</a-dropdown>
<a-drawer v-model:visible="themeConfig.visible" title="主题设置" :width="280"></a-drawer>
</div>
</a-layout-header>
<a-layout class="flex flex-1 overflow-hidden">
<a-layout-sider
class="h-full overflow-hidden dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700"
:width="208"
:collapsed-width="52"
:collapsible="true"
:collapsed="isCollapsed"
:hide-trigger="false"
@collapse="onCollapse"
>
<div class="">
<Menu />
</div>
</a-layout-sider>
<a-layout class="layout-content flex-1">
<a-layout-header class="h-8 bg-white border-b border-slate-200 dark:bg-slate-800 dark:border-slate-700">
<div class="h-full flex items-center gap-2 px-4">
<a-tag class="cursor-pointer">首页</a-tag>
</div>
</a-layout-header>
<a-layout-content class="overflow-x-auto">
<router-view v-slot="{ Component }">
<component :is="Component"></component>
</router-view>
</a-layout-content>
</a-layout>
</a-layout>
</a-layout>
</template>
<script lang="ts" setup>
import { useAppStore, useUserStore } from "@/store";
import { Message } from "@arco-design/web-vue";
import Menu from "./components/menu.vue";
const appStore = useAppStore();
const userStore = useUserStore();
const isCollapsed = ref(false);
const router = useRouter();
const themeConfig = ref({ visible: false });
const onCollapse = (val: boolean) => {
isCollapsed.value = val;
};
const buttons = [
{
icon: "icon-park-outline-moon",
tooltip: "点击切换主题色",
onClick: () => {
appStore.toggleDark();
},
},
{
icon: "icon-park-outline-config",
tooltip: "点击打开设置",
onClick: () => {
themeConfig.value.visible = true;
},
},
];
const userButtons = [
{
icon: "icon-park-outline-config",
text: "个人设置",
onClick: () => {
console.log("个人设置");
},
},
{
icon: "icon-park-outline-logout",
text: "退出登录",
onClick: async () => {
Message.loading({
content: '提示: 正在退出,请稍后...',
duration: 2000,
onClose: () => {
Message.success(`提示: 已成功退出登录!`)
router.push({ name: "_login" });
}
})
},
},
];
</script>
<style scoped lang="less">
@nav-size-height: 60px;
@layout-max-width: 1100px;
.layout {
display: flex;
width: 100%;
height: 100%;
overflow: hidden;
}
.layout-navbar {
position: fixed;
top: 0;
left: 0;
z-index: 100;
width: 100%;
height: @nav-size-height;
}
.layout-sider {
z-index: 99;
height: 100%;
overflow: hidden;
transition: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
> :deep(.arco-layout-sider-children) {
overflow-y: hidden;
}
}
.menu-wrapper {
height: 100%;
overflow: auto;
overflow-x: hidden;
:deep(.arco-menu) {
::-webkit-scrollbar {
width: 12px;
height: 4px;
}
::-webkit-scrollbar-thumb {
border: 4px solid transparent;
background-clip: padding-box;
border-radius: 7px;
background-color: var(--color-text-4);
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--color-text-3);
}
}
}
.layout-content {
min-height: 100vh;
overflow-y: hidden;
background-color: var(--color-fill-2);
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
}
</style>
<route lang="json">
{
"meta": {
"sort": 101,
"title": "首页",
"icon": "icon-park-outline-home"
}
}
</route>

102
src/pages/_login/index.vue Normal file
View File

@ -0,0 +1,102 @@
<template>
<div class="page-login w-full h-full flex items-center justify-center bg-white">
<div class="fixed flex items-center justify-between top-0 m-0 h-13 w-full px-10 z-10">
<!-- <div class="flex items-center">
<img src="/favicon.ico" alt="" width="20" height="20" class="mr-1" />
<h1 class="text-lg m-0">
{{ appStore.title }}
<span class="mx-1 text-slate-500">|</span>
<span class="text-slate-500 font-normal text-sm">{{ appStore.subtitle }}</span>
</h1>
</div>
<div>敬请期待</div> -->
</div>
<div
class="login-box relative mx-6 grid md:grid-cols-[1fr_500px] rounded overflow-hidden w-[1020px] h-[600px] border border-blue-100"
>
<div class="relative hidden md:block w-full h-full overflow-hidden bg-[#09f] px-4">
<img src="@/assets/td.svg" :alt="appStore.title" class="w-full h-full select-none" />
</div>
<div class="relative p-20 px-14 bg-white shadow-sm">
<div class="text-2xl">欢迎登陆</div>
<div class="text-base text-gray-500 mt-3">{{ meridiem }}欢迎登陆{{ appStore.title }}!</div>
<a-form ref="loginForm" :model="model" layout="vertical" class="mt-8">
<a-form-item field="username" label="账号" hide-asterisk>
<a-input v-model="model.username" placeholder="请输入账号/手机号/邮箱" allow-clear>
<template #prefix>
<i class="icon-park-outline-user" />
</template>
</a-input>
</a-form-item>
<a-form-item field="password" label="密码" hide-asterisk>
<a-input-password v-model="model.password" placeholder="请输入密码" allow-clear>
<template #prefix>
<i class="icon-park-outline-lock" />
</template>
</a-input-password>
</a-form-item>
<a-space :size="16" direction="vertical">
<div class="flex items-center justify-between">
<a-checkbox checked="rememberPassword">记住我</a-checkbox>
<a-link @click="onForgetPasswordClick">?</a-link>
</div>
<a-button type="primary" html-type="submit" long class="mt-2" :loading="loading" @click="onSubmitClick">
立即登录
</a-button>
<p type="text" long class="text-gray-400 text-center m-0">暂不支持其他方式登录</p>
</a-space>
</a-form>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { dayjs } from "@/plugins";
import { useAppStore } from "@/store";
import { Modal } from "@arco-design/web-vue";
import { reactive } from "vue";
const meridiem = dayjs.localeData().meridiem(dayjs().hour(), dayjs().minute());
const appStore = useAppStore();
const model = reactive({ username: "", password: "" });
const router = useRouter();
const loading = ref(false);
const onForgetPasswordClick = () => {
Modal.info({
title: "忘记密码?",
content: "如已忘记密码,请联系管理员进行密码重置!",
modalClass: "text-center",
maskClosable: false,
});
};
const onSubmitClick = async () => {
loading.value = true;
await new Promise((resolve) => setTimeout(resolve, 2000));
loading.value = false;
router.push({ path: "/" });
};
</script>
<style lang="less" scoped>
.page-login .bg {
background-image: url(@/assets/wave.svg);
filter: opacity(0.2);
background-color: #c9e5fc;
}
.login-box {
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1);
}
</style>
<route lang="json">
{
"meta": {
"sort": 101,
"title": "登录",
"icon": "icon-park-outline-home"
}
}
</route>

22
src/pages/demo/index.vue Normal file
View File

@ -0,0 +1,22 @@
<template>
<bread-page class="">Demo Page</bread-page>
</template>
<script setup lang="ts"></script>
<style scoped></style>
<route lang="json">
{
"meta": {
"sort": 10201,
"title": "测试页面1",
"icon": "icon-park-outline-add-subtract"
},
"parentMeta": {
"sort": 10201,
"title": "测试分类",
"icon": "icon-park-outline-add-subtract"
}
}
</route>

17
src/pages/demo/test.vue Normal file
View File

@ -0,0 +1,17 @@
<template>
<bread-page class="">Demo/test Page</bread-page>
</template>
<script setup lang="ts"></script>
<style scoped></style>
<route lang="json">
{
"meta": {
"sort": 10202,
"title": "测试页面2",
"icon": "icon-park-outline-aperture-priority"
}
}
</route>

View File

@ -0,0 +1,164 @@
<template>
<div class="m-4 p-4 bg-white">
<Form v-bind="form"></Form>
</div>
</template>
<script setup lang="tsx">
import { Form, useForm } from "@/components";
const sleep = (wait: number) => new Promise((res) => setTimeout(res, wait));
const form = useForm({
items: [
{
field: "username",
label: "姓名",
type: "input",
required: true,
itemProps: {
hideLabel: false,
},
rules: ["password"],
},
{
field: "nickname",
label: "昵称",
type: "input",
disable: ({ model }) => !model.username,
rules: [
{
message: "昵称不能超过 10 个字符",
required: true,
disable: ({ model }) => !model.username,
},
],
},
{
field: "password",
label: "密码",
type: "password",
visible: ({ model }) => model.username,
nodeProps: {
class: "w-full",
},
},
{
field: "gender",
label: "性别",
type: "select",
options: [
{
label: "男",
value: 1,
},
{
label: "女",
value: 2,
},
],
},
{
field: "startTime:endTime",
label: "时间",
type: "time",
nodeProps: {
type: "time-range",
},
help: "时间段",
},
{
field: "startDate:endDate",
label: "日期",
type: "dateRange",
},
{
field: "checkbox",
label: "多选",
type: "checkbox",
options: [
{
label: "选项1",
value: 1,
},
{
label: "选项2",
value: 2,
},
],
},
{
field: "radio",
label: "单选",
type: "radio",
options: [
{
label: "选项1",
value: 1,
},
{
label: "选项2",
value: 2,
},
],
},
{
field: "slider",
label: "音量",
type: "slider",
},
{
field: "provice:city:town",
label: "城市",
type: "cascader",
nodeProps: {
checkStrictly: true,
pathMode: true,
},
options: [
{
label: "广西",
value: "gx",
children: [
{
label: "南宁",
value: "nn",
},
{
label: "桂林",
value: "gl",
children: [
{
label: "阳朔",
value: "ys",
},
{
label: "临桂",
value: "lg",
},
],
},
],
},
],
},
],
submit: async ({ model }) => {
await sleep(3000);
console.log("submit", model);
return { message: "操作成功" };
},
});
</script>
<style scoped></style>
<route lang="json">
{
"meta": {
"sort": 10101,
"title": "首页111",
"icon": "icon-park-outline-home"
}
}
</route>

161
src/pages/home/index.vue Normal file
View File

@ -0,0 +1,161 @@
<template>
<div class="m-4 p-4 bg-white">
<Table v-bind="table"></Table>
</div>
</template>
<script setup lang="tsx">
import { ContentType, api } from "@/api";
import { Table, useTable } from "@/components";
import { dayjs } from "@/plugins";
import { Avatar } from "@arco-design/web-vue";
const url = ref<any>(null);
const table = useTable({
data: async () => {
return [];
},
columns: [
// {
// type: 'index'
// },
{
title: "姓名",
dataIndex: "username",
width: 200,
render: ({ record }) => {
return (
<div class="flex items-center gap-2 w-full">
<div>
<Avatar size={32}>
<img src={record.avatar} width={32} height={32} />
</Avatar>
</div>
<div class="flex-1 overflow-hidden">
<span class="ml-0">{record.nickname}</span>
<div class="text-xs text-gray-400 mt-1 truncate">{record.description}</div>
</div>
</div>
);
},
},
{
title: "昵称",
dataIndex: "username",
},
{
title: "昵称",
dataIndex: "username",
width: 200,
render: ({ record }) => {
return (
<div class="">
<span class="ml-0">{record.username}</span>
<div class="text-xs text-gray-400 mt-1 truncate">创建于 {dayjs(record.createAt).format()}</div>
</div>
);
},
},
{
title: "操作",
type: "button",
width: 70,
buttons: [],
},
],
common: {
modalProps: {
width: 432,
maskClosable: false,
},
formProps: {
layout: "vertical",
},
model: {
avatar: "11",
},
items: [
{
field: "username",
label: "姓名",
type: "input",
required: true,
},
{
field: "nickname",
label: "昵称",
type: "input",
},
{
field: "password",
label: "密码",
type: "password",
},
{
label: "头像",
field: "avatar",
type: "input",
component: ({ model, field }) => {
const onInputChange = (e: Event) => {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) {
return;
}
model[field] = file;
console.log(file);
const reader = new FileReader();
reader.onload = (e) => {
url.value = e.target?.result;
};
reader.readAsDataURL(file);
};
return (
<div class="w-full h-12 flex gap-4 items-center justify-between">
<input type="file" onChange={onInputChange} class="flex-1" />
{url.value && (
<a-avatar size={40}>
<img src={url.value} />
</a-avatar>
)}
</div>
);
},
},
],
},
search: {
items: [
{
field: "username",
label: "姓名",
type: "input",
},
],
},
create: {
title: "新建用户",
submit: ({ model }) => {
return api.user.createUser(model, {
type: ContentType.FormData,
});
},
},
modify: {
title: "修改用户",
},
});
</script>
<style scoped></style>
<route lang="json">
{
"meta": {
"sort": 10101,
"title": "首页",
"icon": "icon-park-outline-home"
}
}
</route>

21
src/pages/post/index.vue Normal file
View File

@ -0,0 +1,21 @@
<template>
<div class="m-4 p-4 bg-white">
Post Page
</div>
</template>
<script setup lang="tsx" name="PostPage">
</script>
<style scoped></style>
<route lang="json">
{
"meta": {
"sort": 10401,
"title": "文章管理",
"icon": "icon-park-outline-document-folder"
}
}
</route>

117
src/pages/user/index.vue Normal file
View File

@ -0,0 +1,117 @@
<template>
<BreadPage>
<Table v-bind="table"></Table>
</BreadPage>
</template>
<script setup lang="tsx">
import { ContentType, api } from "@/api";
import { useTable } from "@/components";
const table = useTable({
data: async (model, paging) => [],
columns: [
{
title: "姓名",
dataIndex: "username",
width: 200,
},
{
title: "昵称",
dataIndex: "name",
},
{
title: "创建时间",
dataIndex: "createdAt",
width: 200,
},
{
title: "操作",
type: "button",
width: 70,
buttons: [
{
type: "modify",
text: "修改",
},
],
},
],
common: {
model: {
avatarUrl: "",
},
items: [
{
field: "username",
label: "姓名1",
type: "input",
required: true,
},
{
field: "nickname",
label: "昵称",
type: "input",
},
{
field: "description",
label: "个人描述",
type: "input",
},
{
field: "password",
label: "密码",
type: "password",
},
{
label: "头像",
field: "avatar?avatarUrl",
type: "input",
},
],
modalProps: {
width: 772,
maskClosable: false,
},
formProps: {
layout: "vertical",
class: "!grid grid-cols-2 gap-x-3",
},
},
search: {
items: [
{
extend: "username",
required: false,
},
],
},
create: {
title: "新建用户",
submit: ({ model }) => {
return api.user.createUser(model as any, {
type: ContentType.FormData,
});
},
},
modify: {
extend: true,
title: "修改用户",
submit: ({ model }) => {
return api.user.updateUser(model.id, model);
},
},
});
</script>
<style scoped></style>
<route lang="json">
{
"meta": {
"sort": 10301,
"title": "用户管理",
"icon": "icon-park-outline-user"
}
}
</route>

View File

@ -0,0 +1,67 @@
import dayjs from "dayjs";
import "dayjs/locale/zh-cn";
import localData from "dayjs/plugin/localeData";
import relativeTime from "dayjs/plugin/relativeTime";
/**
*
*
*/
const DATETIME = "YYYY-MM-DD HH:mm:ss";
/**
*
*/
const DATE = "YYYY-MM-DD";
/**
*
*/
const TIME = "HH:mm:ss";
/**
*
*/
dayjs.locale("zh-cn");
/**
*
* @see https://dayjs.gitee.io/docs/zh-CN/plugin/relative-time
*/
dayjs.extend(relativeTime);
/**
*
* @see https://dayjs.gitee.io/docs/zh-CN/plugin/locale-data
*/
dayjs.extend(localData);
/**
*
*
*/
dayjs.DATETIME = DATETIME;
/**
*
*/
dayjs.DATE = DATE;
/**
*
*/
dayjs.TIME = TIME;
/**
* formatformat使
*/
dayjs.prototype._format = dayjs.prototype.format;
dayjs.prototype.format = function (format?: string) {
if (format) {
return this._format(format);
}
return this._format(dayjs.DATETIME);
};
export { dayjs, DATETIME, DATE, TIME };

Some files were not shown because too many files have changed in this diff Show More