Compare commits
4 Commits
2a27f67b85
...
3f0c83a83b
| Author | SHA1 | Date |
|---|---|---|
|
|
3f0c83a83b | |
|
|
4a97226826 | |
|
|
226cdffea7 | |
|
|
fd7b437102 |
4842
.gitea/stat.html
4842
.gitea/stat.html
File diff suppressed because one or more lines are too long
64
README.md
64
README.md
|
|
@ -56,7 +56,7 @@ pnpm dev
|
||||||
|
|
||||||
- 以文件夹为路由,读取该文件夹下 index.vue 的信息作为路由信息,其他文件会跳过,可以包含子文件夹作为嵌套路由
|
- 以文件夹为路由,读取该文件夹下 index.vue 的信息作为路由信息,其他文件会跳过,可以包含子文件夹作为嵌套路由
|
||||||
- 在 src/pages 的文件夹层级,作为菜单层级,路由层级最终只有 2 层(配合 keep-alive 缓存)
|
- 在 src/pages 的文件夹层级,作为菜单层级,路由层级最终只有 2 层(配合 keep-alive 缓存)
|
||||||
- 在 src/pages 目录下,以 _ 开头的文件夹作为 1 级路由,其他作为 2 级路由,也就是应用路由
|
- 在 src/pages 目录下,以 \_ 开头的文件夹作为 1 级路由,其他作为 2 级路由,也就是应用路由
|
||||||
- 子文件夹下,只有 index.vue 文件有效,其他文件会忽略,这些文件可以作为子组件使用
|
- 子文件夹下,只有 index.vue 文件有效,其他文件会忽略,这些文件可以作为子组件使用
|
||||||
- components 目录会被忽视。
|
- components 目录会被忽视。
|
||||||
- xxx.xx.xx 文件会被忽视,例如 index.my.vue 文件。
|
- xxx.xx.xx 文件会被忽视,例如 index.my.vue 文件。
|
||||||
|
|
@ -76,7 +76,7 @@ pnpm dev
|
||||||
|
|
||||||
目前支持的参数,如下:
|
目前支持的参数,如下:
|
||||||
|
|
||||||
```ts
|
````ts
|
||||||
interface RouteMeta {
|
interface RouteMeta {
|
||||||
/**
|
/**
|
||||||
* 页面标题
|
* 页面标题
|
||||||
|
|
@ -140,7 +140,7 @@ interface RouteMeta {
|
||||||
*/
|
*/
|
||||||
link?: string;
|
link?: string;
|
||||||
}
|
}
|
||||||
```
|
````
|
||||||
|
|
||||||
### 嵌套布局
|
### 嵌套布局
|
||||||
|
|
||||||
|
|
@ -174,8 +174,6 @@ interface RouteMeta {
|
||||||
|
|
||||||
用户登陆后获取的权限,应存储在 userStore.auth 字段中,在路由的 beforeEach 守卫中,会比较两个是否匹配,匹配上则继续,否则会显示如下 403 页面:
|
用户登陆后获取的权限,应存储在 userStore.auth 字段中,在路由的 beforeEach 守卫中,会比较两个是否匹配,匹配上则继续,否则会显示如下 403 页面:
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 动态路由
|
### 动态路由
|
||||||
|
|
||||||
相比于比较流行的加法挂载,我更倾向于减法挂载。即默认加载完所有路由,在 beforeEach 钩子根据权限移除不必要的路由。
|
相比于比较流行的加法挂载,我更倾向于减法挂载。即默认加载完所有路由,在 beforeEach 钩子根据权限移除不必要的路由。
|
||||||
|
|
@ -190,20 +188,11 @@ interface RouteMeta {
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script>
|
<script>
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "MyPage"
|
name: 'MyPage',
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
<route>
|
<route> { "meta": { // 组件名字 "name": "MyPage", // 开启缓存 "cache": true } } </route>
|
||||||
{
|
|
||||||
"meta": {
|
|
||||||
// 组件名字
|
|
||||||
"name": "MyPage",
|
|
||||||
// 开启缓存
|
|
||||||
"cache": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</route>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 条件加载
|
### 条件加载
|
||||||
|
|
@ -218,7 +207,7 @@ VITE_EXTENSION = my
|
||||||
|
|
||||||
### 图标样式
|
### 图标样式
|
||||||
|
|
||||||
基于 [UnoCSS]() 插件,可使用类似 TailwindCSS 的原子样式快速开发,同时默认安装icon-park-outline 图标库,只需引用类名即可得到 SVG 图标。这在路由菜单等需要动态渲染时非常有用,同时所有样式类和图标类都是按需打包的,示例:
|
基于 [UnoCSS]() 插件,可使用类似 TailwindCSS 的原子样式快速开发,同时默认安装 icon-park-outline 图标库,只需引用类名即可得到 SVG 图标。这在路由菜单等需要动态渲染时非常有用,同时所有样式类和图标类都是按需打包的,示例:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<i class="text-sm icon-park-outline-home" />
|
<i class="text-sm icon-park-outline-home" />
|
||||||
|
|
@ -262,13 +251,13 @@ enum MediaEnum {
|
||||||
const media = defineConstants([
|
const media = defineConstants([
|
||||||
{
|
{
|
||||||
value: MediaEnum.VIDEO,
|
value: MediaEnum.VIDEO,
|
||||||
label: "视频",
|
label: '视频',
|
||||||
color: "red",
|
color: 'red',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: MediaEnum.IMAGE,
|
value: MediaEnum.IMAGE,
|
||||||
label: "图片",
|
label: '图片',
|
||||||
color: "blue",
|
color: 'blue',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -288,7 +277,7 @@ media.val(); // [1, 2]
|
||||||
<table ref="tableRef" v-bind="table" />
|
<table ref="tableRef" v-bind="table" />
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { Table, useTable } from "@/components";
|
import { Table, useTable } from '@/components';
|
||||||
|
|
||||||
const table = useTable({
|
const table = useTable({
|
||||||
// 数据源配置,可以是数组或返回对象的异步函数
|
// 数据源配置,可以是数组或返回对象的异步函数
|
||||||
|
|
@ -299,8 +288,8 @@ media.val(); // [1, 2]
|
||||||
// 表格列配置
|
// 表格列配置
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: "用户名",
|
title: '用户名',
|
||||||
dataIndex: "name",
|
dataIndex: 'name',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
@ -313,21 +302,21 @@ media.val(); // [1, 2]
|
||||||
search: {
|
search: {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: "username",
|
field: 'username',
|
||||||
label: "用户名",
|
label: '用户名',
|
||||||
type: "input",
|
type: 'input',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// 新增表单弹窗的配置,类型为useFormModal的入参
|
// 新增表单弹窗的配置,类型为useFormModal的入参
|
||||||
create: {
|
create: {
|
||||||
title: "新增用户",
|
title: '新增用户',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: "username",
|
field: 'username',
|
||||||
label: "用户名",
|
label: '用户名',
|
||||||
type: "input",
|
type: 'input',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
submit: async ({ model }) => {
|
submit: async ({ model }) => {
|
||||||
|
|
@ -337,12 +326,12 @@ media.val(); // [1, 2]
|
||||||
|
|
||||||
// 修改表单弹窗的配置,类型为useFormModal的入参
|
// 修改表单弹窗的配置,类型为useFormModal的入参
|
||||||
modify: {
|
modify: {
|
||||||
title: "修改用户",
|
title: '修改用户',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: "username",
|
field: 'username',
|
||||||
label: "用户名",
|
label: '用户名',
|
||||||
type: "input",
|
type: 'input',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
submit: async ({ model }) => {
|
submit: async ({ model }) => {
|
||||||
|
|
@ -352,6 +341,7 @@ media.val(); // [1, 2]
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
提示:以上每个参数都有类型提示,原组件每个参数都可透传,封装遵循扩展而非限制原则。
|
提示:以上每个参数都有类型提示,原组件每个参数都可透传,封装遵循扩展而非限制原则。
|
||||||
|
|
||||||
### 自动导入
|
### 自动导入
|
||||||
|
|
|
||||||
77
index.html
77
index.html
|
|
@ -9,13 +9,16 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" class="dark:bg-slate-900 dark:text-slate-200">
|
<div id="app" class="dark:bg-slate-900 dark:text-slate-200">
|
||||||
<div class="cube">
|
<div class="loading">
|
||||||
<div></div>
|
<img
|
||||||
<div></div>
|
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHN0eWxlPSJtYXJnaW46IGF1dG87IGJhY2tncm91bmQ6IHJnYigyNTUsIDI1NSwgMjU1KTsgZGlzcGxheTogYmxvY2s7IHNoYXBlLXJlbmRlcmluZzogYXV0bzsiIHdpZHRoPSIyMDBweCIgaGVpZ2h0PSIyMDBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1MCA1MCkiPjxnPjxhbmltYXRlVHJhbnNmb3JtIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIgdHlwZT0icm90YXRlIiB2YWx1ZXM9IjA7NDUiIGtleVRpbWVzPSIwOzEiIGR1cj0iMC4ycyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiPjwvYW5pbWF0ZVRyYW5zZm9ybT48cGF0aCBkPSJNMjkuNDkxNTI0MjA2MTE3MjU1IC01LjUgTDM3LjQ5MTUyNDIwNjExNzI1NSAtNS41IEwzNy40OTE1MjQyMDYxMTcyNTUgNS41IEwyOS40OTE1MjQyMDYxMTcyNTUgNS41IEEzMCAzMCAwIDAgMSAyNC43NDI3NDQwNTAxOTg3MzggMTYuOTY0NTY5NDU3MTQ2NzEyIEwyNC43NDI3NDQwNTAxOTg3MzggMTYuOTY0NTY5NDU3MTQ2NzEyIEwzMC4zOTk1OTgyOTk2OTExMTcgMjIuNjIxNDIzNzA2NjM5MDkyIEwyMi42MjE0MjM3MDY2MzkwOTYgMzAuMzk5NTk4Mjk5NjkxMTE0IEwxNi45NjQ1Njk0NTcxNDY3MTYgMjQuNzQyNzQ0MDUwMTk4NzM0IEEzMCAzMCAwIDAgMSA1LjUgMjkuNDkxNTI0MjA2MTE3MjU1IEw1LjUgMjkuNDkxNTI0MjA2MTE3MjU1IEw1LjUgMzcuNDkxNTI0MjA2MTE3MjU1IEwtNS40OTk5OTk5OTk5OTk5OTcgMzcuNDkxNTI0MjA2MTE3MjU1IEwtNS40OTk5OTk5OTk5OTk5OTcgMjkuNDkxNTI0MjA2MTE3MjU1IEEzMCAzMCAwIDAgMSAtMTYuOTY0NTY5NDU3MTQ2NzA1IDI0Ljc0Mjc0NDA1MDE5ODczOCBMLTE2Ljk2NDU2OTQ1NzE0NjcwNSAyNC43NDI3NDQwNTAxOTg3MzggTC0yMi42MjE0MjM3MDY2MzkwODUgMzAuMzk5NTk4Mjk5NjkxMTE3IEwtMzAuMzk5NTk4Mjk5NjkxMTE3IDIyLjYyMTQyMzcwNjYzOTA5MiBMLTI0Ljc0Mjc0NDA1MDE5ODczOCAxNi45NjQ1Njk0NTcxNDY3MTIgQTMwIDMwIDAgMCAxIC0yOS40OTE1MjQyMDYxMTcyNTUgNS41MDAwMDAwMDAwMDAwMDkgTC0yOS40OTE1MjQyMDYxMTcyNTUgNS41MDAwMDAwMDAwMDAwMDkgTC0zNy40OTE1MjQyMDYxMTcyNTUgNS41MDAwMDAwMDAwMDAwMSBMLTM3LjQ5MTUyNDIwNjExNzI1NSAtNS41MDAwMDAwMDAwMDAwMDEgTC0yOS40OTE1MjQyMDYxMTcyNTUgLTUuNTAwMDAwMDAwMDAwMDAyIEEzMCAzMCAwIDAgMSAtMjQuNzQyNzQ0MDUwMTk4NzM4IC0xNi45NjQ1Njk0NTcxNDY3MDUgTC0yNC43NDI3NDQwNTAxOTg3MzggLTE2Ljk2NDU2OTQ1NzE0NjcwNSBMLTMwLjM5OTU5ODI5OTY5MTExNyAtMjIuNjIxNDIzNzA2NjM5MDg1IEwtMjIuNjIxNDIzNzA2NjM5MDkyIC0zMC4zOTk1OTgyOTk2OTExMTcgTC0xNi45NjQ1Njk0NTcxNDY3MTIgLTI0Ljc0Mjc0NDA1MDE5ODczOCBBMzAgMzAgMCAwIDEgLTUuNTAwMDAwMDAwMDAwMDExIC0yOS40OTE1MjQyMDYxMTcyNTUgTC01LjUwMDAwMDAwMDAwMDAxMSAtMjkuNDkxNTI0MjA2MTE3MjU1IEwtNS41MDAwMDAwMDAwMDAwMTIgLTM3LjQ5MTUyNDIwNjExNzI1NSBMNS40OTk5OTk5OTk5OTk5OTggLTM3LjQ5MTUyNDIwNjExNzI1NSBMNS41IC0yOS40OTE1MjQyMDYxMTcyNTUgQTMwIDMwIDAgMCAxIDE2Ljk2NDU2OTQ1NzE0NjcwMiAtMjQuNzQyNzQ0MDUwMTk4NzQgTDE2Ljk2NDU2OTQ1NzE0NjcwMiAtMjQuNzQyNzQ0MDUwMTk4NzQgTDIyLjYyMTQyMzcwNjYzOTA4IC0zMC4zOTk1OTgyOTk2OTExMiBMMzAuMzk5NTk4Mjk5NjkxMTE3IC0yMi42MjE0MjM3MDY2MzkxIEwyNC43NDI3NDQwNTAxOTg3MzggLTE2Ljk2NDU2OTQ1NzE0NjcxNiBBMzAgMzAgMCAwIDEgMjkuNDkxNTI0MjA2MTE3MjU1IC01LjUwMDAwMDAwMDAwMDAxMyBNMCAtMjBBMjAgMjAgMCAxIDAgMCAyMCBBMjAgMjAgMCAxIDAgMCAtMjAiIGZpbGw9IiMwOWYiPjwvcGF0aD48L2c+PC9nPjwvc3ZnPgo="
|
||||||
<div></div>
|
alt="loading"
|
||||||
<div></div>
|
class="loading-image"
|
||||||
<div></div>
|
width="64"
|
||||||
<div></div>
|
height="64"
|
||||||
|
/>
|
||||||
|
<h1 class="loading-title">欢迎访问%VITE_TITLE%</h1>
|
||||||
|
<div class="loading-tip">资源加载中, 请稍等...</div>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
html,
|
html,
|
||||||
|
|
@ -36,51 +39,27 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
@keyframes cube {
|
.loading {
|
||||||
0% {
|
display: flex;
|
||||||
transform: rotate(45deg) rotateX(-25deg) rotateY(25deg);
|
flex-direction: column;
|
||||||
}
|
align-items: center;
|
||||||
50% {
|
justify-content: center;
|
||||||
transform: rotate(45deg) rotateX(-385deg) rotateY(25deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(45deg) rotateX(-385deg) rotateY(385deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.cube {
|
.loading-image {
|
||||||
animation: cube 2s infinite ease;
|
width: 64px;
|
||||||
height: 40px;
|
height: 64px;
|
||||||
transform-style: preserve-3d;
|
|
||||||
width: 40px;
|
|
||||||
}
|
}
|
||||||
.cube div {
|
.loading-title {
|
||||||
background-color: rgba(255, 255, 255, 0.25);
|
margin: 0;
|
||||||
height: 100%;
|
margin-top: 20px;
|
||||||
position: absolute;
|
font-size: 22px;
|
||||||
width: 100%;
|
font-weight: 400;
|
||||||
border: 2px solid #000;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.cube div:nth-of-type(1) {
|
.loading-tip {
|
||||||
transform: translateZ(-20px) rotateY(180deg);
|
margin-top: 12px;
|
||||||
}
|
line-height: 1;
|
||||||
.cube div:nth-of-type(2) {
|
color: #889;
|
||||||
transform: rotateY(-270deg) translateX(50%);
|
|
||||||
transform-origin: top right;
|
|
||||||
}
|
|
||||||
.cube div:nth-of-type(3) {
|
|
||||||
transform: rotateY(270deg) translateX(-50%);
|
|
||||||
transform-origin: center left;
|
|
||||||
}
|
|
||||||
.cube div:nth-of-type(4) {
|
|
||||||
transform: rotateX(90deg) translateY(-50%);
|
|
||||||
transform-origin: top center;
|
|
||||||
}
|
|
||||||
.cube div:nth-of-type(5) {
|
|
||||||
transform: rotateX(-90deg) translateY(50%);
|
|
||||||
transform-origin: bottom center;
|
|
||||||
}
|
|
||||||
.cube div:nth-of-type(6) {
|
|
||||||
transform: translateZ(20px);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
"@vitejs/plugin-vue": "^5.0.3",
|
"@vitejs/plugin-vue": "^5.0.3",
|
||||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||||
"@vueuse/core": "^10.7.1",
|
"@vueuse/core": "^10.7.1",
|
||||||
|
"arconify": "^0.0.2",
|
||||||
"axios": "^1.6.5",
|
"axios": "^1.6.5",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"dplayer": "^1.27.1",
|
"dplayer": "^1.27.1",
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,10 @@ const getBuildInfo = async () => {
|
||||||
const version = commits ? `${latestTag}.${commits}` : `v${pkg.version}`;
|
const version = commits ? `${latestTag}.${commits}` : `v${pkg.version}`;
|
||||||
const content = `欢迎访问!版本: ${version} 标识: ${hash} 构建: ${time}`;
|
const content = `欢迎访问!版本: ${version} 标识: ${hash} 构建: ${time}`;
|
||||||
const style = `"color: #09f; font-weight: 900;", "font-size: 12px; color: #09f; font-family: ''"`;
|
const style = `"color: #09f; font-weight: 900;", "font-size: 12px; color: #09f; font-family: ''"`;
|
||||||
const vString = `var __APP_VERSION__ = '${version}';\n`;
|
const vString = `\n var __APP_VERSION__ = '${version}';\n`;
|
||||||
const hString = `var __APP_HASH__ = '${hash}';\n`;
|
const hString = ` var __APP_HASH__ = '${hash}';\n`;
|
||||||
const dString = `var __APP_DATE__ = '${time}';\n`;
|
const dString = ` var __APP_DATE__ = '${time}';\n`;
|
||||||
const lString = `console.log(\`%c${LOGO} \n%c${content}\n\`, ${style});\n`;
|
const lString = ` console.log(\`%c${LOGO} \n%c${content}\n\`, ${style});\n`;
|
||||||
return vString + hString + dString + lString;
|
return vString + hString + dString + lString;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
export function onRoutesGenerated(routes: RouteRecordRaw[], mode: string) {
|
||||||
|
const isProd = mode !== 'development';
|
||||||
|
const result = [];
|
||||||
|
for (const route of routes) {
|
||||||
|
const { hide } = route.meta ?? {};
|
||||||
|
if (!route.meta) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (hide === true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isProd && hide === 'prod') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.push(route);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,6 @@ export function addAuthInterceptor(axios: AxiosInstance) {
|
||||||
if (userStore.accessToken) {
|
if (userStore.accessToken) {
|
||||||
config.headers.Authorization = `Bearer ${userStore.accessToken}`;
|
config.headers.Authorization = `Bearer ${userStore.accessToken}`;
|
||||||
}
|
}
|
||||||
// throw Error('dd');
|
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { dayjs } from "@/libs/dayjs";
|
import dayjs from "dayjs";
|
||||||
import { PropType } from "vue";
|
import { PropType } from "vue";
|
||||||
import { FontRender } from "../font";
|
import { FontRender } from "../font";
|
||||||
import { Date } from "./interface";
|
import { Date } from "./interface";
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { dayjs } from '@/libs/dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
import { PropType, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { FontRender } from '../font';
|
import { FontRender } from '../font';
|
||||||
import { Time } from './interface';
|
import { Time } from './interface';
|
||||||
import { PropType } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
|
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
import { Form, FormInstance, Message } from '@arco-design/web-vue';
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { ComputedRef, InjectionKey, PropType, Ref } from 'vue';
|
|
||||||
import { initFormItems } from '../utils/useFormItems';
|
|
||||||
import { FormRef, useFormRef } from '../utils/useFormRef';
|
|
||||||
import { AnFormItem, AnFormItemProps } from './FormItem';
|
|
||||||
import { cloneDeep, isFunction, isObject, merge } from 'lodash-es';
|
|
||||||
import { getModel } from '../utils/useFormModel';
|
|
||||||
|
|
||||||
const SUBMIT_ITEM = {
|
|
||||||
field: 'id',
|
|
||||||
setter: 'submit' as const,
|
|
||||||
itemProps: {
|
|
||||||
hideLabel: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FormContextInterface = FormRef & {
|
|
||||||
model: Ref<Recordable>;
|
|
||||||
items: ComputedRef<AnFormItemProps[]>;
|
|
||||||
loading: Ref<boolean>;
|
|
||||||
submitForm: any;
|
|
||||||
resetForm: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FormContextKey = Symbol('FormContextKey') as InjectionKey<FormContextInterface>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表单组件
|
|
||||||
*/
|
|
||||||
export const AnForm = defineComponent({
|
|
||||||
name: 'AnForm',
|
|
||||||
props: {
|
|
||||||
/**
|
|
||||||
* 表单数据
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* {
|
|
||||||
* id: undefined
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
model: {
|
|
||||||
type: Object as PropType<Recordable>,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 表单项
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* [{
|
|
||||||
* field: 'name',
|
|
||||||
* label: '昵称',
|
|
||||||
* setter: 'input'
|
|
||||||
* }]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
items: {
|
|
||||||
type: Array as PropType<AnFormItemProps[]>,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 提交表单
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* (model) => api.user.addUser(model)
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
submit: {
|
|
||||||
type: [Function, Object] as PropType<AnFormSubmit>,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 传给Form组件的参数
|
|
||||||
* @exmaple
|
|
||||||
* ```ts
|
|
||||||
* {
|
|
||||||
* layout: 'vertical'
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
formProps: {
|
|
||||||
type: Object as PropType<Omit<FormInstance['$props'], 'model' | 'ref'>>,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['update:model'],
|
|
||||||
setup(props, { slots, emit }) {
|
|
||||||
const model = useVModel(props, 'model', emit);
|
|
||||||
const items = computed(() => props.items);
|
|
||||||
const initModel = cloneDeep(model.value);
|
|
||||||
const loading = ref(false);
|
|
||||||
const { formRef, ...formMethods } = useFormRef();
|
|
||||||
|
|
||||||
const submitItem = () => {
|
|
||||||
if (!props.submit) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (isFunction(props.submit)) {
|
|
||||||
return SUBMIT_ITEM;
|
|
||||||
}
|
|
||||||
if (isObject(props.submit)) {
|
|
||||||
return merge({}, SUBMIT_ITEM, props.submit);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
model.value = cloneDeep(initModel);
|
|
||||||
formRef.value?.clearValidate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitForm = async () => {
|
|
||||||
if (await formRef.value?.validate()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const submit: any = typeof props.submit === 'object' ? props.submit.visible : props.submit;
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
const data = getModel(model.value);
|
|
||||||
const res = await submit?.(data, props.items);
|
|
||||||
const msg = res?.data?.message;
|
|
||||||
msg && Message.success(`提示: ${msg}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const context = { slots, loading, resetForm, submitForm, submitItem, model, items, formRef, ...formMethods };
|
|
||||||
provide(FormContextKey, context);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initFormItems(props.items, model.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
return context;
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Form layout="vertical" {...this.formProps} class="an-form" ref="formRef" model={this.model}>
|
|
||||||
{this.items.map(item => (
|
|
||||||
<AnFormItem key={item.field} item={item} items={this.items} model={this.model}></AnFormItem>
|
|
||||||
))}
|
|
||||||
{this.submitItem()}
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type AnFormInstance = InstanceType<typeof AnForm>;
|
|
||||||
|
|
||||||
export type AnFormProps = Pick<AnFormInstance['$props'], 'model' | 'items' | 'submit' | 'formProps'>;
|
|
||||||
|
|
||||||
export type AnFormSubmitFn = (model: Recordable, items: AnFormItemProps[]) => any;
|
|
||||||
|
|
||||||
export type AnFormSubmit = AnFormSubmitFn | AnFormItemProps;
|
|
||||||
|
|
@ -1,226 +0,0 @@
|
||||||
import { FormItem, FieldRule, FormItemInstance, SelectOptionData, SelectOptionGroup } from '@arco-design/web-vue';
|
|
||||||
import { InjectionKey, PropType, provide } from 'vue';
|
|
||||||
import { SetterItem, SetterType, setterMap } from './FormSetter';
|
|
||||||
|
|
||||||
export const FormItemContextKey = Symbol('FormItemContextKey') as InjectionKey<AnFormItemFnProps>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表单项
|
|
||||||
*/
|
|
||||||
export const AnFormItem = defineComponent({
|
|
||||||
name: 'AnFormItem',
|
|
||||||
props: {
|
|
||||||
/**
|
|
||||||
* 表单项
|
|
||||||
*/
|
|
||||||
item: {
|
|
||||||
type: Object as PropType<AnFormItemProps>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 表单项数组
|
|
||||||
*/
|
|
||||||
items: {
|
|
||||||
type: Array as PropType<AnFormItemProps[]>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 表单数据
|
|
||||||
*/
|
|
||||||
model: {
|
|
||||||
type: Object as PropType<Recordable>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const rules = computed(() => props.item.rules?.filter(i => !i.disable?.(props)));
|
|
||||||
const disabled = computed(() => Boolean(props.item.disable?.(props)));
|
|
||||||
|
|
||||||
const setterSlots = (() => {
|
|
||||||
const slots = props.item.setterSlots;
|
|
||||||
if (!slots) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const items: Recordable = {};
|
|
||||||
for (const [name, Slot] of Object.entries(slots)) {
|
|
||||||
items[name] = (p: Recordable) => <Slot {...p} {...props} />;
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const itemSlots = (() => {
|
|
||||||
const Setter = setterMap[props.item.setter as SetterType]?.setter as any;
|
|
||||||
const slots = props.item.itemSlots;
|
|
||||||
if (!slots && !Setter) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const SetterRender = () => (
|
|
||||||
<Setter {...props.item.setterProps} v-model={props.model[props.item.field]}>
|
|
||||||
{setterSlots}
|
|
||||||
</Setter>
|
|
||||||
);
|
|
||||||
if (!slots) {
|
|
||||||
return {
|
|
||||||
default: SetterRender,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const items: Recordable = {};
|
|
||||||
for (const [name, Slot] of Object.entries(slots)) {
|
|
||||||
items[name] = (p: Recordable) => <Slot {...p} {...props}></Slot>;
|
|
||||||
}
|
|
||||||
if (Setter) {
|
|
||||||
items.default = SetterRender;
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
})();
|
|
||||||
|
|
||||||
provide(FormItemContextKey, props);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (props.item.visible && !props.item.visible(props)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<FormItem
|
|
||||||
{...props.item.itemProps}
|
|
||||||
class="an-form-item"
|
|
||||||
label={props.item.label as string}
|
|
||||||
rules={rules.value}
|
|
||||||
disabled={disabled.value}
|
|
||||||
field={props.item.field}
|
|
||||||
>
|
|
||||||
{itemSlots}
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type AnFormItemBoolFn = (args: AnFormItemFnProps) => boolean;
|
|
||||||
|
|
||||||
export type AnFormItemElemFn = (args: AnFormItemFnProps) => any;
|
|
||||||
|
|
||||||
export type AnFormItemFnProps = { model: Recordable; item: AnFormItemProps; items: AnFormItemProps[] };
|
|
||||||
|
|
||||||
export type AnFormItemRule = FieldRule & { disable?: AnFormItemBoolFn };
|
|
||||||
|
|
||||||
export type AnFormItemOption = string | number | boolean | SelectOptionData | SelectOptionGroup;
|
|
||||||
|
|
||||||
export type AnFormItemSlot = (props: AnFormItemFnProps) => any;
|
|
||||||
|
|
||||||
export type AnFormItemSlots = {
|
|
||||||
/**
|
|
||||||
* 默认插槽
|
|
||||||
* @param props 参数
|
|
||||||
*/
|
|
||||||
default?: AnFormItemSlot;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 帮助插槽
|
|
||||||
* @param props 参数
|
|
||||||
*/
|
|
||||||
help?: AnFormItemSlot;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 额外插槽
|
|
||||||
* @param props 参数
|
|
||||||
*/
|
|
||||||
extra?: AnFormItemSlot;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 标签插槽
|
|
||||||
* @param props 参数
|
|
||||||
*/
|
|
||||||
label?: AnFormItemSlot;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnFormItemPropsBase = {
|
|
||||||
/**
|
|
||||||
* 字段名
|
|
||||||
* @description 字段名唯一,支持特殊语法
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* 'username'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
field: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 标签
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* '昵称'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
label?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验规则
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* ['email']
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
rules?: AnFormItemRule[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否可见
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* (props) => Boolean(props.model.id)
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
visible?: AnFormItemBoolFn;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否禁用
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* (props) => Boolean(props.model.id)
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
disable?: AnFormItemBoolFn;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 选项
|
|
||||||
* @description 适用于下拉框等组件
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* [{
|
|
||||||
* label: '方式1',
|
|
||||||
* value: 1
|
|
||||||
* }]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
options?: AnFormItemOption[] | ((args: AnFormItemFnProps) => AnFormItemOption[] | Promise<AnFormItemOption[]>);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表单项参数
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* {
|
|
||||||
* hideLabel: true
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
itemProps?: Partial<Omit<FormItemInstance['$props'], 'field' | 'label' | 'required' | 'rules' | 'disabled'>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表单项插槽
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* {
|
|
||||||
* help: () => <span>帮助提示</span>
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
itemSlots?: AnFormItemSlots;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 内置
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
$init?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnFormItemProps = AnFormItemPropsBase & SetterItem;
|
|
||||||
|
|
@ -1,269 +0,0 @@
|
||||||
import { Button, ButtonInstance, FormInstance, Message, Modal } from '@arco-design/web-vue';
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { InjectionKey, PropType, Ref } from 'vue';
|
|
||||||
import { getModel, setModel } from '../utils/useFormModel';
|
|
||||||
import { AnForm, AnFormInstance, AnFormSubmit } from './Form';
|
|
||||||
import { AnFormItemProps } from './FormItem';
|
|
||||||
import { cloneDeep } from 'lodash-es';
|
|
||||||
|
|
||||||
export interface AnFormModalContext {
|
|
||||||
visible: Ref<boolean>;
|
|
||||||
loading: Ref<boolean>;
|
|
||||||
anFormRef: Ref<AnFormInstance | null>;
|
|
||||||
submitForm: () => any | Promise<any>;
|
|
||||||
open: (data: Recordable) => void;
|
|
||||||
close: () => void;
|
|
||||||
modalTitle: () => any;
|
|
||||||
modalTrigger: () => any;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AnFormModalContextKey = Symbol('AnFormModalContextKey') as InjectionKey<AnFormModalContext>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表单组件
|
|
||||||
*/
|
|
||||||
export const AnFormModal = defineComponent({
|
|
||||||
name: 'AnFormModal',
|
|
||||||
props: {
|
|
||||||
/**
|
|
||||||
* 弹窗标题
|
|
||||||
* @default
|
|
||||||
* ```ts
|
|
||||||
* '新增'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
title: {
|
|
||||||
type: [String, Function] as PropType<AnFormModalTitle>,
|
|
||||||
default: '新增',
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 触发元素
|
|
||||||
* @default
|
|
||||||
* ```ts
|
|
||||||
* '新增'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
trigger: {
|
|
||||||
type: [Boolean, String, Function, Object] as PropType<AnFormModalTrigger>,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 传递给Modal的props
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* {
|
|
||||||
* closable: true
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
modalProps: {
|
|
||||||
type: Object as PropType<Omit<InstanceType<typeof Modal>['$props'], 'visible' | 'title'>>,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 表单数据
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* {
|
|
||||||
* id: undefined
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
model: {
|
|
||||||
type: Object as PropType<Recordable>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 表单项
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* [{
|
|
||||||
* field: 'name',
|
|
||||||
* label: '昵称',
|
|
||||||
* setter: 'input'
|
|
||||||
* }]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
items: {
|
|
||||||
type: Array as PropType<AnFormItemProps[]>,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 提交表单
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* (model) => api.user.addUser(model)
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
submit: {
|
|
||||||
type: [Object, Function] as PropType<AnFormSubmit>,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 传给Form组件的参数
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* {
|
|
||||||
* layout: 'vertical'
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
formProps: {
|
|
||||||
type: Object as PropType<Omit<FormInstance['$props'], 'model' | 'ref'>>,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['update:model', 'submited'],
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const model = useVModel(props, 'model', emit);
|
|
||||||
const originModel = cloneDeep(model.value);
|
|
||||||
const anFormRef = ref<AnFormInstance | null>(null);
|
|
||||||
const visible = ref(false);
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
const modalTitle = () => {
|
|
||||||
if (typeof props.title === 'string') {
|
|
||||||
return props.title;
|
|
||||||
}
|
|
||||||
return <props.title model={props.model} items={props.items}></props.title>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const modalTrigger = () => {
|
|
||||||
if (!props.trigger) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (typeof props.trigger === 'function') {
|
|
||||||
return <props.trigger model={props.model} items={props.items} open={open}></props.trigger>;
|
|
||||||
}
|
|
||||||
const internal = {
|
|
||||||
text: '新增',
|
|
||||||
buttonProps: {},
|
|
||||||
buttonSlots: {},
|
|
||||||
};
|
|
||||||
if (typeof props.trigger === 'string') {
|
|
||||||
internal.text = props.trigger;
|
|
||||||
}
|
|
||||||
if (typeof props.trigger === 'object') {
|
|
||||||
Object.assign(internal, props.trigger);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Button type="primary" {...internal.buttonProps} onClick={open}>
|
|
||||||
{{
|
|
||||||
...internal.buttonSlots,
|
|
||||||
icon: () => <i class="icon-park-outline-add"></i>,
|
|
||||||
default: () => internal.text,
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitForm = async () => {
|
|
||||||
if (await anFormRef.value?.validate()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
const data = getModel(model.value);
|
|
||||||
const res = await (props as any).submit?.(data, props.items);
|
|
||||||
const msg = res?.data?.message;
|
|
||||||
msg && Message.success(msg);
|
|
||||||
visible.value = false;
|
|
||||||
emit('submited', res);
|
|
||||||
} catch {
|
|
||||||
// todo
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const open = async (data: Recordable = {}) => {
|
|
||||||
visible.value = true;
|
|
||||||
await nextTick();
|
|
||||||
model.value = cloneDeep(originModel)
|
|
||||||
setModel(model.value, data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
loading.value = false;
|
|
||||||
visible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClose = () => {};
|
|
||||||
|
|
||||||
const context: AnFormModalContext = {
|
|
||||||
visible,
|
|
||||||
loading,
|
|
||||||
anFormRef,
|
|
||||||
open,
|
|
||||||
close,
|
|
||||||
onClose,
|
|
||||||
submitForm,
|
|
||||||
modalTitle,
|
|
||||||
modalTrigger,
|
|
||||||
};
|
|
||||||
|
|
||||||
provide(AnFormModalContextKey, context);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...context
|
|
||||||
};
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<this.modalTrigger></this.modalTrigger>
|
|
||||||
<Modal
|
|
||||||
titleAlign="start"
|
|
||||||
closable={false}
|
|
||||||
{...this.modalProps}
|
|
||||||
v-model:visible={this.visible}
|
|
||||||
class="an-form-modal"
|
|
||||||
maskClosable={false}
|
|
||||||
unmountOnClose={true}
|
|
||||||
onClose={this.onClose}
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
title: this.modalTitle,
|
|
||||||
default: () => (
|
|
||||||
<AnForm
|
|
||||||
ref="formRef"
|
|
||||||
model={this.model}
|
|
||||||
onUpdate:model={v => this.$emit('update:model', v)}
|
|
||||||
items={this.items}
|
|
||||||
formProps={this.formProps}
|
|
||||||
></AnForm>
|
|
||||||
),
|
|
||||||
footer: () => (
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<div></div>
|
|
||||||
<div class="space-x-2">
|
|
||||||
<Button onClick={() => (this.visible = false)}>取消</Button>
|
|
||||||
<Button type="primary" loading={this.loading} onClick={this.submitForm}>
|
|
||||||
确定
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type AnFormModalTitle = string | ((model: Recordable, items: AnFormItemProps[]) => any);
|
|
||||||
|
|
||||||
export type AnFormModalTrigger =
|
|
||||||
| boolean
|
|
||||||
| string
|
|
||||||
| ((model: Recordable, items: AnFormItemProps[]) => any)
|
|
||||||
| {
|
|
||||||
text?: string;
|
|
||||||
buttonProps?: ButtonInstance['$props'];
|
|
||||||
buttonSlots?: Recordable;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnFormModalInstance = InstanceType<typeof AnFormModal>;
|
|
||||||
|
|
||||||
export type AnFormModalProps = Pick<
|
|
||||||
AnFormModalInstance['$props'],
|
|
||||||
'title' | 'trigger' | 'modalProps' | 'model' | 'items' | 'submit' | 'formProps'
|
|
||||||
>;
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
import setterMap from '../setters';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 键值对类型
|
|
||||||
*/
|
|
||||||
export type SetterMap = typeof setterMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 组件名联合类型
|
|
||||||
*/
|
|
||||||
export type SetterType = keyof SetterMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重新映射
|
|
||||||
*/
|
|
||||||
export type SetterItemMap = {
|
|
||||||
[key in SetterType]: {
|
|
||||||
/**
|
|
||||||
* 控件类型
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* 'input'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
setter: key;
|
|
||||||
/**
|
|
||||||
* 控件参数
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* { type: "password" }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
setterProps?: SetterMap[key]['setterProps'];
|
|
||||||
/**
|
|
||||||
* 控件插槽
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* label: (props) => <span>{props.item.label}</span>
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
setterSlots?: SetterMap[key]['setterSlots'];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 控件类型
|
|
||||||
*/
|
|
||||||
export type SetterItem =
|
|
||||||
| SetterItemMap[SetterType]
|
|
||||||
| { setter?: undefined; setterProps?: undefined; setterSlots?: undefined };
|
|
||||||
|
|
||||||
export { setterMap };
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
import { merge } from 'lodash-es';
|
|
||||||
import { AnForm, AnFormInstance, AnFormProps } from '../components/Form';
|
|
||||||
import { FormItem, useFormItems } from './useFormItems';
|
|
||||||
|
|
||||||
export type FormUseOptions = Partial<Omit<AnFormProps, 'items'>> & {
|
|
||||||
/**
|
|
||||||
* 表单项
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* [{
|
|
||||||
* field: 'name',
|
|
||||||
* label: '昵称',
|
|
||||||
* setter: 'input'
|
|
||||||
* }]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
items?: FormItem[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useFormProps(options: FormUseOptions): Required<AnFormProps> {
|
|
||||||
const { model: _model = {}, items: _items = [], submit = () => null, formProps = {} } = options;
|
|
||||||
const model = merge({ id: undefined }, _model);
|
|
||||||
const items = useFormItems(_items ?? [], model);
|
|
||||||
return {
|
|
||||||
model,
|
|
||||||
items,
|
|
||||||
submit,
|
|
||||||
formProps,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建表单组件的参数
|
|
||||||
*/
|
|
||||||
export const useForm = (options: FormUseOptions) => {
|
|
||||||
const props = reactive(useFormProps(options));
|
|
||||||
const formRef = ref<AnFormInstance | null>(null);
|
|
||||||
|
|
||||||
const AnFormer = () => (
|
|
||||||
<AnForm
|
|
||||||
ref={(el: any) => (formRef.value = el)}
|
|
||||||
v-model:model={props.model}
|
|
||||||
items={props.items}
|
|
||||||
submit={props.submit}
|
|
||||||
formProps={props.formProps}
|
|
||||||
></AnForm>
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
component: AnFormer,
|
|
||||||
formRef,
|
|
||||||
props,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
import { defaultsDeep, has, merge, omit } from 'lodash-es';
|
|
||||||
import { AnFormItemProps, AnFormItemPropsBase } from '../components/FormItem';
|
|
||||||
import { SetterItem, setterMap } from '../components/FormSetter';
|
|
||||||
import { Rule, useFormRules } from './useFormRules';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表单项数据
|
|
||||||
*/
|
|
||||||
export type FormItem = Omit<AnFormItemPropsBase, 'rules'> &
|
|
||||||
SetterItem & {
|
|
||||||
/**
|
|
||||||
* 默认值
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* '1'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
value?: any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否必填
|
|
||||||
* @default
|
|
||||||
* ```ts
|
|
||||||
* false
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
required?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验规则
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* ['email']
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
rules?: Rule[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 参数 `setterProps.placeholder` 的快捷语法
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* '请输入用户名称'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
placeholder?: string | string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const ITEM: Partial<FormItem> = {
|
|
||||||
setter: 'input',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useFormItems(items: FormItem[], model: Recordable) {
|
|
||||||
const data: AnFormItemProps[] = [];
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
let target: AnFormItemProps = defaultsDeep({}, ITEM);
|
|
||||||
|
|
||||||
if (!item.setter || typeof item.setter === 'string') {
|
|
||||||
const setter = setterMap[item.setter ?? 'input'];
|
|
||||||
if (setter) {
|
|
||||||
defaultsDeep(target, { setterProps: setter.setterProps ?? {} });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target = merge(target, omit(item, ['required', 'rules', 'value', 'placeholder']));
|
|
||||||
|
|
||||||
if (item.required || item.rules) {
|
|
||||||
const rules = useFormRules(item)!;
|
|
||||||
target.rules = rules;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.setterProps && has(item, 'placeholder')) {
|
|
||||||
(target.setterProps as Recordable).placholder = item.placeholder;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!has(model, item.field)) {
|
|
||||||
model[item.field] = item.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.push(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
import { merge } from 'lodash-es';
|
|
||||||
import { AnFormModal, AnFormModalProps } from '../components/FormModal';
|
|
||||||
import { useFormProps } from './useForm';
|
|
||||||
import { FormItem } from './useFormItems';
|
|
||||||
|
|
||||||
export type FormModalUseOptions = Partial<Omit<AnFormModalProps, 'items'>> & {
|
|
||||||
/**
|
|
||||||
* 弹窗宽度
|
|
||||||
* @description 参数 `modalProps.width` 的便捷语法
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* 580
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
width?: number;
|
|
||||||
/**
|
|
||||||
* modal宽度
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* 1080
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
modalWidth?: number;
|
|
||||||
/**
|
|
||||||
* 表单类名
|
|
||||||
* @description 参数 `formProps.class` 的便捷语法
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* 'grid grid-cols-2'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
formClass?: unknown;
|
|
||||||
/**
|
|
||||||
* 表单项
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* [{
|
|
||||||
* field: 'name',
|
|
||||||
* label: '昵称',
|
|
||||||
* setter: 'input'
|
|
||||||
* }]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
items: FormItem[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useFormModalProps(options: FormModalUseOptions): AnFormModalProps {
|
|
||||||
if (options.width) {
|
|
||||||
merge(options, { modalProps: { width: options.width } });
|
|
||||||
}
|
|
||||||
if (options.formClass) {
|
|
||||||
merge(options, { formProps: { class: options.formClass } });
|
|
||||||
}
|
|
||||||
const { items, model, formProps } = useFormProps({ ...options, submit: undefined });
|
|
||||||
const { trigger, title, submit, modalProps } = options;
|
|
||||||
return {
|
|
||||||
trigger,
|
|
||||||
model,
|
|
||||||
items,
|
|
||||||
title,
|
|
||||||
submit,
|
|
||||||
formProps,
|
|
||||||
modalProps,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useFormModal(options: FormModalUseOptions) {
|
|
||||||
const modalRef = ref<InstanceType<typeof AnFormModal> | null>(null);
|
|
||||||
const formRef = computed(() => modalRef.value?.anFormRef);
|
|
||||||
const open = (data: Recordable = {}) => modalRef.value?.open(data);
|
|
||||||
const rawProps = useFormModalProps(options);
|
|
||||||
const props = reactive(rawProps);
|
|
||||||
|
|
||||||
const component = () => (
|
|
||||||
<AnFormModal
|
|
||||||
ref={(el: any) => (modalRef.value = el)}
|
|
||||||
title={props.title}
|
|
||||||
trigger={props.trigger}
|
|
||||||
modalProps={props.modalProps as any}
|
|
||||||
model={props.model}
|
|
||||||
items={props.items}
|
|
||||||
formProps={props.formProps}
|
|
||||||
submit={props.submit}
|
|
||||||
onUpdate:model={model => ((props as any).model = model)}
|
|
||||||
></AnFormModal>
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props,
|
|
||||||
component,
|
|
||||||
modalRef,
|
|
||||||
formRef,
|
|
||||||
open,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
import { FieldRule } from "@arco-design/web-vue";
|
|
||||||
import { has, isString } from "lodash-es";
|
|
||||||
import { AnFormItemRule } from "../components/FormItem";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 内置规则
|
|
||||||
*/
|
|
||||||
export const FieldRuleMap = 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 FieldRuleMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 完整类型
|
|
||||||
*/
|
|
||||||
export type Rule = FieldStringRule | AnFormItemRule;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 助手函数(获得TS提示)
|
|
||||||
*/
|
|
||||||
function defineRuleMap<T extends Record<string, FieldRule>>(ruleMap: T) {
|
|
||||||
return ruleMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取表单规则
|
|
||||||
* @param item 表单项
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const useFormRules = <T extends { required?: boolean; rules?: Rule[] }>(item: T) => {
|
|
||||||
const data: AnFormItemRule[] = [];
|
|
||||||
const { required, rules } = item;
|
|
||||||
|
|
||||||
if (!has(item, "required") && !has(item, "rules")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (required) {
|
|
||||||
data.push(FieldRuleMap.required);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const rule of rules ?? []) {
|
|
||||||
if (isString(rule)) {
|
|
||||||
if (FieldRuleMap[rule]) {
|
|
||||||
data.push(FieldRuleMap[rule]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data.push(rule);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
export * from './components/Form';
|
|
||||||
export * from './components/FormItem';
|
|
||||||
export * from './components/FormModal';
|
|
||||||
export * from './components/FormSetter';
|
|
||||||
export * from './utils/useFormItems';
|
|
||||||
export * from './utils/useFormModel';
|
|
||||||
export * from './utils/useFormRef';
|
|
||||||
export * from './hooks/useForm';
|
|
||||||
export * from './hooks/useFormModal';
|
|
||||||
export * from './hooks/useFormItems';
|
|
||||||
export * from './hooks/useFormRules';
|
|
||||||
export * from './setters';
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { Cascader, CascaderInstance } from '@arco-design/web-vue';
|
|
||||||
import { defineSetter, initOptions } from './util';
|
|
||||||
|
|
||||||
type CascaderProps = CascaderInstance['$props'];
|
|
||||||
|
|
||||||
type CascaderSlots = 'label' | 'prefix' | 'arrowIcon' | 'loadingIcon' | 'searchIcon' | 'empty' | 'option';
|
|
||||||
|
|
||||||
export default defineSetter<CascaderProps, CascaderSlots>({
|
|
||||||
setter: Cascader,
|
|
||||||
setterProps: {
|
|
||||||
placeholder: '请选择',
|
|
||||||
allowClear: true,
|
|
||||||
expandTrigger: 'hover',
|
|
||||||
},
|
|
||||||
onSetup: initOptions as any,
|
|
||||||
});
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import { DatePicker, DatePickerInstance } from '@arco-design/web-vue';
|
|
||||||
import { defineSetter } from './util';
|
|
||||||
import { PickerProps } from '@arco-design/web-vue/es/date-picker/interface';
|
|
||||||
|
|
||||||
type DateProps = DatePickerInstance['$props'] & Partial<PickerProps>;
|
|
||||||
|
|
||||||
type DateSlots =
|
|
||||||
| 'prefix'
|
|
||||||
| 'suffixIcon'
|
|
||||||
| 'iconNextDouble'
|
|
||||||
| 'iconPrevDouble'
|
|
||||||
| 'iconNext'
|
|
||||||
| 'iconPrev'
|
|
||||||
| 'cell'
|
|
||||||
| 'extra';
|
|
||||||
|
|
||||||
export default defineSetter<DateProps, DateSlots>({
|
|
||||||
setter: DatePicker,
|
|
||||||
setterProps: {
|
|
||||||
placeholder: '请选择',
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { RangePicker, RangePickerInstance } from '@arco-design/web-vue';
|
|
||||||
import { defineSetter } from './util';
|
|
||||||
|
|
||||||
type RangeProps = RangePickerInstance['$props'];
|
|
||||||
|
|
||||||
type RangeSlots = "none";
|
|
||||||
|
|
||||||
export default defineSetter<RangeProps, RangeSlots>({
|
|
||||||
setter: RangePicker,
|
|
||||||
setterProps: {
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { Input, InputInstance } from '@arco-design/web-vue';
|
|
||||||
import { defineSetter } from './util';
|
|
||||||
|
|
||||||
type InputProps = InputInstance['$props'];
|
|
||||||
|
|
||||||
type InputSlots = 'prepend' | 'append' | 'suffix' | 'prefix';
|
|
||||||
|
|
||||||
export default defineSetter<InputProps, InputSlots>({
|
|
||||||
setter: Input,
|
|
||||||
setterProps: {
|
|
||||||
placeholder: '请输入',
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { InputInstance, InputNumber, InputNumberInstance } from '@arco-design/web-vue';
|
|
||||||
import { defineSetter } from './util';
|
|
||||||
|
|
||||||
type NumberProps = InputInstance['$props'] | InputNumberInstance['$props'];
|
|
||||||
|
|
||||||
type NumberSlots = 'minus' | 'plus' | 'append' | 'prepend' | 'suffix' | 'prefix';
|
|
||||||
|
|
||||||
export default defineSetter<NumberProps, NumberSlots>({
|
|
||||||
setter: InputNumber,
|
|
||||||
setterProps: {
|
|
||||||
placeholder: '请输入',
|
|
||||||
defaultValue: 0,
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { InputInstance, InputPassword, InputPasswordInstance } from '@arco-design/web-vue';
|
|
||||||
import { defineSetter } from './util';
|
|
||||||
|
|
||||||
type PasswordProps = InputInstance['$props'] & InputPasswordInstance['$props'];
|
|
||||||
|
|
||||||
type PasswordSlots = 'none';
|
|
||||||
|
|
||||||
export default defineSetter<PasswordProps, PasswordSlots>({
|
|
||||||
setter: InputPassword,
|
|
||||||
setterProps: {
|
|
||||||
placeholder: '请输入',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { InputInstance, InputSearch, InputSearchInstance } from '@arco-design/web-vue';
|
|
||||||
import { defineSetter } from './util';
|
|
||||||
|
|
||||||
type SearchProps = InputInstance['$props'] & InputSearchInstance['$props'];
|
|
||||||
|
|
||||||
type SearchSlots = "none";
|
|
||||||
|
|
||||||
export default defineSetter<SearchProps, SearchSlots>({
|
|
||||||
setter: InputSearch,
|
|
||||||
setterProps: {
|
|
||||||
placeholder: '请输入',
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import { Select, SelectInstance } from '@arco-design/web-vue';
|
|
||||||
import { defineSetter, initOptions } from './util';
|
|
||||||
|
|
||||||
type SelectProps = SelectInstance['$props'];
|
|
||||||
|
|
||||||
type SelectSlots =
|
|
||||||
| 'trigger'
|
|
||||||
| 'prefix'
|
|
||||||
| 'searchIcon'
|
|
||||||
| 'loadingIcon'
|
|
||||||
| 'arrowIcon'
|
|
||||||
| 'footer'
|
|
||||||
| 'header'
|
|
||||||
| 'label'
|
|
||||||
| 'option'
|
|
||||||
| 'empty';
|
|
||||||
|
|
||||||
export default defineSetter<SelectProps, SelectSlots>({
|
|
||||||
setter: Select,
|
|
||||||
onSetup: initOptions as any,
|
|
||||||
setterProps: {
|
|
||||||
placeholder: '请选择',
|
|
||||||
allowClear: true,
|
|
||||||
allowSearch: true,
|
|
||||||
options: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { Button } from '@arco-design/web-vue';
|
|
||||||
import { FormContextKey } from '../components/Form';
|
|
||||||
import { defineSetter } from './util';
|
|
||||||
|
|
||||||
export default defineSetter<{}, 'none'>({
|
|
||||||
setter() {
|
|
||||||
const { submitForm, resetForm } = inject(FormContextKey)!;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button type="primary" onClick={submitForm} class="mr-3">
|
|
||||||
提交
|
|
||||||
</Button>
|
|
||||||
<Button onClick={resetForm}>重置</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
setterProps: {},
|
|
||||||
});
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { InputInstance, Textarea, TextareaInstance } from '@arco-design/web-vue';
|
|
||||||
import { defineSetter } from './util';
|
|
||||||
|
|
||||||
type TextareaProps = InputInstance['$props'] & TextareaInstance['$props'];
|
|
||||||
|
|
||||||
type TextareaSlots = "none";
|
|
||||||
|
|
||||||
export default defineSetter<TextareaProps, TextareaSlots>({
|
|
||||||
setter: Textarea,
|
|
||||||
setterProps: {
|
|
||||||
placeholder: '请输入',
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { TimePicker, TimePickerInstance } from '@arco-design/web-vue';
|
|
||||||
import { defineSetter } from './util';
|
|
||||||
|
|
||||||
type TimeProps = TimePickerInstance['$props'];
|
|
||||||
|
|
||||||
type TimeSlots = 'prefix' | 'suffixIcon' | 'extra';
|
|
||||||
|
|
||||||
export default defineSetter<TimeProps, TimeSlots>({
|
|
||||||
setter: TimePicker,
|
|
||||||
setterProps: {
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import { TreeSelect, TreeSelectInstance } from '@arco-design/web-vue';
|
|
||||||
import { defineSetter, initOptions } from './util';
|
|
||||||
|
|
||||||
type TreeSelectProps = TreeSelectInstance['$props'];
|
|
||||||
|
|
||||||
type TreeSelectSlots =
|
|
||||||
| 'trigger'
|
|
||||||
| 'prefix'
|
|
||||||
| 'label'
|
|
||||||
| 'header'
|
|
||||||
| 'loader'
|
|
||||||
| 'empty'
|
|
||||||
| 'footer'
|
|
||||||
| 'treeSlotExtra'
|
|
||||||
| 'treeSlotTitle'
|
|
||||||
| 'treeSlotIcon'
|
|
||||||
| 'treeSlotSwitcherIcon';
|
|
||||||
|
|
||||||
export default defineSetter<TreeSelectProps, TreeSelectSlots>({
|
|
||||||
setter: TreeSelect,
|
|
||||||
onSetup: (arg: any) => initOptions(arg, 'data') as any,
|
|
||||||
setterProps: {
|
|
||||||
placeholder: '请选择',
|
|
||||||
allowClear: true,
|
|
||||||
allowSearch: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import cascader from './Cascader';
|
|
||||||
import date from './Date';
|
|
||||||
import dateRange from './DateRange';
|
|
||||||
import input from './Input';
|
|
||||||
import number from './Number';
|
|
||||||
import password from './Password';
|
|
||||||
import search from './Search';
|
|
||||||
import select from './Select';
|
|
||||||
import submit from './Submit';
|
|
||||||
import textarea from './Textarea';
|
|
||||||
import time from './Time';
|
|
||||||
import treeSelect from './TreeSelect';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
input,
|
|
||||||
number,
|
|
||||||
search,
|
|
||||||
textarea,
|
|
||||||
select,
|
|
||||||
treeSelect,
|
|
||||||
time,
|
|
||||||
password,
|
|
||||||
cascader,
|
|
||||||
date,
|
|
||||||
submit,
|
|
||||||
dateRange,
|
|
||||||
};
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import { Component } from 'vue';
|
|
||||||
import { AnFormItemPropsBase, AnFormItemSlot, AnFormItemFnProps } from '../components/FormItem';
|
|
||||||
|
|
||||||
export interface ItemSetter<P extends object, S extends string> {
|
|
||||||
/**
|
|
||||||
* 输入组件
|
|
||||||
*/
|
|
||||||
setter: Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 输入组件参数
|
|
||||||
*/
|
|
||||||
setterProps?: P;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 空间插槽
|
|
||||||
*/
|
|
||||||
setterSlots?: {
|
|
||||||
/**
|
|
||||||
* 控件插槽
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* (props) => {
|
|
||||||
* return <span>{props.item.label}</span>
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
[key in S]?: AnFormItemSlot;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化钩子
|
|
||||||
*/
|
|
||||||
onSetup?: (args: { model: Recordable; item: AnFormItemPropsBase; items: AnFormItemPropsBase[] }) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function defineSetter<P extends object, S extends string>(setter: ItemSetter<P, S>) {
|
|
||||||
return setter;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initOptions({ item, model }: AnFormItemFnProps, key: string = 'options') {
|
|
||||||
const setterProps: Recordable = item.setterProps!;
|
|
||||||
if (Array.isArray(item.options) && item.setterProps) {
|
|
||||||
setterProps[key] = item.options;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof item.options === 'function') {
|
|
||||||
setterProps[key] = reactive([]);
|
|
||||||
item.$init = async () => {
|
|
||||||
const res = await (item as any).options({ item, model });
|
|
||||||
if (Array.isArray(res)) {
|
|
||||||
setterProps[key].splice(0);
|
|
||||||
setterProps[key].push(...res);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = res?.data?.data;
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
const maped = data.map((i: any) => ({ ...i, value: i.id, label: i.name }));
|
|
||||||
setterProps[key].splice(0);
|
|
||||||
setterProps[key].push(...maped);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
item.$init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { AnFormItemProps } from '../components/FormItem';
|
|
||||||
import { setterMap } from '../components/FormSetter';
|
|
||||||
|
|
||||||
export const getFormItem = (items: AnFormItemProps[], field: string) => {
|
|
||||||
return items.find(i => i.field === field);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const initFormItems = (items: AnFormItemProps[], model: Recordable) => {
|
|
||||||
for (const item of items) {
|
|
||||||
const setter = setterMap[item.setter!];
|
|
||||||
setter.onSetup?.({ item, items, model });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
export function getModel(model: Recordable) {
|
|
||||||
const data: Recordable = {};
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(model)) {
|
|
||||||
if (value === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (/^\[.+\]$/.test(key)) {
|
|
||||||
getModelArray(key, value, data);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (/^\{.+\}$/.test(key)) {
|
|
||||||
getModelObject(key, value, data);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
data[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setModel(model: Recordable, data: Recordable) {
|
|
||||||
for (const [key, value] of Object.entries(model)) {
|
|
||||||
if (/^\[.+\]$/.test(key)) {
|
|
||||||
model[key] = setModelArray(data, key);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (/^\{.+\}$/.test(key)) {
|
|
||||||
model[key] = setModelObject(data, key);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
model[key] = data[key];
|
|
||||||
}
|
|
||||||
console.log(model, data);
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
|
|
||||||
function rmString(str: string) {
|
|
||||||
const field = str.replaceAll(/\s/g, '');
|
|
||||||
return field.match(/^(\{|\[)(.+)(\}|\])$/)?.[1] ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function setModelArray(data: Recordable, key: string) {
|
|
||||||
const result: any[] = [];
|
|
||||||
const field = rmString(key);
|
|
||||||
for (const key of field.split(',')) {
|
|
||||||
result.push(data[key]);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setModelObject(data: Recordable, key: string) {
|
|
||||||
const result: Recordable = {};
|
|
||||||
const field = rmString(key);
|
|
||||||
for (const key of field.split(',')) {
|
|
||||||
result[key] = data[key];
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getModelArray(key: string, value: any, data: Recordable) {
|
|
||||||
let field = rmString(key);
|
|
||||||
|
|
||||||
if (!field) {
|
|
||||||
data[key] = value;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
field.split(',').forEach((key, index) => {
|
|
||||||
data[key] = value?.[index];
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getModelObject(key: string, value: any, data: Recordable) {
|
|
||||||
const field = rmString(key);
|
|
||||||
|
|
||||||
if (!field) {
|
|
||||||
data[key] = value;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key of field.split(',')) {
|
|
||||||
data[key] = value?.[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import { FormInstance } from "@arco-design/web-vue";
|
|
||||||
|
|
||||||
export function useFormRef() {
|
|
||||||
/**
|
|
||||||
* 原始表单实例
|
|
||||||
*/
|
|
||||||
const formRef = ref<FormInstance | null>(null);
|
|
||||||
|
|
||||||
type Validate = FormInstance["validate"];
|
|
||||||
type ValidateField = FormInstance["validateField"];
|
|
||||||
type ResetFields = FormInstance["resetFields"];
|
|
||||||
type ClearValidate = FormInstance["clearValidate"];
|
|
||||||
type SetFields = FormInstance["setFields"];
|
|
||||||
type ScrollToField = FormInstance["scrollToField"];
|
|
||||||
|
|
||||||
const validate: Validate = async (...args) => formRef.value?.validate(...args);
|
|
||||||
const validateField: ValidateField = async (...args) => formRef.value?.validateField(...args);
|
|
||||||
const resetFields: ResetFields = (...args) => formRef.value?.resetFields(...args);
|
|
||||||
const clearValidate: ClearValidate = (...args) => formRef.value?.clearValidate(...args);
|
|
||||||
const setFields: SetFields = (...args) => formRef.value?.setFields(...args);
|
|
||||||
const scrollToField: ScrollToField = (...args) => formRef.value?.scrollToField(...args);
|
|
||||||
|
|
||||||
return {
|
|
||||||
formRef,
|
|
||||||
validate,
|
|
||||||
validateField,
|
|
||||||
resetFields,
|
|
||||||
clearValidate,
|
|
||||||
setFields,
|
|
||||||
scrollToField,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FormRef = ReturnType<typeof useFormRef>;
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import BreadCrumb from './bread-crumb.vue';
|
import BreadCrumb from './AnBreadcrumb.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -1,306 +0,0 @@
|
||||||
import { AnForm, AnFormInstance, AnFormModal, AnFormModalInstance, AnFormModalProps, AnFormProps, getModel } from '@/components/AnForm';
|
|
||||||
import AnEmpty from '@/components/AnEmpty/AnEmpty.vue';
|
|
||||||
import { Button, PaginationProps, Table, TableColumnData, TableData, TableInstance } from '@arco-design/web-vue';
|
|
||||||
import { isArray, isFunction, merge } from 'lodash-es';
|
|
||||||
import { InjectionKey, PropType, Ref, VNodeChild, defineComponent, ref } from 'vue';
|
|
||||||
import { PluginContainer } from '../hooks/useTablePlugin';
|
|
||||||
|
|
||||||
type DataFn = (filter: { page: number; size: number; [key: string]: any }) => any | Promise<any>;
|
|
||||||
export type ArcoTableProps = Omit<TableInstance['$props'], 'ref' | 'pagination' | 'loading' | 'data' | 'onPageChange' | 'onPageSizeChange'>;
|
|
||||||
export const AnTableContextKey = Symbol('AnTableContextKey') as InjectionKey<AnTableContext>;
|
|
||||||
export type TableColumnRender = (data: { record: TableData; column: TableColumnData; rowIndex: number }) => VNodeChild;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表格组件
|
|
||||||
*/
|
|
||||||
export const AnTable = defineComponent({
|
|
||||||
name: 'AnTable',
|
|
||||||
props: {
|
|
||||||
/**
|
|
||||||
* 表格列
|
|
||||||
*/
|
|
||||||
columns: {
|
|
||||||
type: Array as PropType<TableColumnData[]>,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 表格数据
|
|
||||||
* @description 数组或者函数
|
|
||||||
*/
|
|
||||||
source: {
|
|
||||||
type: [Array, Function] as PropType<TableData[] | DataFn>,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 分页配置
|
|
||||||
*/
|
|
||||||
paging: {
|
|
||||||
type: Object as PropType<PaginationProps & { hide?: boolean }>,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 搜索表单
|
|
||||||
*/
|
|
||||||
search: {
|
|
||||||
type: Object as PropType<AnFormProps>,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 新建弹窗
|
|
||||||
*/
|
|
||||||
create: {
|
|
||||||
type: Object as PropType<AnFormModalProps>,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 修改弹窗
|
|
||||||
*/
|
|
||||||
modify: {
|
|
||||||
type: Object as PropType<AnFormModalProps>,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 传递给 Table 组件的属性
|
|
||||||
*/
|
|
||||||
tableProps: {
|
|
||||||
type: Object as PropType<ArcoTableProps>,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 插件列表
|
|
||||||
*/
|
|
||||||
pluginer: {
|
|
||||||
type: Object as PropType<PluginContainer>,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const loading = ref(false);
|
|
||||||
const renderData = ref<TableData[]>([]);
|
|
||||||
const tableRef = ref<TableInstance | null>(null);
|
|
||||||
const searchRef = ref<AnFormInstance | null>(null);
|
|
||||||
const createRef = ref<AnFormModalInstance | null>(null);
|
|
||||||
const modifyRef = ref<AnFormModalInstance | null>(null);
|
|
||||||
|
|
||||||
const useTablePaging = () => {
|
|
||||||
const getPaging = () => {
|
|
||||||
return {
|
|
||||||
page: props.paging?.current ?? 1,
|
|
||||||
size: props.paging?.pageSize ?? 10,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const setPaging = (paging: PaginationProps) => {
|
|
||||||
if (props.paging) {
|
|
||||||
merge(props.paging, paging);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetPaging = () => {
|
|
||||||
setPaging({ current: 1, pageSize: 10 });
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
getPaging,
|
|
||||||
setPaging,
|
|
||||||
resetPaging,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const { getPaging, setPaging, resetPaging } = useTablePaging();
|
|
||||||
|
|
||||||
const loadData = async () => {
|
|
||||||
if (await searchRef.value?.validate()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const paging = getPaging();
|
|
||||||
const search = getModel(props.search?.model ?? {});
|
|
||||||
|
|
||||||
if (isArray(props.source)) {
|
|
||||||
// todo
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFunction(props.source)) {
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
let params = { ...search, ...paging };
|
|
||||||
let resData = (await props.pluginer?.callLoadHook(props.source, params)) || (await props.source(params));
|
|
||||||
let data: any[] = [];
|
|
||||||
let total = 0;
|
|
||||||
if (isArray(resData)) {
|
|
||||||
data = resData;
|
|
||||||
total = resData.length;
|
|
||||||
} else {
|
|
||||||
data = resData.data.data;
|
|
||||||
total = resData.data.total;
|
|
||||||
}
|
|
||||||
renderData.value = data;
|
|
||||||
setPaging({ total });
|
|
||||||
} catch (e) {
|
|
||||||
console.log('AnTable load fail: ', e);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const reload = () => {
|
|
||||||
setPaging({ current: 1, pageSize: 10 });
|
|
||||||
return loadData();
|
|
||||||
};
|
|
||||||
|
|
||||||
const refresh = () => {
|
|
||||||
return loadData();
|
|
||||||
};
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
if (Array.isArray(props.source)) {
|
|
||||||
renderData.value = props.source;
|
|
||||||
resetPaging();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadData();
|
|
||||||
});
|
|
||||||
|
|
||||||
const onPageChange = (page: number) => {
|
|
||||||
setPaging({ current: page });
|
|
||||||
loadData();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPageSizeChange = (size: number) => {
|
|
||||||
setPaging({ current: 1, pageSize: size });
|
|
||||||
loadData();
|
|
||||||
};
|
|
||||||
|
|
||||||
const context: AnTableContext = {
|
|
||||||
loading,
|
|
||||||
renderData,
|
|
||||||
tableRef,
|
|
||||||
searchRef,
|
|
||||||
createRef,
|
|
||||||
modifyRef,
|
|
||||||
loadData,
|
|
||||||
reload,
|
|
||||||
refresh,
|
|
||||||
onPageChange,
|
|
||||||
onPageSizeChange,
|
|
||||||
props,
|
|
||||||
};
|
|
||||||
|
|
||||||
props.pluginer?.callSetupHook(context);
|
|
||||||
provide(AnTableContextKey, context);
|
|
||||||
|
|
||||||
return context;
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div class="an-table table w-full">
|
|
||||||
<div class={`mb-3 flex gap-2 toolbar justify-between`}>
|
|
||||||
{this.create && <AnFormModal {...this.create} ref="createRef" onSubmited={this.reload}></AnFormModal>}
|
|
||||||
{this.modify && <AnFormModal {...this.modify} trigger={false} ref="modifyRef" onSubmited={this.refresh}></AnFormModal>}
|
|
||||||
{this.$slots.action?.(this.renderData)}
|
|
||||||
{this.pluginer?.actions && (
|
|
||||||
<div class={`flex-1 flex gap-2 items-center`}>
|
|
||||||
{this.pluginer.actions.map(Action => (
|
|
||||||
<Action />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{this.search && (
|
|
||||||
<div>
|
|
||||||
<AnForm ref="searchRef" v-model:model={this.search.model} items={this.search.items} formProps={this.search.formProps}>
|
|
||||||
{{
|
|
||||||
submit: () => (
|
|
||||||
<Button type="primary" loading={this.loading} onClick={this.reload}>
|
|
||||||
{{
|
|
||||||
default: () => '查询',
|
|
||||||
icon: () => <i class="icon-park-outline-search"></i>,
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
</AnForm>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{this.pluginer?.widgets && (
|
|
||||||
<div class="flex gap-2">
|
|
||||||
{this.pluginer.widgets.map(Widget => (
|
|
||||||
<Widget />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Table
|
|
||||||
row-key="id"
|
|
||||||
bordered={false}
|
|
||||||
{...this.$attrs}
|
|
||||||
{...this.tableProps}
|
|
||||||
ref="tableRef"
|
|
||||||
loading={this.loading}
|
|
||||||
pagination={this.paging?.hide ? false : this.paging}
|
|
||||||
data={this.renderData}
|
|
||||||
columns={this.columns}
|
|
||||||
onPageChange={this.onPageChange}
|
|
||||||
onPageSizeChange={this.onPageSizeChange}
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
empty: () => <AnEmpty />,
|
|
||||||
...this.$slots,
|
|
||||||
}}
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表格组件实例
|
|
||||||
*/
|
|
||||||
export type AnTableInstance = InstanceType<typeof AnTable>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表格组件参数
|
|
||||||
*/
|
|
||||||
export type AnTableProps = Pick<AnTableInstance['$props'], 'source' | 'columns' | 'search' | 'paging' | 'create' | 'modify' | 'tableProps' | 'pluginer'>;
|
|
||||||
|
|
||||||
export interface AnTableContext {
|
|
||||||
/**
|
|
||||||
* 是否加载中
|
|
||||||
*/
|
|
||||||
loading: Ref<boolean>;
|
|
||||||
/**
|
|
||||||
* 表格实例
|
|
||||||
*/
|
|
||||||
tableRef: Ref<TableInstance | null>;
|
|
||||||
/**
|
|
||||||
* 搜索表单实例
|
|
||||||
*/
|
|
||||||
searchRef: Ref<AnFormInstance | null>;
|
|
||||||
/**
|
|
||||||
* 新增弹窗实例
|
|
||||||
*/
|
|
||||||
createRef: Ref<AnFormModalInstance | null>;
|
|
||||||
/**
|
|
||||||
* 修改弹窗实例
|
|
||||||
*/
|
|
||||||
modifyRef: Ref<AnFormModalInstance | null>;
|
|
||||||
/**
|
|
||||||
* 当前表格数据
|
|
||||||
*/
|
|
||||||
renderData: Ref<TableData[]>;
|
|
||||||
/**
|
|
||||||
* 加载数据
|
|
||||||
*/
|
|
||||||
loadData: () => Promise<void>;
|
|
||||||
/**
|
|
||||||
* 重置加载
|
|
||||||
*/
|
|
||||||
reload: () => Promise<void>;
|
|
||||||
/**
|
|
||||||
* 重新加载
|
|
||||||
*/
|
|
||||||
refresh: () => Promise<void>;
|
|
||||||
/**
|
|
||||||
* 原表格参数
|
|
||||||
*/
|
|
||||||
props: AnTableProps;
|
|
||||||
onPageChange: any;
|
|
||||||
onPageSizeChange: any;
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import { dayjs } from '@/libs/dayjs';
|
|
||||||
import { Avatar } from '@arco-design/web-vue';
|
|
||||||
import { TableColumn } from '../hooks/useTableColumn';
|
|
||||||
|
|
||||||
export function useUpdateColumn(extra: TableColumn = {}): TableColumn {
|
|
||||||
return {
|
|
||||||
title: '最近修改',
|
|
||||||
dataIndex: 'createdAt',
|
|
||||||
width: 180,
|
|
||||||
render: ({ record }) => (
|
|
||||||
<div class="flex items-center gap-2 overflow-hidden">
|
|
||||||
<span>
|
|
||||||
<Avatar size={22}>{record.updatedBy?.substr(0,1) ?? '无'}</Avatar>
|
|
||||||
</span>
|
|
||||||
<span class="truncate" title={record.updatedAt}>
|
|
||||||
{dayjs(record.updatedAt).fromNow()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
...extra,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCreateColumn(extra: TableColumn = {}): TableColumn {
|
|
||||||
return {
|
|
||||||
title: '作者',
|
|
||||||
dataIndex: 'createdAt',
|
|
||||||
width: 180,
|
|
||||||
render: ({ record }) => (
|
|
||||||
<div class="flex direction-col items-center gap-2 overflow-hidden">
|
|
||||||
<span>
|
|
||||||
{record.createdBy ?? '无'}
|
|
||||||
</span>
|
|
||||||
<span class="text-gray-400 text-xs truncate" title={record.createdAt}>
|
|
||||||
{dayjs(record.createdAt).fromNow()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
...extra,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { ButtonProps, TableData } from '@arco-design/web-vue';
|
|
||||||
|
|
||||||
export interface AnTableActionBase {
|
|
||||||
text: string;
|
|
||||||
icon: string | Component;
|
|
||||||
visible: () => boolean;
|
|
||||||
disable: () => boolean;
|
|
||||||
buttonProps: ButtonProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AnTableActionBatch {
|
|
||||||
type: 'batch';
|
|
||||||
onClick: (rows: TableData) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AnTableAction = AnTableActionBase & AnTableActionBatch;
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { FormModalUseOptions, useFormModalProps } from '@/components/AnForm';
|
|
||||||
|
|
||||||
export type UseCreateFormOptions = FormModalUseOptions & {};
|
|
||||||
|
|
||||||
export function useCreateForm(options: UseCreateFormOptions) {
|
|
||||||
if (options.width) {
|
|
||||||
if (!options.modalProps) {
|
|
||||||
(options as any).modalProps = {};
|
|
||||||
}
|
|
||||||
(options.modalProps as any).width = options.width;
|
|
||||||
delete options.width;
|
|
||||||
}
|
|
||||||
if (options.formClass) {
|
|
||||||
if (!options.formProps) {
|
|
||||||
(options as any).formProps = {};
|
|
||||||
}
|
|
||||||
options.formProps!.class = options.formClass;
|
|
||||||
delete options.formClass;
|
|
||||||
}
|
|
||||||
return useFormModalProps(options);
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
import { FormItem, FormModalUseOptions, useFormModalProps, AnFormModalProps } from '@/components/AnForm';
|
|
||||||
import { cloneDeep, merge } from 'lodash-es';
|
|
||||||
import { ExtendFormItem } from './useSearchForm';
|
|
||||||
import { TableUseOptions } from './useTable';
|
|
||||||
|
|
||||||
export type ModifyForm = Omit<FormModalUseOptions, 'items' | 'trigger'> & {
|
|
||||||
/**
|
|
||||||
* 是否继承新建弹窗
|
|
||||||
* @default
|
|
||||||
* ```ts
|
|
||||||
* false
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
extend?: boolean;
|
|
||||||
/**
|
|
||||||
* 表单项
|
|
||||||
* ```tsx
|
|
||||||
* [{
|
|
||||||
* extend: 'name', // 从 create.items 中继承
|
|
||||||
* }]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
items?: ExtendFormItem[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useModifyForm(options: TableUseOptions, createModel: Recordable): AnFormModalProps | undefined {
|
|
||||||
const { create, modify } = options;
|
|
||||||
|
|
||||||
if (!modify) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: FormModalUseOptions = { items: [], model: cloneDeep(createModel) };
|
|
||||||
if (modify.extend && create) {
|
|
||||||
result = merge({}, create);
|
|
||||||
}
|
|
||||||
result = merge(result, modify);
|
|
||||||
|
|
||||||
if (modify.items) {
|
|
||||||
const items: FormItem[] = [];
|
|
||||||
const createItemMap: Record<string, FormItem> = {};
|
|
||||||
for (const item of create?.items ?? []) {
|
|
||||||
createItemMap[item.field] = item;
|
|
||||||
}
|
|
||||||
for (let item of modify.items ?? []) {
|
|
||||||
if (item.extend) {
|
|
||||||
item = merge({}, createItemMap[item.field!] ?? {}, item);
|
|
||||||
}
|
|
||||||
items.push(item as any);
|
|
||||||
}
|
|
||||||
if (items.length) {
|
|
||||||
result.items = items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modify.width || create?.width) {
|
|
||||||
if (!result.modalProps) {
|
|
||||||
(result as any).modalProps = {};
|
|
||||||
}
|
|
||||||
(result.modalProps as any).width = modify.width || create?.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modify.formClass || create?.formClass) {
|
|
||||||
if (!result.formProps) {
|
|
||||||
(result as any).formProps = {};
|
|
||||||
}
|
|
||||||
result.formProps!.class = modify.formClass || create?.formClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
return useFormModalProps(result);
|
|
||||||
}
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
import { defaultsDeep, isArray, merge } from 'lodash-es';
|
|
||||||
import { AnFormProps, FormUseOptions, AnFormItemProps, FormItem, useFormItems } from '@/components/AnForm';
|
|
||||||
|
|
||||||
export type ExtendFormItem = Partial<
|
|
||||||
FormItem & {
|
|
||||||
/**
|
|
||||||
* 从新建弹窗继承表单项
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* 'name'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
extend: string;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type SearchFormItem = ExtendFormItem & {
|
|
||||||
/**
|
|
||||||
* 是否点击图标后进行搜索
|
|
||||||
* @description 仅 setter: 'search' 类型可用
|
|
||||||
* @default
|
|
||||||
* ```ts
|
|
||||||
* false
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
searchable?: boolean;
|
|
||||||
/**
|
|
||||||
* 是否回车后进行查询
|
|
||||||
* @default
|
|
||||||
* ```ts
|
|
||||||
* false
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
enterable?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SearchForm = Omit<FormUseOptions, 'items' | 'submit'> & {
|
|
||||||
/**
|
|
||||||
* 搜索表单项
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* [{
|
|
||||||
* extend: 'name' // 从 create.items 继承
|
|
||||||
* }]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
items?: SearchFormItem[];
|
|
||||||
/**
|
|
||||||
* 是否隐藏查询按钮
|
|
||||||
* @default
|
|
||||||
* ```tsx
|
|
||||||
* false
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
hideSearch?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useSearchForm(
|
|
||||||
search?: SearchForm | SearchFormItem[],
|
|
||||||
extendItems: AnFormItemProps[] = []
|
|
||||||
): AnFormProps | undefined {
|
|
||||||
if (!search) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isArray(search)) {
|
|
||||||
search = { items: search };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { items: _items = [], hideSearch, model: _model, formProps: _formProps } = search;
|
|
||||||
const extendMap = extendItems.reduce((m, v) => ((m[v.field] = v), m), {} as Record<string, AnFormItemProps>);
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
items: [] as AnFormItemProps[],
|
|
||||||
model: _model ?? {},
|
|
||||||
formProps: defaultsDeep({}, _formProps, { layout: 'inline' }),
|
|
||||||
};
|
|
||||||
|
|
||||||
const defualts: Partial<AnFormItemProps> = {
|
|
||||||
setter: 'input',
|
|
||||||
itemProps: {
|
|
||||||
hideLabel: true,
|
|
||||||
},
|
|
||||||
setterProps: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const items: AnFormItemProps[] = [];
|
|
||||||
|
|
||||||
for (const _item of _items) {
|
|
||||||
const { searchable, enterable, field, extend, ...itemRest } = _item;
|
|
||||||
if ((field || extend) === 'submit' && hideSearch) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let item: AnFormItemProps = defaultsDeep({}, itemRest, defualts);
|
|
||||||
if (extend) {
|
|
||||||
const extendItem = extendMap[extend];
|
|
||||||
if (extendItem) {
|
|
||||||
item = merge({}, extendItem, itemRest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (searchable && item.setter === 'search') {
|
|
||||||
(item as any).setterProps.onSearch = () => null;
|
|
||||||
}
|
|
||||||
if (enterable) {
|
|
||||||
(item as any).setterProps.onPressEnter = () => null;
|
|
||||||
}
|
|
||||||
if (item.setterProps) {
|
|
||||||
(item.setterProps as any).placeholder = (item.setterProps as any).placeholder ?? item.label;
|
|
||||||
}
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
props.items = useFormItems(items, props.model);
|
|
||||||
|
|
||||||
return props;
|
|
||||||
}
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
import { useFormModalProps } from '@/components/AnForm';
|
|
||||||
import { AnTable, AnTableInstance, AnTableProps } from '../components/Table';
|
|
||||||
import { ModifyForm, useModifyForm } from './useModiyForm';
|
|
||||||
import { SearchForm, SearchFormItem, useSearchForm } from './useSearchForm';
|
|
||||||
import { TableColumn, useTableColumns } from './useTableColumn';
|
|
||||||
import { AnTablePlugin, PluginContainer } from './useTablePlugin';
|
|
||||||
import { UseCreateFormOptions } from './useCreateForm';
|
|
||||||
import { FunctionalComponent } from 'vue';
|
|
||||||
|
|
||||||
export interface TableUseOptions extends Pick<AnTableProps, 'source' | 'tableProps' | 'paging'> {
|
|
||||||
/**
|
|
||||||
* 唯一ID
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* 'UserTable'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
id?: string;
|
|
||||||
/**
|
|
||||||
* 插件列表
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* [useRefresh()]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
plugins?: AnTablePlugin[];
|
|
||||||
/**
|
|
||||||
* 表格列
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* [{
|
|
||||||
* dataIndex: 'title',
|
|
||||||
* title: '标题'
|
|
||||||
* }]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
columns?: TableColumn[];
|
|
||||||
/**
|
|
||||||
* 操作栏
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* [{
|
|
||||||
* text: '按钮',
|
|
||||||
* onClick: () => null,
|
|
||||||
* }]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
actions?: any[];
|
|
||||||
/**
|
|
||||||
* 搜索表单
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* [{
|
|
||||||
* field: 'name',
|
|
||||||
* label: '用户名称',
|
|
||||||
* setter: 'input'
|
|
||||||
* }]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
search?: SearchForm | SearchFormItem[];
|
|
||||||
/**
|
|
||||||
* 新建弹窗
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* {
|
|
||||||
* title: '添加用户',
|
|
||||||
* items: [],
|
|
||||||
* submit: (model) => {}
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
create?: UseCreateFormOptions;
|
|
||||||
/**
|
|
||||||
* 修改弹窗
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* {
|
|
||||||
* extend: true, // 基于新建弹窗扩展
|
|
||||||
* title: '修改用户',
|
|
||||||
* submit: (model) => {}
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
modify?: ModifyForm;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTableProps(options: TableUseOptions): AnTableProps {
|
|
||||||
const { source, tableProps } = options;
|
|
||||||
|
|
||||||
const columns = useTableColumns(options.columns ?? []);
|
|
||||||
const paging = { hide: false, showTotal: true, showPageSize: true, ...(options.paging ?? {}) };
|
|
||||||
const search = options.search && useSearchForm(options.search);
|
|
||||||
const create = options.create && useFormModalProps(options.create);
|
|
||||||
const modify = options.modify && useModifyForm(options, create?.model ?? {} );
|
|
||||||
|
|
||||||
return {
|
|
||||||
tableProps,
|
|
||||||
columns,
|
|
||||||
source,
|
|
||||||
search,
|
|
||||||
paging,
|
|
||||||
create,
|
|
||||||
modify,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTable(options: TableUseOptions) {
|
|
||||||
const tableRef = ref<AnTableInstance | null>(null);
|
|
||||||
if (!options.plugins) {
|
|
||||||
options.plugins = [];
|
|
||||||
}
|
|
||||||
const pluginer = new PluginContainer(options.plugins);
|
|
||||||
options = pluginer.callOptionsHook(options);
|
|
||||||
|
|
||||||
const rawProps = useTableProps(options);
|
|
||||||
const props = reactive(rawProps);
|
|
||||||
|
|
||||||
const AnTabler: FunctionalComponent = (_, { slots }) => (
|
|
||||||
<AnTable
|
|
||||||
ref={(el: any) => (tableRef.value = el)}
|
|
||||||
tableProps={props.tableProps}
|
|
||||||
columns={props.columns}
|
|
||||||
source={props.source}
|
|
||||||
paging={props.paging}
|
|
||||||
search={props.search}
|
|
||||||
create={props.create as any}
|
|
||||||
modify={props.modify as any}
|
|
||||||
pluginer={pluginer}
|
|
||||||
>
|
|
||||||
{slots}
|
|
||||||
</AnTable>
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
component: AnTabler,
|
|
||||||
tableRef,
|
|
||||||
props,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
import { Divider, Link, TableColumnData } from '@arco-design/web-vue';
|
|
||||||
|
|
||||||
interface TableBaseColumn {
|
|
||||||
/**
|
|
||||||
* 类型
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* 'delete'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
type?: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TableIndexColumn {
|
|
||||||
/**
|
|
||||||
* 类型
|
|
||||||
*/
|
|
||||||
type: 'index';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TableColumnButton {
|
|
||||||
/**
|
|
||||||
* 特殊类型
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* 'delete'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
type?: 'modify' | 'delete';
|
|
||||||
/**
|
|
||||||
* 确认弹窗配置
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* '确定删除吗?'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
confirm?: string;
|
|
||||||
/**
|
|
||||||
* 按钮文本
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* '修改'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
text?: string;
|
|
||||||
/**
|
|
||||||
* 按钮参数
|
|
||||||
* @see ALink
|
|
||||||
*/
|
|
||||||
buttonProps?: Recordable;
|
|
||||||
icon?: string;
|
|
||||||
/**
|
|
||||||
* 是否可见
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* (props) => props.record.status === 1
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
visible?: (args: Recordable) => boolean;
|
|
||||||
/**
|
|
||||||
* 是否禁用
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* (props) => props.record.status === 1
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
disable?: (args: Recordable) => boolean;
|
|
||||||
/**
|
|
||||||
* 处理函数
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* (props) => api.user.rmUser(props.record.id)
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
onClick?: (props: any) => any | Promise<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TableButtonColumn {
|
|
||||||
/**
|
|
||||||
* 类型
|
|
||||||
*/
|
|
||||||
type: 'button';
|
|
||||||
/**
|
|
||||||
* 按钮列表
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* [{
|
|
||||||
* type: 'delete',
|
|
||||||
* text: '删除',
|
|
||||||
* onClick: (args) => api.user.rmUser(args.record.id)
|
|
||||||
* }]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
buttons: TableColumnButton[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TableDropdownColumn {
|
|
||||||
/**
|
|
||||||
* 类型
|
|
||||||
*/
|
|
||||||
type: 'dropdown';
|
|
||||||
/**
|
|
||||||
* 下拉列表
|
|
||||||
*/
|
|
||||||
dropdowns: any[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TableColumn = TableColumnData &
|
|
||||||
(TableIndexColumn | TableBaseColumn | TableButtonColumn | TableDropdownColumn) & {
|
|
||||||
/**
|
|
||||||
* 是否可配置
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* true
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
configable?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useTableColumns(data: TableColumn[]) {
|
|
||||||
const columns: TableColumnData[] = [];
|
|
||||||
|
|
||||||
for (let column of data) {
|
|
||||||
// if (column.type === "index") {
|
|
||||||
// column = useTableIndexColumn(column);
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (column.type === 'button') {
|
|
||||||
column = useTableButtonColumn(column);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (column.type === "dropdown") {
|
|
||||||
// column = useTableDropdownColumn(column);
|
|
||||||
// }
|
|
||||||
|
|
||||||
columns.push(column);
|
|
||||||
}
|
|
||||||
|
|
||||||
return columns;
|
|
||||||
}
|
|
||||||
|
|
||||||
function useTableIndexColumn() {}
|
|
||||||
|
|
||||||
function useTableButtonColumn(column: TableButtonColumn & TableColumnData) {
|
|
||||||
const { type, buttons } = column;
|
|
||||||
const items: TableColumnButton[] = [];
|
|
||||||
for (const button of buttons) {
|
|
||||||
items.push(button);
|
|
||||||
}
|
|
||||||
column.render = props => {
|
|
||||||
return items.map((item, index) => {
|
|
||||||
if (item.visible && !item.visible(props)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{index !== 0 && <Divider direction="vertical" margin={4} />}
|
|
||||||
<Link {...item.buttonProps} disabled={item.disable?.(props)} onClick={() => item.onClick?.(props)}>
|
|
||||||
{{
|
|
||||||
default: () => item.text,
|
|
||||||
// icon: () => item.icon ? <i class={item.icon}></i> : null
|
|
||||||
}}
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return column;
|
|
||||||
}
|
|
||||||
|
|
||||||
function useTableDropdownColumn() {}
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
import { Component } from 'vue';
|
|
||||||
import { AnTableContext } from '../components/Table';
|
|
||||||
import { TableUseOptions } from './useTable';
|
|
||||||
import { TableColumn } from './useTableColumn';
|
|
||||||
import { useTableRefresh } from '../plugins/useTableRefresh';
|
|
||||||
import { useColumnConfig } from '../plugins/useTableConfig';
|
|
||||||
import { useRowFormat } from '../plugins/useRowFormat';
|
|
||||||
import { useRowDelete } from '../plugins/useRowDelete';
|
|
||||||
import { useRowModify } from '../plugins/useRowModify';
|
|
||||||
|
|
||||||
export interface AnTablePlugin {
|
|
||||||
/**
|
|
||||||
* 插件ID(唯一)
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* 'refresh'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提供给其他插件使用的变量
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* { isOk: true }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
provide?: Recordable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 在表格组件的 `setup` 函数中调用
|
|
||||||
*/
|
|
||||||
onSetup?: (context: AnTableContext) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 钩子
|
|
||||||
*/
|
|
||||||
options?: (options: TableUseOptions) => TableUseOptions | null | undefined | void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析参数之前调用
|
|
||||||
*/
|
|
||||||
parse?: (options: TableUseOptions) => TableUseOptions | null | undefined | void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析参数之后调用
|
|
||||||
*/
|
|
||||||
parsed?: (options: any) => any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表格列
|
|
||||||
*/
|
|
||||||
column?: (column: TableColumn) => TableColumn;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加部件栏组件
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* () => <Button>测试</Button>
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
widget?: () => (props: any) => any | Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加操作栏组件
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* () => <Button>测试</Button>
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
action?: () => (props: any) => any | Component;
|
|
||||||
|
|
||||||
onSearch?: (search: Recordable) => any[] | { data: any[]; total: number };
|
|
||||||
|
|
||||||
onLoad?: (search: Recordable) => void;
|
|
||||||
onLoaded?: (res: any) => void;
|
|
||||||
onLoadOk?: (res: any) => void;
|
|
||||||
onLoadFail?: (e: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const callHookWithData = async (name: string, plugins: AnTablePlugin[], data?: any) => {
|
|
||||||
for (const plugin of plugins) {
|
|
||||||
data = (await (plugin as any)[name]?.(data)) ?? data;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const callHookFirst = async (name: string, plugins: AnTablePlugin[], ...args: any[]) => {
|
|
||||||
for (const plugin of plugins) {
|
|
||||||
const data = await (plugin as any)[name]?.(...args);
|
|
||||||
if (data) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class PluginContainer {
|
|
||||||
actions: any[] = [];
|
|
||||||
widgets: any[] = [];
|
|
||||||
|
|
||||||
constructor(private plugins: AnTablePlugin[]) {
|
|
||||||
this.plugins.unshift(useTableRefresh(), useRowFormat(), useRowDelete(), useRowModify());
|
|
||||||
for (const plugin of plugins) {
|
|
||||||
const action = plugin.action?.();
|
|
||||||
if (action) {
|
|
||||||
this.actions.push(action);
|
|
||||||
}
|
|
||||||
const widget = plugin.widget?.();
|
|
||||||
if (widget) {
|
|
||||||
this.widgets.push(widget);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
callSetupHook(context: AnTableContext) {
|
|
||||||
for (const plugin of this.plugins) {
|
|
||||||
plugin.onSetup?.(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
callOptionsHook(options: any) {
|
|
||||||
for (const plugin of this.plugins) {
|
|
||||||
options = plugin.options?.(options) ?? options;
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
callActionHook(options: any) {
|
|
||||||
for (const plugin of this.plugins) {
|
|
||||||
options = plugin.options?.(options) ?? options;
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
callWidgetHook(options: any) {
|
|
||||||
for (const plugin of this.plugins) {
|
|
||||||
options = plugin.options?.(options) ?? options;
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
callLoadHook(data: any[] | ((...args: any[]) => Promise<any> | any), params: Recordable) {
|
|
||||||
return callHookFirst('onLoad', this.plugins, data, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
callLoadedHook(res: any) {
|
|
||||||
return callHookWithData('onLoaded', this.plugins, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
callLoadOkHook(res: any) {
|
|
||||||
return callHookWithData('onLoadOk', this.plugins, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
callLoadFailHook(res: any) {
|
|
||||||
return callHookWithData('onLoadFail', this.plugins, res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
export * from './components/column';
|
|
||||||
export * from './components/Table';
|
|
||||||
export * from './hooks/useTable';
|
|
||||||
export * from './hooks/useTablePlugin';
|
|
||||||
export * from './hooks/useTableColumn';
|
|
||||||
export * from './hooks/useSearchForm';
|
|
||||||
export * from './hooks/useModiyForm';
|
|
||||||
export * from './plugins/useTableConfig';
|
|
||||||
export * from './plugins/useTableRefresh';
|
|
||||||
export * from './plugins/useTableSelect';
|
|
||||||
export * from './plugins/useTableDelete';
|
|
||||||
export * from './plugins/useRowDelete';
|
|
||||||
export * from './plugins/useRowModify';
|
|
||||||
export * from './plugins/useRowFormat';
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import { delConfirm, delOptions } from '@/utils';
|
|
||||||
import { AnTableContext } from '../components/Table';
|
|
||||||
import { AnTablePlugin } from '../hooks/useTablePlugin';
|
|
||||||
import { Message } from '@arco-design/web-vue';
|
|
||||||
import { defaultsDeep } from 'lodash-es';
|
|
||||||
|
|
||||||
export function useRowDelete(): AnTablePlugin {
|
|
||||||
let ctx: AnTableContext;
|
|
||||||
return {
|
|
||||||
id: 'rowDelete',
|
|
||||||
onSetup(context) {
|
|
||||||
ctx = context;
|
|
||||||
},
|
|
||||||
options(options) {
|
|
||||||
for (const column of options.columns ?? []) {
|
|
||||||
if (column.type !== 'button') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const btn = column.buttons.find(i => i.type === 'delete');
|
|
||||||
if (!btn) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
defaultsDeep(btn, {
|
|
||||||
buttonProps: {
|
|
||||||
status: 'danger',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const onClick = btn.onClick;
|
|
||||||
btn.onClick = async props => {
|
|
||||||
let confirm = btn.confirm ?? {};
|
|
||||||
if (typeof confirm === 'string') {
|
|
||||||
confirm = { content: confirm };
|
|
||||||
}
|
|
||||||
delConfirm({
|
|
||||||
...delOptions,
|
|
||||||
...confirm,
|
|
||||||
async onBeforeOk() {
|
|
||||||
const res: any = await onClick?.(props);
|
|
||||||
const msg = res?.data?.message;
|
|
||||||
msg && Message.success(`提示: ${msg}`);
|
|
||||||
ctx.refresh();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import { AnTablePlugin } from '../hooks/useTablePlugin';
|
|
||||||
|
|
||||||
export function useRowFormat(): AnTablePlugin {
|
|
||||||
return {
|
|
||||||
id: 'format',
|
|
||||||
options(options) {
|
|
||||||
for (const column of options.columns ?? []) {
|
|
||||||
if (column.render) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
column.render = ({ record, column }) => record[column.dataIndex!] ?? '-';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { AnTableContext } from '../components/Table';
|
|
||||||
import { AnTablePlugin } from '../hooks/useTablePlugin';
|
|
||||||
|
|
||||||
export function useRowModify(): AnTablePlugin {
|
|
||||||
let ctx: AnTableContext;
|
|
||||||
return {
|
|
||||||
id: 'rowModify',
|
|
||||||
onSetup(context) {
|
|
||||||
ctx = context;
|
|
||||||
},
|
|
||||||
options(options) {
|
|
||||||
for (const column of options.columns ?? []) {
|
|
||||||
if (column.type !== 'button') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const btn = column.buttons.find(i => i.type === 'modify');
|
|
||||||
if (!btn) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const onClick = btn.onClick;
|
|
||||||
btn.onClick = async props => {
|
|
||||||
const data = (await onClick?.(props)) ?? props.record;
|
|
||||||
ctx.modifyRef.value?.open(data);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,198 +0,0 @@
|
||||||
import { Button, Checkbox, Divider, InputNumber, Popover, Scrollbar, Tag } from '@arco-design/web-vue';
|
|
||||||
import { PropType } from 'vue';
|
|
||||||
import { AnTablePlugin } from '../hooks/useTablePlugin';
|
|
||||||
import { AnTableContext } from '../components/Table';
|
|
||||||
|
|
||||||
interface Item {
|
|
||||||
dataIndex: string;
|
|
||||||
enable: boolean;
|
|
||||||
autoWidth: boolean;
|
|
||||||
width: number;
|
|
||||||
editable: boolean;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TableColumnConfig = defineComponent({
|
|
||||||
props: {
|
|
||||||
columns: {
|
|
||||||
type: Object as PropType<any[]>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const checkAll = ref(false);
|
|
||||||
const visible = ref(false);
|
|
||||||
const items = ref<Item[]>([]);
|
|
||||||
const checked = computed(() => items.value.filter(i => i.enable));
|
|
||||||
const indeterminate = computed(() => {
|
|
||||||
const check = checked.value.length;
|
|
||||||
const total = items.value.length;
|
|
||||||
return 0 < check && check < total;
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => visible.value,
|
|
||||||
value => {
|
|
||||||
if (value) {
|
|
||||||
init();
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const init = () => {
|
|
||||||
const list: Item[] = [];
|
|
||||||
for (const column of props.columns) {
|
|
||||||
list.push({
|
|
||||||
dataIndex: column.dataIndex,
|
|
||||||
title: column.title,
|
|
||||||
enable: true,
|
|
||||||
autoWidth: false,
|
|
||||||
width: column.width ?? 60,
|
|
||||||
editable: !column.configable,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
items.value = list;
|
|
||||||
onItemChange();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onItemChange = () => {
|
|
||||||
if (checked.value.length === 0) {
|
|
||||||
checkAll.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (checked.value.length === items.value.length) {
|
|
||||||
checkAll.value = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCheckAllChange = (value: any) => {
|
|
||||||
for (const item of items.value) {
|
|
||||||
if (item.editable) {
|
|
||||||
item.enable = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onReset = () => {
|
|
||||||
visible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onConfirm = () => {
|
|
||||||
visible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onItemUp = (index: number) => {
|
|
||||||
[items.value[index - 1], items.value[index]] = [items.value[index], items.value[index - 1]];
|
|
||||||
};
|
|
||||||
|
|
||||||
const onItemDown = (index: number) => {
|
|
||||||
[items.value[index + 1], items.value[index]] = [items.value[index], items.value[index + 1]];
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => (
|
|
||||||
<Popover v-model:popup-visible={visible.value} position="br" trigger="click">
|
|
||||||
{{
|
|
||||||
default: () => (
|
|
||||||
<Button class="float-right">{{ icon: () => <span class="icon-park-outline-config"></span> }}</Button>
|
|
||||||
),
|
|
||||||
content: () => (
|
|
||||||
<>
|
|
||||||
<div class="mb-1 leading-none border-b border-gray-100 pb-3">设置表格列</div>
|
|
||||||
<Scrollbar outer-class="h-96 overflow-hidden" class="h-full overflow-auto">
|
|
||||||
<ul class="grid m-0 p-0 divide-y divide-gray-100 w-[700px] overflow-auto overscroll-contain">
|
|
||||||
{items.value.map((item, index) => (
|
|
||||||
<li
|
|
||||||
key={item.dataIndex}
|
|
||||||
class="group flex items-center justify-between gap-6 py-2 pr-8 select-none"
|
|
||||||
>
|
|
||||||
<div class="flex-1 flex justify-between gap-2">
|
|
||||||
<Checkbox v-model={item.enable} disabled={!item.editable} onChange={onItemChange}>
|
|
||||||
{item.title}
|
|
||||||
</Checkbox>
|
|
||||||
<span class="hidden group-hover:inline-block ml-4">
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
shape="circle"
|
|
||||||
size="mini"
|
|
||||||
disabled={index == 0}
|
|
||||||
onClick={() => onItemUp(index)}
|
|
||||||
>
|
|
||||||
{{ icon: () => <i class="icon-park-outline-arrow-up"></i> }}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
shape="circle"
|
|
||||||
size="mini"
|
|
||||||
disabled={index == items.value.length - 1}
|
|
||||||
onClick={() => onItemDown(index)}
|
|
||||||
>
|
|
||||||
{{ icon: () => <i class="icon-park-outline-arrow-down"></i> }}
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2 items-center">
|
|
||||||
<Checkbox v-model={item.autoWidth} disabled={!item.editable}>
|
|
||||||
{{
|
|
||||||
checkbox: ({ checked }: any) => (
|
|
||||||
<Tag checked={checked} checkable={item.editable} color="blue">
|
|
||||||
自适应
|
|
||||||
</Tag>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
</Checkbox>
|
|
||||||
<Divider direction="vertical" margin={8}></Divider>
|
|
||||||
<InputNumber
|
|
||||||
size="small"
|
|
||||||
v-model={item.width}
|
|
||||||
disabled={item.autoWidth || !item.editable}
|
|
||||||
min={60}
|
|
||||||
step={10}
|
|
||||||
class="!w-20"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-400">像素</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</Scrollbar>
|
|
||||||
<div class="mt-4 flex gap-2 items-center justify-between">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Checkbox indeterminate={indeterminate.value} v-model={checkAll.value} onChange={onCheckAllChange}>
|
|
||||||
全选
|
|
||||||
</Checkbox>
|
|
||||||
<span class="text-xs text-gray-400 ml-1">
|
|
||||||
({checked.value.length}/{items.value.length})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="space-x-2">
|
|
||||||
<Button onClick={onReset}>重置</Button>
|
|
||||||
<Button type="primary" onClick={onConfirm}>
|
|
||||||
确定
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插件:表格列配置
|
|
||||||
* @description 配置ID将缓存结果在本地
|
|
||||||
*/
|
|
||||||
export function useColumnConfig(): AnTablePlugin {
|
|
||||||
let context: AnTableContext;
|
|
||||||
return {
|
|
||||||
id: 'columnconfig',
|
|
||||||
onSetup(ctx) {
|
|
||||||
context = ctx;
|
|
||||||
},
|
|
||||||
widget() {
|
|
||||||
return () => <TableColumnConfig columns={context.props.columns!} />;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
import { Ref } from 'vue';
|
|
||||||
import { AnTableContext, ArcoTableProps } from '../components/Table';
|
|
||||||
import { AnTablePlugin } from '../hooks/useTablePlugin';
|
|
||||||
import { useTableSelect } from './useTableSelect';
|
|
||||||
import { delConfirm, delOptions, sleep } from '@/utils';
|
|
||||||
import { Button, Message, TableInstance } from '@arco-design/web-vue';
|
|
||||||
|
|
||||||
interface UseTableDeleteOptions {
|
|
||||||
confirm?: string;
|
|
||||||
onDelete?: (keys: (string | number)[]) => any | Promise<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTableDelete(options: UseTableDeleteOptions = {}): AnTablePlugin {
|
|
||||||
let selected: Ref<any[]>;
|
|
||||||
let context: AnTableContext;
|
|
||||||
let tableProps: ArcoTableProps;
|
|
||||||
const { confirm, onDelete } = options;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: 'deletemany',
|
|
||||||
onSetup(ctx) {
|
|
||||||
context = ctx;
|
|
||||||
tableProps = ctx.props.tableProps!;
|
|
||||||
},
|
|
||||||
options(options) {
|
|
||||||
let selectPlugin = options.plugins?.find(i => i.id === 'selection');
|
|
||||||
if (!selectPlugin) {
|
|
||||||
selectPlugin = useTableSelect();
|
|
||||||
options.plugins!.push(selectPlugin);
|
|
||||||
}
|
|
||||||
selected = selectPlugin.provide!.selectedKeys;
|
|
||||||
return options;
|
|
||||||
},
|
|
||||||
onLoaded() {
|
|
||||||
console.log('loaded');
|
|
||||||
selected.value = [];
|
|
||||||
},
|
|
||||||
action() {
|
|
||||||
const onClick = async (props: any) => {
|
|
||||||
delConfirm({
|
|
||||||
...delOptions,
|
|
||||||
content: confirm ?? '危险操作,确定删除所选数据吗?',
|
|
||||||
async onBeforeOk() {
|
|
||||||
await sleep(3000);
|
|
||||||
try {
|
|
||||||
const res: any = await onDelete?.(props);
|
|
||||||
const msg = res?.data?.message;
|
|
||||||
msg && Message.success(`提示: ${msg}`);
|
|
||||||
if (tableProps) {
|
|
||||||
(tableProps as any).selectedKeys = [];
|
|
||||||
}
|
|
||||||
selected.value = [];
|
|
||||||
context.refresh();
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
console.log('删除失败:', e);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return props => (
|
|
||||||
<Button status="danger" disabled={!selected.value.length} onClick={() => onClick(props)}>
|
|
||||||
{{
|
|
||||||
icon: () => <i class="icon-park-outline-delete" />,
|
|
||||||
default: () => '删除',
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import { Button } from '@arco-design/web-vue';
|
|
||||||
import { AnTablePlugin } from '../hooks/useTablePlugin';
|
|
||||||
import { AnTableContext } from '../components/Table';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插件:添加刷新按钮
|
|
||||||
* @description 位于搜索栏附近
|
|
||||||
*/
|
|
||||||
export function useTableRefresh(): AnTablePlugin {
|
|
||||||
let context: AnTableContext;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: 'refresh',
|
|
||||||
onSetup(ctx) {
|
|
||||||
context = ctx;
|
|
||||||
},
|
|
||||||
widget() {
|
|
||||||
return () => {
|
|
||||||
const { loading, refresh } = context;
|
|
||||||
return (
|
|
||||||
<Button loading={loading.value} onClick={refresh}>
|
|
||||||
{{
|
|
||||||
icon: () => <span class="icon-park-outline-redo"></span>,
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
import { cloneDeep, defaultsDeep, merge } from 'lodash-es';
|
|
||||||
import { TableUseOptions } from '../hooks/useTable';
|
|
||||||
import { AnTablePlugin } from '../hooks/useTablePlugin';
|
|
||||||
import { AnTableContext, ArcoTableProps } from '../components/Table';
|
|
||||||
|
|
||||||
const defaults: TableUseOptions = {
|
|
||||||
tableProps: {
|
|
||||||
rowSelection: {
|
|
||||||
showCheckedAll: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
interface UseTableSelectOptions {
|
|
||||||
key?: string;
|
|
||||||
mode?: 'key' | 'row' | 'both';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插件:表格多选
|
|
||||||
* @description 请配合其他插件使用
|
|
||||||
*/
|
|
||||||
export function useTableSelect({ key = 'id', mode = 'key' }: UseTableSelectOptions = {}): AnTablePlugin {
|
|
||||||
let context: AnTableContext;
|
|
||||||
const selectedKeys = ref<(string | number)[]>([]);
|
|
||||||
const selectedRows = ref<any[]>([]);
|
|
||||||
const setKeys = (keys: any[]) => {
|
|
||||||
const tableProps = context.props.tableProps;
|
|
||||||
if (tableProps) {
|
|
||||||
(tableProps as any).selectedKeys = keys;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: 'selection',
|
|
||||||
provide: {
|
|
||||||
selectedKeys,
|
|
||||||
selectedRows,
|
|
||||||
},
|
|
||||||
onSetup(ctx) {
|
|
||||||
context = ctx;
|
|
||||||
},
|
|
||||||
options(options) {
|
|
||||||
const opts: TableUseOptions = defaultsDeep({}, defaults);
|
|
||||||
|
|
||||||
if (!opts.tableProps!.rowKey) {
|
|
||||||
opts.tableProps!.rowKey = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === 'both' || mode === 'key') {
|
|
||||||
opts.tableProps!.onSelectionChange = rowkeys => {
|
|
||||||
selectedKeys.value = rowkeys;
|
|
||||||
setKeys(rowkeys);
|
|
||||||
console.log(rowkeys);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === 'both' || mode === 'row') {
|
|
||||||
opts.tableProps!.onSelect = (rowkeys, rowkey, record) => {
|
|
||||||
const index = selectedRows.value.findIndex((i: any) => i[key] == record[key]);
|
|
||||||
if (index > -1) {
|
|
||||||
selectedRows.value.splice(index, 1);
|
|
||||||
}
|
|
||||||
setKeys(selectedRows.value.map(i => i.id));
|
|
||||||
};
|
|
||||||
opts.tableProps!.onSelectAll = checked => {
|
|
||||||
if (checked) {
|
|
||||||
selectedRows.value = cloneDeep([]);
|
|
||||||
} else {
|
|
||||||
selectedRows.value = [];
|
|
||||||
}
|
|
||||||
setKeys(selectedRows.value.map(i => i.id));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return merge(options, opts);
|
|
||||||
},
|
|
||||||
onLoaded() {
|
|
||||||
setKeys([]);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
export function useVisible(initial = false) {
|
|
||||||
const visible = ref(initial);
|
|
||||||
const show = () => (visible.value = true);
|
|
||||||
const hide = () => (visible.value = false);
|
|
||||||
const toggle = () => (visible.value = !visible.value);
|
|
||||||
|
|
||||||
return {
|
|
||||||
visible,
|
|
||||||
show,
|
|
||||||
hide,
|
|
||||||
toggle,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import NProgress from 'nprogress';
|
|
||||||
import 'nprogress/nprogress.css';
|
|
||||||
import './nprogress.css';
|
|
||||||
import { App } from 'vue';
|
|
||||||
|
|
||||||
NProgress.configure({
|
|
||||||
showSpinner: false,
|
|
||||||
trickleSpeed: 200,
|
|
||||||
minimum: 0.3,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 作为VUE插件进行初始化
|
|
||||||
*/
|
|
||||||
NProgress.install = function (app: App) {};
|
|
||||||
|
|
||||||
export { NProgress };
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import 'nprogress';
|
|
||||||
import { App } from 'vue';
|
|
||||||
|
|
||||||
declare module 'nprogress' {
|
|
||||||
interface NProgress {
|
|
||||||
install: (app: App) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { createApp } from 'vue';
|
|
||||||
import App from '@/App.vue';
|
import App from '@/App.vue';
|
||||||
|
import { api } from '@/api';
|
||||||
|
import { dayjs } from '@/plugins/dayjs';
|
||||||
|
import { NProgress } from '@/plugins/nprogress';
|
||||||
import { router } from '@/router';
|
import { router } from '@/router';
|
||||||
import { store } from '@/store';
|
import { store } from '@/store';
|
||||||
import { style } from '@/styles';
|
import { style } from '@/styles';
|
||||||
import { dayjs } from '@/libs/dayjs';
|
import { createApp } from 'vue';
|
||||||
import { NProgress } from '@/libs/nprogress';
|
|
||||||
import { api } from '@/api';
|
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import Image404 from './image-404.svg?raw';
|
import Image404 from './404.svg?raw';
|
||||||
|
|
||||||
defineOptions({ name: 'AllUncatchedPage' });
|
defineOptions({ name: 'AllUncatchedPage' });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,16 @@
|
||||||
<password-modal></password-modal>
|
<password-modal></password-modal>
|
||||||
</span>
|
</span>
|
||||||
<template #content>
|
<template #content>
|
||||||
<a-doption class="bg-transparent!">
|
<!-- <a-doption class="bg-transparent!">
|
||||||
<div class="w-[200px] flex items-center gap-2">
|
<div class="w-[200px] flex items-center gap-2">
|
||||||
<a-avatar :size="32">
|
<a-avatar :size="32">
|
||||||
<img :src="userStore.avatar || 'https://github.com/juetan.png'" :alt="userStore.nickname" />
|
<img :src="userStore.avatar || 'https://github.com/juetan.png'" :alt="userStore.nickname" />
|
||||||
</a-avatar>
|
</a-avatar>
|
||||||
<div class="leading-4 text-base my-2">
|
<div class="leading-4 text-base my-2">
|
||||||
{{ userStore.nickname }}
|
<div class="flex items-center gap-2">
|
||||||
<a-tag color="red" size="small" >管理员</a-tag>
|
{{ userStore.nickname }}
|
||||||
|
<a-tag color="red" size="small">管理员</a-tag>
|
||||||
|
</div>
|
||||||
<div class="text-xs text-gray-400">
|
<div class="text-xs text-gray-400">
|
||||||
<span class="text-gray-400">@{{ userStore.username }}</span>
|
<span class="text-gray-400">@{{ userStore.username }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -38,7 +40,6 @@
|
||||||
</template>
|
</template>
|
||||||
账号信息
|
账号信息
|
||||||
</a-doption>
|
</a-doption>
|
||||||
<!-- <a-divider :margin="4" class="border-gray-100!"></a-divider> -->
|
|
||||||
<a-doption @click="router.push('/user')">
|
<a-doption @click="router.push('/user')">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-config"></i>
|
<i class="icon-park-outline-config"></i>
|
||||||
|
|
@ -51,7 +52,7 @@
|
||||||
</template>
|
</template>
|
||||||
关于
|
关于
|
||||||
</a-doption>
|
</a-doption>
|
||||||
<a-divider :margin="4" class="border-gray-100!"></a-divider>
|
<a-divider :margin="4" class="border-gray-100!"></a-divider> -->
|
||||||
<a-doption @click="logout">
|
<a-doption @click="logout">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-power"></i>
|
<i class="icon-park-outline-power"></i>
|
||||||
|
|
@ -63,10 +64,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useFormModal } from '@/components/AnForm';
|
|
||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
import { delConfirm, sleep } from '@/utils';
|
import { delConfirm, sleep } from '@/utils';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { useFormModal } from 'arconify';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
@ -85,10 +86,12 @@ const logout = async () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const { component: PasswordModal, open } = useFormModal({
|
const PasswordModal = useFormModal({
|
||||||
title: '修改密码',
|
|
||||||
trigger: false,
|
trigger: false,
|
||||||
width: 500,
|
modalProps: {
|
||||||
|
title: '修改密码',
|
||||||
|
width: 500,
|
||||||
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'password',
|
field: 'password',
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,12 @@
|
||||||
<a-layout class="layout">
|
<a-layout class="layout">
|
||||||
<a-layout-header class="h-13 overflow-hidden flex justify-between items-center gap-4 px-2 pr-4 border-b border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700">
|
<a-layout-header class="h-13 overflow-hidden flex justify-between items-center gap-4 px-2 pr-4 border-b border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700">
|
||||||
<div class="h-13 flex items-center">
|
<div class="h-13 flex items-center">
|
||||||
<!-- <a-button size="small" @click="isCollapsed = !isCollapsed">
|
|
||||||
<template #icon>
|
|
||||||
<i class="icon-park-outline-hamburger-button text-base"></i>
|
|
||||||
</template>
|
|
||||||
</a-button> -->
|
|
||||||
<router-link to="/" class="px-2 flex items-center gap-2 text-slate-700">
|
<router-link to="/" class="px-2 flex items-center gap-2 text-slate-700">
|
||||||
<img src="/favicon.ico" alt="" width="24" height="24" class="" />
|
<img :src="appStore.logoUrl" alt="" width="24" height="24" class="" />
|
||||||
<h1 class="relative text-[18px] leading-[22px] dark:text-white m-0 p-0 font-normal">
|
<h1 class="relative text-[20px] leading-[22px] dark:text-white m-0 p-0 font-normal">
|
||||||
{{ appStore.title }}
|
{{ appStore.title }}
|
||||||
<span class="absolute -right-10 -top-1 font-normal text-xs text-gray-400"> v0.0.1 </span>
|
<span class="absolute -right-10 -top-1 font-normal text-xs text-gray-400"> v0.0.1 </span>
|
||||||
</h1>
|
</h1>
|
||||||
<!-- <span class="text-gray-400">{{ appStore.subtitle }}</span> -->
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|
@ -56,9 +50,6 @@
|
||||||
<a-layout class="layout-content flex-1">
|
<a-layout class="layout-content flex-1">
|
||||||
<a-layout-content class="overflow-x-auto">
|
<a-layout-content class="overflow-x-auto">
|
||||||
<a-spin :loading="appStore.pageLoding" class="block h-full w-full">
|
<a-spin :loading="appStore.pageLoding" class="block h-full w-full">
|
||||||
<template #icon>
|
|
||||||
<div class="loader"></div>
|
|
||||||
</template>
|
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<keep-alive :include="menuStore.caches">
|
<keep-alive :include="menuStore.caches">
|
||||||
<component v-if="hasAuth" :is="Component"></component>
|
<component v-if="hasAuth" :is="Component"></component>
|
||||||
|
|
@ -75,11 +66,11 @@
|
||||||
<script lang="tsx" setup>
|
<script lang="tsx" setup>
|
||||||
import { useAppStore } from '@/store/app';
|
import { useAppStore } from '@/store/app';
|
||||||
import { useMenuStore } from '@/store/menu';
|
import { useMenuStore } from '@/store/menu';
|
||||||
|
import { useUserStore } from '@/store/user';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { useFullscreen } from '@vueuse/core';
|
import { useFullscreen } from '@vueuse/core';
|
||||||
import Menu from './Menu.vue';
|
import Menu from './Menu.vue';
|
||||||
import userDropdown from './UserDropdown.vue';
|
import userDropdown from './UserDropdown.vue';
|
||||||
import { useUserStore } from '@/store/user';
|
|
||||||
|
|
||||||
defineOptions({ name: 'LayoutPage' });
|
defineOptions({ name: 'LayoutPage' });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-center w-full overflow-hidden">
|
<div class="flex items-center justify-center w-full overflow-hidden">
|
||||||
<div
|
<div class="login-box w-[960px] h-[560px] relative mx-4 grid md:grid-cols-2 rounded-lg overflow-hidden border border-blue-100">
|
||||||
class="login-box w-[960px] h-[560px] relative mx-4 grid md:grid-cols-2 rounded-lg overflow-hidden border border-blue-100"
|
<div class="login-left relative hidden md:block w-full h-full overflow-hidden bg-[rgb(var(--primary-6))] px-4"></div>
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="login-left relative hidden md:block w-full h-full overflow-hidden bg-[rgb(var(--primary-6))] px-4"
|
|
||||||
></div>
|
|
||||||
<div class="relative p-20 px-8 md:px-14 bg-white shadow-sm">
|
<div class="relative p-20 px-8 md:px-14 bg-white shadow-sm">
|
||||||
<div class="text-xl text-brand-500 font-semibold">用户登陆</div>
|
<div class="text-xl text-brand-500 font-semibold">用户登陆</div>
|
||||||
<div class="text-gray-500 mt-2.5">{{ meridiem }}好,欢迎访问 {{ appStore.title }} 系统!</div>
|
<div class="text-gray-500 mt-2.5">{{ meridiem }}好,欢迎访问 {{ appStore.title }} 系统!</div>
|
||||||
<a-form ref="formRef" :model="model" :rules="formRules" layout="vertical" class="mt-6">
|
<a-form ref="formRef" :model="model" :rules="formRules" layout="vertical" class="mt-6">
|
||||||
<a-form-item field="username" label="账号" :disabled="loading" hide-asterisk>
|
<a-form-item field="username" label="账号" :disabled="loading" hide-asterisk>
|
||||||
<a-input v-model="model.username" placeholder="请输入账号/手机号/邮箱" allow-clear>
|
<a-input v-model="model.username" placeholder="请输入账号" allow-clear>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<i class="icon-park-outline-user" />
|
<i class="icon-park-outline-user" />
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -56,10 +52,10 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { dayjs } from '@/libs/dayjs';
|
|
||||||
import { useAppStore } from '@/store/app';
|
import { useAppStore } from '@/store/app';
|
||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
import { FieldRule, Form, Message, Modal, Notification } from '@arco-design/web-vue';
|
import { FieldRule, Form, Message, Modal, Notification } from '@arco-design/web-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { reactive } from 'vue';
|
import { reactive } from 'vue';
|
||||||
|
|
||||||
defineOptions({ name: 'LoginPage' });
|
defineOptions({ name: 'LoginPage' });
|
||||||
|
|
@ -77,7 +73,7 @@ const formRules: Record<string, FieldRule[]> = {
|
||||||
username: [
|
username: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: '请输入账号/手机号/邮箱',
|
message: '请输入账号',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
password: [
|
password: [
|
||||||
|
|
@ -101,8 +97,8 @@ const onSubmitForm = async () => {
|
||||||
if (await formRef.value?.validate()) {
|
if (await formRef.value?.validate()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
|
||||||
const res = await api.auth.login(model);
|
const res = await api.auth.login(model);
|
||||||
userStore.setAccessToken(res.data.data);
|
userStore.setAccessToken(res.data.data);
|
||||||
Notification.success({
|
Notification.success({
|
||||||
|
|
@ -113,9 +109,8 @@ const onSubmitForm = async () => {
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const message = error?.response?.data?.message;
|
const message = error?.response?.data?.message;
|
||||||
message && Message.warning(`提示:${message}`);
|
message && Message.warning(`提示:${message}`);
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
}
|
||||||
|
loading.value = false;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
|
||||||
import { listToTree } from '@/utils/listToTree';
|
import { listToTree } from '@/utils/listToTree';
|
||||||
|
import { useTable } from 'arconify';
|
||||||
|
|
||||||
const { component: CategoryTable } = useTable({
|
const CategoryTable = useTable({
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: '分类名称',
|
title: '分类名称',
|
||||||
|
|
@ -18,14 +18,12 @@ const { component: CategoryTable } = useTable({
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden">
|
||||||
<span>
|
<span>
|
||||||
{record.title}
|
{record.title}
|
||||||
<span class="text-gray-400 text-xs truncate ml-2">@{record.slug}</span>
|
<span class="text-gray-400 text-xs truncate ml-2">@{record.slug}</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="text-gray-400 text-xs truncate mt-0.5">{record.description}</div>
|
<div class="text-gray-400 text-xs truncate mt-0.5">{record.description}</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
useCreateColumn(),
|
|
||||||
useUpdateColumn(),
|
|
||||||
{
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
title: '操作',
|
title: '操作',
|
||||||
|
|
@ -48,10 +46,10 @@ const { component: CategoryTable } = useTable({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
source: async model => {
|
data: async model => {
|
||||||
const res = await api.category.getCategories(model);
|
const res = await api.category.getCategories(model);
|
||||||
const data = listToTree(res.data.data ?? []);
|
const data = listToTree(res.data.data ?? []);
|
||||||
return { data: { data, total: (res.data as any).total } };
|
return { data, total: (res.data as any).total };
|
||||||
},
|
},
|
||||||
search: [
|
search: [
|
||||||
{
|
{
|
||||||
|
|
@ -63,8 +61,9 @@ const { component: CategoryTable } = useTable({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
create: {
|
create: {
|
||||||
title: '添加分类',
|
modalProps: {
|
||||||
width: 580,
|
width: 580,
|
||||||
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'title',
|
field: 'title',
|
||||||
|
|
@ -78,8 +77,8 @@ const { component: CategoryTable } = useTable({
|
||||||
setter: 'input',
|
setter: 'input',
|
||||||
required: true,
|
required: true,
|
||||||
setterProps: {
|
setterProps: {
|
||||||
placeholder: '只包含字母、小数和连字符'
|
placeholder: '只包含字母、小数和连字符',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'description',
|
field: 'description',
|
||||||
|
|
@ -94,7 +93,6 @@ const { component: CategoryTable } = useTable({
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: '修改分类',
|
|
||||||
submit: model => {
|
submit: model => {
|
||||||
return api.category.setCategory(model.id, model as any);
|
return api.category.setCategory(model.id, model as any);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
import { useTable } from 'arconify';
|
||||||
|
|
||||||
const { component: CategoryTable } = useTable({
|
const CategoryTable = useTable({
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: '文章标题',
|
title: '文章标题',
|
||||||
|
|
@ -20,8 +20,6 @@ const { component: CategoryTable } = useTable({
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
useCreateColumn(),
|
|
||||||
useUpdateColumn(),
|
|
||||||
{
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
title: '操作',
|
title: '操作',
|
||||||
|
|
@ -39,7 +37,10 @@ const { component: CategoryTable } = useTable({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
source: async model => api.post.getPosts(model),
|
data: async model => {
|
||||||
|
const res = await api.post.getPosts(model);
|
||||||
|
return { data: [], total: 100 };
|
||||||
|
},
|
||||||
search: [
|
search: [
|
||||||
{
|
{
|
||||||
field: 'nickname',
|
field: 'nickname',
|
||||||
|
|
@ -50,8 +51,10 @@ const { component: CategoryTable } = useTable({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
create: {
|
create: {
|
||||||
title: '添加文章',
|
modalProps: {
|
||||||
width: 1080,
|
title: '添加文章',
|
||||||
|
width: 1080,
|
||||||
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'title',
|
field: 'title',
|
||||||
|
|
@ -81,7 +84,6 @@ const { component: CategoryTable } = useTable({
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: '修改文章',
|
|
||||||
submit: model => {
|
submit: model => {
|
||||||
return api.post.updatePost(model.id, model);
|
return api.post.updatePost(model.id, model);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<bread-page>
|
<AnPage>
|
||||||
<CategoryTable />
|
<CategoryTable />
|
||||||
</bread-page>
|
</AnPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
|
||||||
import { listToTree } from '@/utils/listToTree';
|
import { listToTree } from '@/utils/listToTree';
|
||||||
|
import { useTable } from 'arconify';
|
||||||
|
|
||||||
const { component: CategoryTable } = useTable({
|
const CategoryTable = useTable({
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: '分类名称',
|
title: '分类名称',
|
||||||
|
|
@ -17,19 +17,18 @@ const { component: CategoryTable } = useTable({
|
||||||
render: ({ record }) => (
|
render: ({ record }) => (
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden">
|
||||||
<span>
|
<span>
|
||||||
{record.name}
|
{record.name ?? '无'}
|
||||||
<span class="text-orange-500 truncate ml-2">@{record.code}</span>
|
<span class="text-orange-500 truncate ml-2">@{record.code}</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="text-gray-400 text-xs truncate mt-0.5">{record.description}</div>
|
<div class="text-gray-400 text-xs truncate mt-0.5">{record.description}</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
useCreateColumn(),
|
|
||||||
useUpdateColumn(),
|
|
||||||
{
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
title: '操作',
|
title: '操作',
|
||||||
width: 120,
|
width: 120,
|
||||||
|
align: 'right',
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
type: 'modify',
|
type: 'modify',
|
||||||
|
|
@ -45,10 +44,10 @@ const { component: CategoryTable } = useTable({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
source: async model => {
|
data: async model => {
|
||||||
const res = await api.fileCategory.getFileCategorys(model);
|
const res = await api.fileCategory.getFileCategorys(model);
|
||||||
const data = listToTree(res.data.data ?? []);
|
const data = listToTree(res.data.data ?? []);
|
||||||
return { data: { data, total: (res.data as any).total } };
|
return [];
|
||||||
},
|
},
|
||||||
search: [
|
search: [
|
||||||
{
|
{
|
||||||
|
|
@ -60,8 +59,10 @@ const { component: CategoryTable } = useTable({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
create: {
|
create: {
|
||||||
title: '添加分类',
|
modalProps: {
|
||||||
width: 580,
|
title: '添加分类',
|
||||||
|
width: 580,
|
||||||
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
|
|
@ -91,7 +92,6 @@ const { component: CategoryTable } = useTable({
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: '修改分类',
|
|
||||||
submit: model => {
|
submit: model => {
|
||||||
return api.fileCategory.setFileCategory(model.id, model as any);
|
return api.fileCategory.setFileCategory(model.id, model as any);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
|
||||||
import { listToTree } from '@/utils/listToTree';
|
import { listToTree } from '@/utils/listToTree';
|
||||||
|
import { useTable } from 'arconify';
|
||||||
|
|
||||||
const { component: CategoryTable } = useTable({
|
const CategoryTable = useTable({
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: '分类名称',
|
title: '分类名称',
|
||||||
|
|
@ -16,14 +16,13 @@ const { component: CategoryTable } = useTable({
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden">
|
||||||
<span>
|
<span>
|
||||||
{record.name}
|
{record.name}
|
||||||
<span class="text-gray-400 text-xs truncate ml-2">@{record.code}</span>
|
<span class="text-gray-400 text-xs truncate ml-2">@{record.code}</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="text-gray-400 text-xs truncate mt-0.5">{record.description}</div>
|
<div class="text-gray-400 text-xs truncate mt-0.5">{record.description}</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
useCreateColumn(),
|
|
||||||
useUpdateColumn(),
|
|
||||||
{
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
title: '操作',
|
title: '操作',
|
||||||
|
|
@ -43,10 +42,10 @@ const { component: CategoryTable } = useTable({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
source: async model => {
|
data: async model => {
|
||||||
const res = await api.fileCategory.getFileCategorys(model);
|
const res = await api.fileCategory.getFileCategorys(model);
|
||||||
const data = listToTree(res.data.data ?? []);
|
const data = listToTree(res.data.data ?? []);
|
||||||
return { data: { data, total: (res.data as any).total } };
|
return [];
|
||||||
},
|
},
|
||||||
search: [
|
search: [
|
||||||
{
|
{
|
||||||
|
|
@ -58,8 +57,10 @@ const { component: CategoryTable } = useTable({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
create: {
|
create: {
|
||||||
title: '添加分类',
|
modalProps: {
|
||||||
width: 580,
|
title: '添加分类',
|
||||||
|
width: 580,
|
||||||
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
|
|
@ -73,8 +74,8 @@ const { component: CategoryTable } = useTable({
|
||||||
setter: 'input',
|
setter: 'input',
|
||||||
required: true,
|
required: true,
|
||||||
setterProps: {
|
setterProps: {
|
||||||
placeholder: '只包含字母、小数和连字符'
|
placeholder: '只包含字母、小数和连字符',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'description',
|
field: 'description',
|
||||||
|
|
@ -89,7 +90,6 @@ const { component: CategoryTable } = useTable({
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: '修改分类',
|
|
||||||
submit: model => {
|
submit: model => {
|
||||||
return api.fileCategory.setFileCategory(model.id, model as any);
|
return api.fileCategory.setFileCategory(model.id, model as any);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="w-[210px] h-full overflow-hidden grid grid-rows-[auto_1fr]">
|
<div class="w-[210px] h-full overflow-hidden grid grid-rows-[auto_1fr]">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<a-input-search allow-clear placeholder="分类名称" class="mb-2" @search="updateFileCategories"></a-input-search>
|
<a-input-search allow-clear placeholder="分类名称" class="mb-2" @search="updateFileCategories"></a-input-search>
|
||||||
<a-button @click="() => open()">
|
<a-button @click="() => CategoryModal.open()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-add"></i>
|
<i class="icon-park-outline-add"></i>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -12,10 +12,7 @@
|
||||||
<a-spin :loading="loading" class="w-full h-full">
|
<a-spin :loading="loading" class="w-full h-full">
|
||||||
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
|
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
|
||||||
<ul v-if="list.length" class="pl-0 mt-0">
|
<ul v-if="list.length" class="pl-0 mt-0">
|
||||||
<li
|
<li :class="{ active: !current?.id }" class="group flex items-center justify-between gap-1 h-8 rounded mb-2 pl-3 hover:bg-gray-100 cursor-pointer">
|
||||||
:class="{ active: !current?.id }"
|
|
||||||
class="group flex items-center justify-between gap-1 h-8 rounded mb-2 pl-3 hover:bg-gray-100 cursor-pointer"
|
|
||||||
>
|
|
||||||
<div class="flex-1 h-full flex items-center gap-2 overflow-hidden" @click="emit('change', {})">
|
<div class="flex-1 h-full flex items-center gap-2 overflow-hidden" @click="emit('change', {})">
|
||||||
<i class="icon-park-outline-folder-close align-[-2px]"></i>
|
<i class="icon-park-outline-folder-close align-[-2px]"></i>
|
||||||
<span class="flex-1 truncate">全部</span>
|
<span class="flex-1 truncate">全部</span>
|
||||||
|
|
@ -40,7 +37,7 @@
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
<template #content>
|
<template #content>
|
||||||
<a-doption @click="open(item)">
|
<a-doption @click="CategoryModal.open(item)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-edit"></i>
|
<i class="icon-park-outline-edit"></i>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -65,9 +62,9 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { FileCategory, api } from '@/api';
|
import { FileCategory, api } from '@/api';
|
||||||
import { useFormModal } from '@/components/AnForm';
|
|
||||||
import { delConfirm } from '@/utils';
|
import { delConfirm } from '@/utils';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { useFormModal } from 'arconify';
|
||||||
import { PropType } from 'vue';
|
import { PropType } from 'vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
|
|
@ -101,10 +98,11 @@ const onDeleteRow = async (row: FileCategory) => {
|
||||||
Message.success(res.data.message);
|
Message.success(res.data.message);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { component: CategoryModal, open } = useFormModal({
|
const CategoryModal = useFormModal({
|
||||||
title: model => (!model.id ? '新建分类' : '修改分类'),
|
|
||||||
trigger: false,
|
trigger: false,
|
||||||
width: 580,
|
modalProps: {
|
||||||
|
width: 580,
|
||||||
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="bg-white p-4">
|
<div class="bg-white p-4">
|
||||||
<MaterialTable>
|
<MaterialTable>
|
||||||
<template #action>
|
<template #action>
|
||||||
<AnUpload @success="() => tableRef?.refresh()"></AnUpload>
|
<AnUpload @success="() => MaterialTable.tableRef.value?.refresh()"></AnUpload>
|
||||||
</template>
|
</template>
|
||||||
</MaterialTable>
|
</MaterialTable>
|
||||||
<AnPreview v-model:visible="viewer.visible" :type="viewer.type" :url="viewer.url"></AnPreview>
|
<AnPreview v-model:visible="viewer.visible" :type="viewer.type" :url="viewer.url"></AnPreview>
|
||||||
|
|
@ -26,9 +26,9 @@
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { FileCategory, api } from '@/api';
|
import { FileCategory, api } from '@/api';
|
||||||
import { useTable, useTableDelete, useUpdateColumn } from '@/components/AnTable';
|
|
||||||
import { FileTypes } from '@/constants/file';
|
import { FileTypes } from '@/constants/file';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { useTable } from 'arconify';
|
||||||
import numeral from 'numeral';
|
import numeral from 'numeral';
|
||||||
import AnCategory from './AnCategory.vue';
|
import AnCategory from './AnCategory.vue';
|
||||||
import AnPreview from './AnPreview.vue';
|
import AnPreview from './AnPreview.vue';
|
||||||
|
|
@ -46,11 +46,11 @@ const preview = (record: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCategoryChange = (category: FileCategory) => {
|
const onCategoryChange = (category: FileCategory) => {
|
||||||
if (props.search?.model) {
|
if (MaterialTable.tableRef.value?.search?.model) {
|
||||||
props.search.model.categoryId = category.id;
|
MaterialTable.tableRef.value.search.model.categoryId = category.id;
|
||||||
}
|
}
|
||||||
current.value = category;
|
current.value = category;
|
||||||
tableRef.value?.refresh();
|
MaterialTable.tableRef.value?.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyLink = (record: Recordable) => {
|
const copyLink = (record: Recordable) => {
|
||||||
|
|
@ -58,12 +58,7 @@ const copyLink = (record: Recordable) => {
|
||||||
Message.success(`已复制 ${record.name} 的地址!`);
|
Message.success(`已复制 ${record.name} 的地址!`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const MaterialTable = useTable({
|
||||||
component: MaterialTable,
|
|
||||||
tableRef,
|
|
||||||
props,
|
|
||||||
} = useTable({
|
|
||||||
plugins: [useTableDelete()],
|
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: '文件名称',
|
title: '文件名称',
|
||||||
|
|
@ -82,10 +77,7 @@ const {
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col overflow-hidden">
|
<div class="flex flex-col overflow-hidden">
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
<span
|
<span class="truncate hover:text-brand-500 hover:decoration-underline underline-offset-2 cursor-pointer" onClick={() => preview(record)}>
|
||||||
class="truncate hover:text-brand-500 hover:decoration-underline underline-offset-2 cursor-pointer"
|
|
||||||
onClick={() => preview(record)}
|
|
||||||
>
|
|
||||||
{record.name}
|
{record.name}
|
||||||
</span>
|
</span>
|
||||||
{/* <span
|
{/* <span
|
||||||
|
|
@ -116,12 +108,11 @@ const {
|
||||||
width: 150,
|
width: 150,
|
||||||
render: ({ record }) => numeral(record.size).format('0 b'),
|
render: ({ record }) => numeral(record.size).format('0 b'),
|
||||||
},
|
},
|
||||||
// useCreateColumn(),
|
|
||||||
useUpdateColumn(),
|
|
||||||
{
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
title: '操作',
|
title: '操作',
|
||||||
width: 160,
|
width: 160,
|
||||||
|
align: 'right',
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: '下载',
|
text: '下载',
|
||||||
|
|
@ -144,8 +135,8 @@ const {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
source: async model => {
|
data: async model => {
|
||||||
return api.file.getFiles(model);
|
return [];
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
hideSearch: false,
|
hideSearch: false,
|
||||||
|
|
@ -177,8 +168,10 @@ const {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
title: '修改素材',
|
modalProps: {
|
||||||
width: 580,
|
title: '修改素材',
|
||||||
|
width: 580,
|
||||||
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<BreadPage>
|
<AnPage>
|
||||||
<CategoryTable />
|
<CategoryTable />
|
||||||
</BreadPage>
|
</AnPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
import { useTable } from 'arconify';
|
||||||
|
|
||||||
const { component: CategoryTable } = useTable({
|
const CategoryTable = useTable({
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: '文章标题',
|
title: '文章标题',
|
||||||
|
|
@ -20,8 +20,6 @@ const { component: CategoryTable } = useTable({
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
useCreateColumn(),
|
|
||||||
useUpdateColumn(),
|
|
||||||
{
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
title: '操作',
|
title: '操作',
|
||||||
|
|
@ -39,7 +37,7 @@ const { component: CategoryTable } = useTable({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
source: async model => api.post.getPosts(model),
|
data: async model => [],
|
||||||
search: [
|
search: [
|
||||||
{
|
{
|
||||||
field: 'nickname',
|
field: 'nickname',
|
||||||
|
|
@ -50,8 +48,10 @@ const { component: CategoryTable } = useTable({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
create: {
|
create: {
|
||||||
title: '添加文章',
|
modalProps: {
|
||||||
width: 1080,
|
title: '添加文章',
|
||||||
|
width: 1080,
|
||||||
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'title',
|
field: 'title',
|
||||||
|
|
@ -81,7 +81,6 @@ const { component: CategoryTable } = useTable({
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: '修改文章',
|
|
||||||
submit: model => {
|
submit: model => {
|
||||||
return api.post.updatePost(model.id, model);
|
return api.post.updatePost(model.id, model);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,7 @@
|
||||||
<i class="icon-park-outline-delete text-xs"></i>
|
<i class="icon-park-outline-delete text-xs"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="py-3 px-3 border border-dashed rounded-sm border-gray-400 text-gray-500 hover:bg-gray-100 cursor-pointer">
|
||||||
class="py-3 px-3 border border-dashed rounded-sm border-gray-400 text-gray-500 hover:bg-gray-100 cursor-pointer"
|
|
||||||
>
|
|
||||||
<i class="icon-park-outline-add ml-2"></i>
|
<i class="icon-park-outline-add ml-2"></i>
|
||||||
添加服务1
|
添加服务1
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -59,9 +57,7 @@
|
||||||
<ul class="list-none w-full m-0 p-0">
|
<ul class="list-none w-full m-0 p-0">
|
||||||
<li v-for="i in 8" class="w-full h-6 items-center overflow-hidden justify-between flex gap-2 mb-2">
|
<li v-for="i in 8" class="w-full h-6 items-center overflow-hidden justify-between flex gap-2 mb-2">
|
||||||
<a-tag>{{ i }}</a-tag>
|
<a-tag>{{ i }}</a-tag>
|
||||||
<span class="flex-1 truncate hover:underline underline-offset-2 hover:text-brand-500 cursor-pointer">
|
<span class="flex-1 truncate hover:underline underline-offset-2 hover:text-brand-500 cursor-pointer"> 但是预测已加载的数据不足以 </span>
|
||||||
但是预测已加载的数据不足以
|
|
||||||
</span>
|
|
||||||
<span class="text-gray-400">3天前</span>
|
<span class="text-gray-400">3天前</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -71,7 +67,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="tsx">
|
||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { Editor } from '@/components/AnEditor';
|
import { Editor } from '@/components/AnEditor';
|
||||||
import { useTable } from '@/components/AnTable';
|
|
||||||
import { TableColumnData } from '@arco-design/web-vue';
|
import { TableColumnData } from '@arco-design/web-vue';
|
||||||
|
import { useTable } from 'arconify';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
defineOptions({ name: 'SystemLoglPage' });
|
defineOptions({ name: 'SystemLoglPage' });
|
||||||
|
|
@ -35,9 +35,11 @@ const useTwoRowsColumn = (tkey: string, bkey: string): TableColumnData['render']
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const { component: LoginLogTable } = useTable({
|
const LoginLogTable = useTable({
|
||||||
source: async model => {
|
data: async model => {
|
||||||
return api.log.getLoginLogs(model);
|
const res = await api.log.getLoginLogs(model);
|
||||||
|
const { data, total = 10 } = res.data as any;
|
||||||
|
return { data, total };
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
|
|
@ -48,9 +50,7 @@ const { component: LoginLogTable } = useTable({
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
class={
|
class={
|
||||||
record.status === null || record.status
|
record.status === null || record.status ? 'text-base text-green-500 icon-park-outline-check-one mr-2' : 'text-base text-red-500 icon-park-outline-close-one mr-2'
|
||||||
? 'text-base text-green-500 icon-park-outline-check-one mr-2'
|
|
||||||
: 'text-base text-red-500 icon-park-outline-close-one mr-2'
|
|
||||||
}
|
}
|
||||||
></span>
|
></span>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -65,7 +65,6 @@ const { component: LoginLogTable } = useTable({
|
||||||
title: '登陆地址',
|
title: '登陆地址',
|
||||||
dataIndex: 'ip',
|
dataIndex: 'ip',
|
||||||
width: 200,
|
width: 200,
|
||||||
render: useTwoRowsColumn('addr', 'ip'),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作系统',
|
title: '操作系统',
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<BreadPage>
|
<BreadPage>
|
||||||
<OperationTable></OperationTable>
|
|
||||||
</BreadPage>
|
</BreadPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { useTable } from '@/components/AnTable';
|
|
||||||
import { Image } from '@arco-design/web-vue';
|
|
||||||
|
|
||||||
const data: any = []
|
|
||||||
defineOptions({ name: 'SystemLogoPage' });
|
defineOptions({ name: 'SystemLogoPage' });
|
||||||
|
|
||||||
const { component: OperationTable } = useTable({
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
title: '标题',
|
|
||||||
dataIndex: 'title',
|
|
||||||
width: 140,
|
|
||||||
render: ({ record }) => {
|
|
||||||
return (
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<div>
|
|
||||||
<Image width={188} src={record['cover-src']}></Image>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<a-link href={record['title-href']} target="_blank">
|
|
||||||
{ record.title }
|
|
||||||
</a-link>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a-link href={record['user-href']}>{record.user}</a-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
source: model => {
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
search: [
|
|
||||||
{
|
|
||||||
field: 'nickname',
|
|
||||||
label: '登陆账号',
|
|
||||||
setter: 'input',
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<bread-page>
|
<AnPage>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-base">常规设置</h2>
|
<h2 class="m-0 text-base">常规设置</h2>
|
||||||
<p class="text-gray-500 mt-1">首次为你的帐户添加密码时,你需要前往密码重置页面,以便我们验证你的身份。</p>
|
<p class="text-gray-500 mt-1">首次为你的帐户添加密码时,你需要前往密码重置页面,以便我们验证你的身份。</p>
|
||||||
</div>
|
</div>
|
||||||
<a-form :model="{}" label-align="left" class="space-y-6 mt-6 col-form divide-y">
|
<a-form :model="{}" label-align="left" class="space-y-6 mt-6 col-form divide-y divide-gray-100">
|
||||||
<a-form-item label="站点LOGO">
|
<a-form-item label="站点LOGO">
|
||||||
<a-avatar :size="64">
|
<a-avatar :size="64">
|
||||||
<img :src="appStore.logo" alt="" />
|
<img :src="appStore.logoUrl" alt="" />
|
||||||
<template #trigger-icon>
|
<template #trigger-icon>
|
||||||
<i class="icon-park-outline-edit"></i>
|
<i class="icon-park-outline-edit"></i>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
<a-button type="primary">保存修改</a-button>
|
<a-button type="primary">保存修改</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</bread-page>
|
</AnPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<bread-page>
|
<AnPage>
|
||||||
<!-- <div>
|
<!-- <div>
|
||||||
<div class="bg-white">
|
<div class="bg-white">
|
||||||
<div v-for="t1 in types" :key="t1.label" class="flex items-center">
|
<div v-for="t1 in types" :key="t1.label" class="flex items-center">
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
|
<a-radio></a-radio>
|
||||||
<div class="mb-3">功能列表</div>
|
<div class="mb-3">功能列表</div>
|
||||||
<div v-for="i in 3" class="border-t py-4 flex justify-between items-center gap-4">
|
<div v-for="i in 3" class="border-t py-4 flex justify-between items-center gap-4">
|
||||||
<div class="flex gap-3 items-center">
|
<div class="flex gap-3 items-center">
|
||||||
|
|
@ -40,7 +41,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</bread-page>
|
</AnPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<bread-page>
|
<AnPage>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="flex item-center justify-between gap-4">
|
<div class="flex item-center justify-between gap-4">
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</bread-page>
|
</AnPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,16 @@
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { useFormModal } from '@/components/AnForm';
|
import { useFormModal, useTable } from 'arconify';
|
||||||
import { TableColumnRender, useCreateColumn, useTable } from '@/components/AnTable';
|
|
||||||
|
|
||||||
defineOptions({ name: 'SystemDepartmentPage' });
|
defineOptions({ name: 'SystemDepartmentPage' });
|
||||||
|
|
||||||
const { component: PasswordModal, open } = useFormModal({
|
const PasswordModal = useFormModal({
|
||||||
title: '重置密码',
|
|
||||||
trigger: false,
|
trigger: false,
|
||||||
width: 432,
|
modalProps: {
|
||||||
|
title: '重置密码',
|
||||||
|
width: 432,
|
||||||
|
},
|
||||||
model: {
|
model: {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
nickname: undefined,
|
nickname: undefined,
|
||||||
|
|
@ -30,7 +31,7 @@ const { component: PasswordModal, open } = useFormModal({
|
||||||
submit: model => api.user.setUser(model.id, model as any),
|
submit: model => api.user.setUser(model.id, model as any),
|
||||||
});
|
});
|
||||||
|
|
||||||
const usernameRender: TableColumnRender = ({ record }) => (
|
const usernameRender = ({ record }) => (
|
||||||
<div class="flex items-center gap-4 w-full overflow-hidden">
|
<div class="flex items-center gap-4 w-full overflow-hidden">
|
||||||
<a-avatar size={32} class="!bg-brand-500">
|
<a-avatar size={32} class="!bg-brand-500">
|
||||||
{record.avatar?.startsWith('/') ? <img src={record.avatar} alt="" /> : record.nickname?.[0]}
|
{record.avatar?.startsWith('/') ? <img src={record.avatar} alt="" /> : record.nickname?.[0]}
|
||||||
|
|
@ -54,16 +55,13 @@ const usernameRender: TableColumnRender = ({ record }) => (
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const { component: UserTable } = useTable({
|
const UserTable = useTable({
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: '用户昵称',
|
title: '用户昵称',
|
||||||
dataIndex: 'username',
|
dataIndex: 'username',
|
||||||
render: usernameRender,
|
render: usernameRender,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
...useCreateColumn(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
|
|
@ -88,8 +86,10 @@ const { component: UserTable } = useTable({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
source: model => {
|
data: async model => {
|
||||||
return api.user.getUsers(model);
|
const res = await api.user.getUsers(model);
|
||||||
|
const { data, total } = res.data as any;
|
||||||
|
return { data, total };
|
||||||
},
|
},
|
||||||
search: [
|
search: [
|
||||||
{
|
{
|
||||||
|
|
@ -99,9 +99,13 @@ const { component: UserTable } = useTable({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
create: {
|
create: {
|
||||||
title: '新建用户',
|
modalProps: {
|
||||||
width: 820,
|
title: '新建用户',
|
||||||
formClass: '!grid grid-cols-2 gap-x-6',
|
width: 820,
|
||||||
|
},
|
||||||
|
formProps: {
|
||||||
|
class: '!grid grid-cols-2 gap-x-6',
|
||||||
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'avatar',
|
field: 'avatar',
|
||||||
|
|
@ -156,7 +160,7 @@ const { component: UserTable } = useTable({
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: '修改用户',
|
|
||||||
submit: model => {
|
submit: model => {
|
||||||
return api.user.setUser(model.id, model as any);
|
return api.user.setUser(model.id, model as any);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="w-[210px] h-full overflow-hidden grid grid-rows-[auto_1fr]">
|
<div class="w-[210px] h-full overflow-hidden grid grid-rows-[auto_1fr]">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<a-input-search allow-clear placeholder="字典类型" class="mb-2"></a-input-search>
|
<a-input-search allow-clear placeholder="字典类型" class="mb-2"></a-input-search>
|
||||||
<a-button @click="open()">
|
<a-button @click="DictTypeModal.open()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-add"></i>
|
<i class="icon-park-outline-add"></i>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
<template #content>
|
<template #content>
|
||||||
<a-doption @click="open(item)">
|
<a-doption @click="DictTypeModal.open(item)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-edit"></i>
|
<i class="icon-park-outline-edit"></i>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -53,9 +53,9 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DictType, api } from '@/api';
|
import { DictType, api } from '@/api';
|
||||||
import { useFormModal } from '@/components/AnForm';
|
|
||||||
import { delConfirm } from '@/utils';
|
import { delConfirm } from '@/utils';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { useFormModal } from 'arconify';
|
||||||
import { PropType } from 'vue';
|
import { PropType } from 'vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
|
|
@ -81,10 +81,12 @@ const onDeleteRow = async (row: DictType) => {
|
||||||
Message.success(res.data.message);
|
Message.success(res.data.message);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { component: DictTypeModal, open } = useFormModal({
|
const DictTypeModal = useFormModal({
|
||||||
title: ({ model }) => (!model.id ? '新建字典类型' : '修改字典类型'),
|
|
||||||
trigger: false,
|
trigger: false,
|
||||||
width: 580,
|
modalProps: {
|
||||||
|
title: '字典类型',
|
||||||
|
width: 580,
|
||||||
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
|
|
|
||||||
|
|
@ -27,17 +27,17 @@
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { DictType, api } from '@/api';
|
import { DictType, api } from '@/api';
|
||||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
import { useTable } from 'arconify';
|
||||||
import AnGroup from './Group.vue';
|
import AnGroup from './Group.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'SystemDictPage' });
|
defineOptions({ name: 'SystemDictPage' });
|
||||||
const current = ref<DictType>();
|
const current = ref<DictType>();
|
||||||
const onTypeChange = (item: DictType) => {
|
const onTypeChange = (item: DictType) => {
|
||||||
current.value = item;
|
current.value = item;
|
||||||
tableRef.value?.refresh();
|
DictTable.tableRef.value?.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
const { component: DictTable, tableRef } = useTable({
|
const DictTable = useTable({
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: '字典项',
|
title: '字典项',
|
||||||
|
|
@ -52,8 +52,6 @@ const { component: DictTable, tableRef } = useTable({
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
useCreateColumn(),
|
|
||||||
useUpdateColumn(),
|
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
|
|
@ -73,8 +71,8 @@ const { component: DictTable, tableRef } = useTable({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
source: search => {
|
data: search => {
|
||||||
return api.dict.getDicts({ ...search, typeId: current.value?.id } as any);
|
return [];
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
hideSearch: true,
|
hideSearch: true,
|
||||||
|
|
@ -89,8 +87,10 @@ const { component: DictTable, tableRef } = useTable({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
title: '新增字典',
|
modalProps: {
|
||||||
width: 580,
|
title: '新增字典',
|
||||||
|
width: 580,
|
||||||
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
|
|
@ -117,7 +117,7 @@ const { component: DictTable, tableRef } = useTable({
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: '修改字典',
|
|
||||||
submit: model => {
|
submit: model => {
|
||||||
const data = { ...model, typeId: current.value?.id } as any;
|
const data = { ...model, typeId: current.value?.id } as any;
|
||||||
return api.dict.setDict(model.id, data);
|
return api.dict.setDict(model.id, data);
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
|
||||||
import { MenuType, MenuTypes } from '@/constants/menu';
|
import { MenuType, MenuTypes } from '@/constants/menu';
|
||||||
import { flatMenus } from '@/router';
|
import { flatMenus } from '@/router';
|
||||||
import { listToTree } from '@/utils/listToTree';
|
import { listToTree } from '@/utils/listToTree';
|
||||||
|
import { useTable } from 'arconify';
|
||||||
|
|
||||||
defineOptions({ name: 'SystemMenuPage' });
|
defineOptions({ name: 'SystemMenuPage' });
|
||||||
|
|
||||||
|
|
@ -17,10 +17,10 @@ const menuArr = flatMenus.map(i => ({ label: i.title, value: i.id }));
|
||||||
const expanded = ref(false);
|
const expanded = ref(false);
|
||||||
const toggleExpand = () => {
|
const toggleExpand = () => {
|
||||||
expanded.value = !expanded.value;
|
expanded.value = !expanded.value;
|
||||||
tableRef.value?.tableRef?.expandAll(expanded.value);
|
MenuTable.tableRef.value?.tableRef?.expandAll(expanded.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { component: MenuTable, tableRef } = useTable({
|
const MenuTable = useTable({
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: () => (
|
title: () => (
|
||||||
|
|
@ -66,8 +66,6 @@ const { component: MenuTable, tableRef } = useTable({
|
||||||
</a-tag>
|
</a-tag>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
useCreateColumn(),
|
|
||||||
useUpdateColumn(),
|
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
|
|
@ -94,7 +92,7 @@ const { component: MenuTable, tableRef } = useTable({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
source: search => api.menu.getMenus({ ...search, tree: true, size: 0 }),
|
data: search => [],
|
||||||
search: [
|
search: [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
|
|
@ -104,9 +102,13 @@ const { component: MenuTable, tableRef } = useTable({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
create: {
|
create: {
|
||||||
title: '新建菜单',
|
modalProps: {
|
||||||
width: 980,
|
title: '新建菜单',
|
||||||
formClass: '!grid grid-cols-2 gap-x-4',
|
width: 980,
|
||||||
|
},
|
||||||
|
formProps: {
|
||||||
|
class: '!grid grid-cols-2 gap-x-4',
|
||||||
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'parentId',
|
field: 'parentId',
|
||||||
|
|
@ -205,7 +207,7 @@ const { component: MenuTable, tableRef } = useTable({
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: '修改菜单',
|
|
||||||
submit: model => {
|
submit: model => {
|
||||||
return api.menu.setMenu(model.id, model);
|
return api.menu.setMenu(model.id, model);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
import { useTable } from 'arconify';
|
||||||
|
|
||||||
defineOptions({ name: 'SystemRolePage' });
|
defineOptions({ name: 'SystemRolePage' });
|
||||||
|
|
||||||
const { component: RoleTable } = useTable({
|
const RoleTable = useTable({
|
||||||
source: () => {
|
data: () => {
|
||||||
return api.role.getRoles();
|
return [];
|
||||||
},
|
},
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
|
|
@ -28,8 +28,6 @@ const { component: RoleTable } = useTable({
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
useCreateColumn(),
|
|
||||||
useUpdateColumn(),
|
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
|
|
@ -63,8 +61,10 @@ const { component: RoleTable } = useTable({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
create: {
|
create: {
|
||||||
title: '新建角色',
|
modalProps: {
|
||||||
width: 580,
|
title: '新建角色',
|
||||||
|
width: 580,
|
||||||
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
|
|
@ -90,7 +90,6 @@ const { component: RoleTable } = useTable({
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: '修改角色',
|
|
||||||
submit: model => {
|
submit: model => {
|
||||||
return api.role.updateRole(model.id, model);
|
return api.role.updateRole(model.id, model);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,16 @@
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
import { useFormModal } from '@/components/AnForm';
|
import { TableColumnRender, useFormModal, useTable } from 'arconify';
|
||||||
import { TableColumnRender, useTable } from '@/components/AnTable';
|
|
||||||
|
|
||||||
defineOptions({ name: 'SystemUserPage' });
|
defineOptions({ name: 'SystemUserPage' });
|
||||||
|
|
||||||
const { component: PasswordModal, open } = useFormModal({
|
const PasswordModal = useFormModal({
|
||||||
title: '重置密码',
|
|
||||||
trigger: false,
|
trigger: false,
|
||||||
width: 432,
|
modalProps: {
|
||||||
|
title: '重置密码',
|
||||||
|
width: 432,
|
||||||
|
},
|
||||||
model: {
|
model: {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
nickname: undefined,
|
nickname: undefined,
|
||||||
|
|
@ -39,12 +40,11 @@ const usernameRender: TableColumnRender = ({ record }) => (
|
||||||
<div>
|
<div>
|
||||||
<span class="cursor-pointer ">{record.nickname}</span>
|
<span class="cursor-pointer ">{record.nickname}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const { component: UserTable } = useTable({
|
const UserTable = useTable({
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
title: '用户昵称',
|
title: '用户昵称',
|
||||||
|
|
@ -53,7 +53,7 @@ const { component: UserTable } = useTable({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '创建',
|
title: '创建',
|
||||||
render: () => '3 天前'
|
render: () => '3 天前',
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// ...useCreateColumn(),
|
// ...useCreateColumn(),
|
||||||
|
|
@ -65,6 +65,7 @@ const { component: UserTable } = useTable({
|
||||||
title: '操作',
|
title: '操作',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
width: 200,
|
width: 200,
|
||||||
|
align: 'right',
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: '重置密码',
|
text: '重置密码',
|
||||||
|
|
@ -84,8 +85,8 @@ const { component: UserTable } = useTable({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
source: model => {
|
data: model => {
|
||||||
return api.user.getUsers(model);
|
return [];
|
||||||
},
|
},
|
||||||
search: [
|
search: [
|
||||||
{
|
{
|
||||||
|
|
@ -95,9 +96,13 @@ const { component: UserTable } = useTable({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
create: {
|
create: {
|
||||||
title: '新建用户',
|
modalProps: {
|
||||||
width: 820,
|
title: '新建用户',
|
||||||
formClass: '!grid grid-cols-2 gap-x-6',
|
width: 820,
|
||||||
|
},
|
||||||
|
formProps: {
|
||||||
|
class: '!grid grid-cols-2 gap-x-6',
|
||||||
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
field: 'avatar',
|
field: 'avatar',
|
||||||
|
|
@ -152,7 +157,6 @@ const { component: UserTable } = useTable({
|
||||||
},
|
},
|
||||||
modify: {
|
modify: {
|
||||||
extend: true,
|
extend: true,
|
||||||
title: '修改用户',
|
|
||||||
submit: model => {
|
submit: model => {
|
||||||
return api.user.setUser(model.id, model as any);
|
return api.user.setUser(model.id, model as any);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import NProgress from 'nprogress';
|
||||||
|
import 'nprogress/nprogress.css';
|
||||||
|
import './nprogress.css';
|
||||||
|
import { App } from 'vue';
|
||||||
|
|
||||||
|
declare module 'nprogress' {
|
||||||
|
interface NProgress {
|
||||||
|
install: (app: App) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作为VUE插件进行初始化
|
||||||
|
*/
|
||||||
|
NProgress.install = function (app: App) {
|
||||||
|
NProgress.configure({
|
||||||
|
showSpinner: false,
|
||||||
|
trickleSpeed: 200,
|
||||||
|
minimum: 0.3,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NProgress };
|
||||||
|
|
@ -51,8 +51,8 @@ const transformRoutes = (routes: RouteRecordRaw[]) => {
|
||||||
|
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
if (route.name === APP_ROUTE_NAME) {
|
if (route.name === APP_ROUTE_NAME) {
|
||||||
app = route;
|
|
||||||
route.children = appRoutes;
|
route.children = appRoutes;
|
||||||
|
app = route;
|
||||||
}
|
}
|
||||||
if ((route.name as string)?.startsWith(TOP_ROUTE_PREF)) {
|
if ((route.name as string)?.startsWith(TOP_ROUTE_PREF)) {
|
||||||
route.path = route.path.replace(TOP_ROUTE_PREF, '');
|
route.path = route.path.replace(TOP_ROUTE_PREF, '');
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,40 @@
|
||||||
import { env } from "@/config/env";
|
import { env } from '@/config/env';
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
export interface AppStore {
|
||||||
|
/**
|
||||||
|
* 站点标题
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* 站点副标题
|
||||||
|
*/
|
||||||
|
subtitle: string;
|
||||||
|
/**
|
||||||
|
* 图标地址
|
||||||
|
*/
|
||||||
|
logoUrl: string;
|
||||||
|
/**
|
||||||
|
* 是否为暗模式
|
||||||
|
*/
|
||||||
|
isDarkMode: boolean;
|
||||||
|
/**
|
||||||
|
* 页面是否加载中,用于路由首次加载
|
||||||
|
*/
|
||||||
|
pageLoding: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const useAppStore = defineStore({
|
export const useAppStore = defineStore({
|
||||||
id: "app",
|
id: 'app',
|
||||||
state: (): AppStore => ({
|
state: (): AppStore => {
|
||||||
isDarkMode: false,
|
return {
|
||||||
title: env.title,
|
isDarkMode: false,
|
||||||
logo: "/favicon.ico",
|
title: env.title,
|
||||||
subtitle: env.subtitle,
|
logoUrl: '/favicon.ico',
|
||||||
pageLoding: false,
|
subtitle: env.subtitle,
|
||||||
pageTags: [],
|
pageLoding: false,
|
||||||
}),
|
};
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
/**
|
/**
|
||||||
* 切换暗/亮模式
|
* 切换暗/亮模式
|
||||||
|
|
@ -23,8 +47,8 @@ export const useAppStore = defineStore({
|
||||||
* 切换为亮模式
|
* 切换为亮模式
|
||||||
*/
|
*/
|
||||||
setLight() {
|
setLight() {
|
||||||
document.body.setAttribute("arco-theme", "light");
|
document.body.setAttribute('arco-theme', 'light');
|
||||||
document.body.classList.remove("dark");
|
document.body.classList.remove('dark');
|
||||||
this.isDarkMode = false;
|
this.isDarkMode = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -32,8 +56,8 @@ export const useAppStore = defineStore({
|
||||||
* 切换为暗模式
|
* 切换为暗模式
|
||||||
*/
|
*/
|
||||||
setDark() {
|
setDark() {
|
||||||
document.body.setAttribute("arco-theme", "dark");
|
document.body.setAttribute('arco-theme', 'dark');
|
||||||
document.body.classList.add("dark");
|
document.body.classList.add('dark');
|
||||||
this.isDarkMode = true;
|
this.isDarkMode = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -43,66 +67,5 @@ export const useAppStore = defineStore({
|
||||||
setPageLoading(loading: boolean) {
|
setPageLoading(loading: boolean) {
|
||||||
this.pageLoding = loading;
|
this.pageLoding = loading;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加页面标签
|
|
||||||
* @param tag 标签
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
addPageTag(tag: PageTag) {
|
|
||||||
if (this.pageTags.some((i) => i.id === tag.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.pageTags.push({
|
|
||||||
closable: true,
|
|
||||||
closible: false,
|
|
||||||
actived: false,
|
|
||||||
...tag,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除页面标签
|
|
||||||
* @param tag 标签
|
|
||||||
*/
|
|
||||||
delPageTag(tag: PageTag) {
|
|
||||||
const index = this.pageTags.findIndex((i) => i.id === tag.id);
|
|
||||||
if (index > -1) {
|
|
||||||
this.pageTags.splice(index, 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface AppStore {
|
|
||||||
logo: string;
|
|
||||||
/**
|
|
||||||
* 是否为暗模式
|
|
||||||
*/
|
|
||||||
isDarkMode: boolean;
|
|
||||||
/**
|
|
||||||
* 站点标题
|
|
||||||
*/
|
|
||||||
title: string;
|
|
||||||
/**
|
|
||||||
* 站点副标题
|
|
||||||
*/
|
|
||||||
subtitle: string;
|
|
||||||
/**
|
|
||||||
* 页面是否加载中,用于路由首次加载
|
|
||||||
*/
|
|
||||||
pageLoding: boolean;
|
|
||||||
/**
|
|
||||||
* 标签数组
|
|
||||||
*/
|
|
||||||
pageTags: PageTag[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PageTag {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
path: string;
|
|
||||||
closable?: boolean;
|
|
||||||
closible?: boolean;
|
|
||||||
actived?: boolean;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -63,13 +63,14 @@ body {
|
||||||
&.arco-menu-vertical .arco-menu-item {
|
&.arco-menu-vertical .arco-menu-item {
|
||||||
line-height: 36px;
|
line-height: 36px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
|
color: rgba(0, 29, 59, .6);
|
||||||
}
|
}
|
||||||
&.arco-menu-vertical .arco-menu-group-title {
|
&.arco-menu-vertical .arco-menu-group-title {
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
[class^="icon-"] {
|
[class^="icon-"] {
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
vertical-align: -2px;
|
vertical-align: -2px;
|
||||||
}
|
}
|
||||||
.arco-menu-item {
|
.arco-menu-item {
|
||||||
|
|
@ -78,10 +79,12 @@ body {
|
||||||
background-color: var(--color-neutral-2);
|
background-color: var(--color-neutral-2);
|
||||||
}
|
}
|
||||||
&.arco-menu-selected {
|
&.arco-menu-selected {
|
||||||
// color: @arcoblue-6;
|
// color: #333;
|
||||||
// background-color: rgb(var(--primary-1));
|
color: rgb(var(--primary-6));
|
||||||
color: #fff;
|
// background-color: rgb(var(--primary-2));
|
||||||
background-color: rgb(var(--primary-6));
|
background-color: var(--color-neutral-2);
|
||||||
|
// color: #fff;
|
||||||
|
// background-color: rgb(var(--primary-6));
|
||||||
.arco-menu-icon {
|
.arco-menu-icon {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
@ -162,9 +165,6 @@ body {
|
||||||
|
|
||||||
|
|
||||||
.col-form {
|
.col-form {
|
||||||
.arco-form-item-wrapper-col {
|
|
||||||
// flex-direction: row;
|
|
||||||
}
|
|
||||||
.arco-form-item-content-wrapper {
|
.arco-form-item-content-wrapper {
|
||||||
width: 450px;
|
width: 450px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import "uno.css";
|
import 'arconify/es/style.css';
|
||||||
import { Plugin } from "vue";
|
import 'uno.css';
|
||||||
import "./css-arco.less";
|
import { Plugin } from 'vue';
|
||||||
import "./css-base.less";
|
import './css-arco.less';
|
||||||
import "./css-transition.less";
|
import './css-base.less';
|
||||||
import "./css-unocss.less";
|
import './css-transition.less';
|
||||||
|
import './css-unocss.less';
|
||||||
|
|
||||||
export const style: Plugin = {
|
export const style: Plugin = {
|
||||||
install(app) {},
|
install(app) {},
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,11 @@ declare module 'vue' {
|
||||||
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
|
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
|
||||||
AModal: typeof import('@arco-design/web-vue')['Modal']
|
AModal: typeof import('@arco-design/web-vue')['Modal']
|
||||||
AnAudio: typeof import('./../components/AnViewer/AnAudio.vue')['default']
|
AnAudio: typeof import('./../components/AnViewer/AnAudio.vue')['default']
|
||||||
AnEmpty: typeof import('./../components/AnEmpty/AnEmpty.vue')['default']
|
AnBreadcrumb: typeof import('./../components/AnBreadcrumb.vue')['default']
|
||||||
AnForbidden: typeof import('./../components/AnForbidden/AnForbidden.vue')['default']
|
AnEmpty: typeof import('./../components/AnEmpty.vue')['default']
|
||||||
AnPage: typeof import('./../components/AnPage/AnPage.vue')['default']
|
AnForbidden: typeof import('./../components/AnForbidden.vue')['default']
|
||||||
|
AnPage: typeof import('./../components/AnPage.vue')['default']
|
||||||
|
AnRoute: typeof import('./../components/AnRoute.vue')['default']
|
||||||
AnToast: typeof import('./../components/AnToast/AnToast.vue')['default']
|
AnToast: typeof import('./../components/AnToast/AnToast.vue')['default']
|
||||||
AnViewer: typeof import('./../components/AnViewer/AnViewer.vue')['default']
|
AnViewer: typeof import('./../components/AnViewer/AnViewer.vue')['default']
|
||||||
APagination: typeof import('@arco-design/web-vue')['Pagination']
|
APagination: typeof import('@arco-design/web-vue')['Pagination']
|
||||||
|
|
@ -59,8 +61,6 @@ declare module 'vue' {
|
||||||
ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
|
ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
|
||||||
AUpload: typeof import('@arco-design/web-vue')['Upload']
|
AUpload: typeof import('@arco-design/web-vue')['Upload']
|
||||||
BaseOption: typeof import('./../components/AnEditor/components/BaseOption.vue')['default']
|
BaseOption: typeof import('./../components/AnEditor/components/BaseOption.vue')['default']
|
||||||
BreadCrumb: typeof import('./../components/AnBreadcrumb/bread-crumb.vue')['default']
|
|
||||||
BreadPage: typeof import('./../components/AnBreadcrumb/bread-page.vue')['default']
|
|
||||||
ColorPicker: typeof import('./../components/AnEditor/components/ColorPicker.vue')['default']
|
ColorPicker: typeof import('./../components/AnEditor/components/ColorPicker.vue')['default']
|
||||||
ContextMenu: typeof import('./../components/AnEditor/components/ContextMenu.vue')['default']
|
ContextMenu: typeof import('./../components/AnEditor/components/ContextMenu.vue')['default']
|
||||||
ContextMenuList: typeof import('./../components/AnEditor/components/ContextMenuList.vue')['default']
|
ContextMenuList: typeof import('./../components/AnEditor/components/ContextMenuList.vue')['default']
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { defineConfig, presetIcons, presetUno } from 'unocss';
|
||||||
|
import { arcoToUnoColor } from './scripts/vite/color';
|
||||||
|
import iconFile from './scripts/vite/file.json';
|
||||||
|
import iconFmt from './scripts/vite/fmt.json';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提供CSS和图标的按需生成
|
||||||
|
* @see https://github.com/unocss/unocss#readme
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
theme: {
|
||||||
|
colors: {
|
||||||
|
brand: arcoToUnoColor('primary'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
presets: [
|
||||||
|
presetUno(),
|
||||||
|
presetIcons({
|
||||||
|
prefix: '',
|
||||||
|
collections: {
|
||||||
|
'icon-file': iconFile,
|
||||||
|
'icon-fmt': iconFmt,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
content: {
|
||||||
|
pipeline: {
|
||||||
|
include: ['src/**/*.{vue,ts,tsx,css,scss,sass,less,styl}'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -2,7 +2,6 @@ import Vue from '@vitejs/plugin-vue';
|
||||||
import VueJsx from '@vitejs/plugin-vue-jsx';
|
import VueJsx from '@vitejs/plugin-vue-jsx';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { visualizer } from 'rollup-plugin-visualizer';
|
import { visualizer } from 'rollup-plugin-visualizer';
|
||||||
import { presetIcons, presetUno } from 'unocss';
|
|
||||||
import Unocss from 'unocss/vite';
|
import Unocss from 'unocss/vite';
|
||||||
import AutoImport from 'unplugin-auto-import/vite';
|
import AutoImport from 'unplugin-auto-import/vite';
|
||||||
import { ArcoResolver } from 'unplugin-vue-components/resolvers';
|
import { ArcoResolver } from 'unplugin-vue-components/resolvers';
|
||||||
|
|
@ -10,11 +9,9 @@ import AutoComponent from 'unplugin-vue-components/vite';
|
||||||
import router from 'unplugin-vue-router/vite';
|
import router from 'unplugin-vue-router/vite';
|
||||||
import { defineConfig, loadEnv } from 'vite';
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
import Page from 'vite-plugin-pages';
|
import Page from 'vite-plugin-pages';
|
||||||
import { arcoToUnoColor } from './scripts/vite/color';
|
|
||||||
import iconFile from './scripts/vite/file.json';
|
|
||||||
import iconFmt from './scripts/vite/fmt.json';
|
|
||||||
import extension from './scripts/vite/plugin-extension';
|
import extension from './scripts/vite/plugin-extension';
|
||||||
import info from './scripts/vite/plugin-info';
|
import info from './scripts/vite/plugin-info';
|
||||||
|
import { onRoutesGenerated } from './scripts/vite/plugin-pages';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* vite 配置
|
* vite 配置
|
||||||
|
|
@ -81,52 +78,14 @@ export default defineConfig(({ mode }) => {
|
||||||
exclude: ['**/components/*', '**/*.*.*', '**/!(index).*'],
|
exclude: ['**/components/*', '**/*.*.*', '**/!(index).*'],
|
||||||
importMode: 'sync',
|
importMode: 'sync',
|
||||||
extensions: ['vue'],
|
extensions: ['vue'],
|
||||||
onRoutesGenerated(routes) {
|
onRoutesGenerated: routes => onRoutesGenerated(routes, mode),
|
||||||
const isProd = mode !== 'development';
|
|
||||||
const result = [];
|
|
||||||
for (const route of routes) {
|
|
||||||
const { hide } = route.meta ?? {};
|
|
||||||
if (!route.meta) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (hide === true) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (isProd && hide === 'prod') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
result.push(route);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提供CSS和图标的按需生成
|
* 提供CSS和图标的按需生成
|
||||||
* @see https://github.com/unocss/unocss#readme
|
* @see https://github.com/unocss/unocss#readme
|
||||||
*/
|
*/
|
||||||
Unocss({
|
Unocss(),
|
||||||
theme: {
|
|
||||||
colors: {
|
|
||||||
brand: arcoToUnoColor('primary'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
presets: [
|
|
||||||
presetUno(),
|
|
||||||
presetIcons({
|
|
||||||
prefix: '',
|
|
||||||
collections: {
|
|
||||||
'icon-file': iconFile,
|
|
||||||
'icon-fmt': iconFmt,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
content: {
|
|
||||||
pipeline: {
|
|
||||||
include: ['src/**/*.{vue,ts,tsx,css,scss,sass,less,styl}'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提供产物分析报告
|
* 提供产物分析报告
|
||||||
|
|
@ -134,7 +93,7 @@ export default defineConfig(({ mode }) => {
|
||||||
*/
|
*/
|
||||||
visualizer({
|
visualizer({
|
||||||
title: `构建统计 | ${env.VITE_SUBTITLE}`,
|
title: `构建统计 | ${env.VITE_SUBTITLE}`,
|
||||||
filename: '.gitea/stat.html',
|
filename: 'dist/stat.html',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue