Compare commits
No commits in common. "3f0c83a83b02db7b4eeacd13736c17189ce85a82" and "2a27f67b850ab0a8c9e4151a589b3f795ca1413e" have entirely different histories.
3f0c83a83b
...
2a27f67b85
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 的信息作为路由信息,其他文件会跳过,可以包含子文件夹作为嵌套路由
|
||||
- 在 src/pages 的文件夹层级,作为菜单层级,路由层级最终只有 2 层(配合 keep-alive 缓存)
|
||||
- 在 src/pages 目录下,以 \_ 开头的文件夹作为 1 级路由,其他作为 2 级路由,也就是应用路由
|
||||
- 在 src/pages 目录下,以 _ 开头的文件夹作为 1 级路由,其他作为 2 级路由,也就是应用路由
|
||||
- 子文件夹下,只有 index.vue 文件有效,其他文件会忽略,这些文件可以作为子组件使用
|
||||
- components 目录会被忽视。
|
||||
- xxx.xx.xx 文件会被忽视,例如 index.my.vue 文件。
|
||||
|
|
@ -76,7 +76,7 @@ pnpm dev
|
|||
|
||||
目前支持的参数,如下:
|
||||
|
||||
````ts
|
||||
```ts
|
||||
interface RouteMeta {
|
||||
/**
|
||||
* 页面标题
|
||||
|
|
@ -140,7 +140,7 @@ interface RouteMeta {
|
|||
*/
|
||||
link?: string;
|
||||
}
|
||||
````
|
||||
```
|
||||
|
||||
### 嵌套布局
|
||||
|
||||
|
|
@ -174,6 +174,8 @@ interface RouteMeta {
|
|||
|
||||
用户登陆后获取的权限,应存储在 userStore.auth 字段中,在路由的 beforeEach 守卫中,会比较两个是否匹配,匹配上则继续,否则会显示如下 403 页面:
|
||||
|
||||

|
||||
|
||||
### 动态路由
|
||||
|
||||
相比于比较流行的加法挂载,我更倾向于减法挂载。即默认加载完所有路由,在 beforeEach 钩子根据权限移除不必要的路由。
|
||||
|
|
@ -188,11 +190,20 @@ interface RouteMeta {
|
|||
|
||||
```html
|
||||
<script>
|
||||
defineOptions({
|
||||
name: 'MyPage',
|
||||
});
|
||||
defineOptions({
|
||||
name: "MyPage"
|
||||
})
|
||||
</script>
|
||||
<route> { "meta": { // 组件名字 "name": "MyPage", // 开启缓存 "cache": true } } </route>
|
||||
<route>
|
||||
{
|
||||
"meta": {
|
||||
// 组件名字
|
||||
"name": "MyPage",
|
||||
// 开启缓存
|
||||
"cache": true
|
||||
}
|
||||
}
|
||||
</route>
|
||||
```
|
||||
|
||||
### 条件加载
|
||||
|
|
@ -207,7 +218,7 @@ VITE_EXTENSION = my
|
|||
|
||||
### 图标样式
|
||||
|
||||
基于 [UnoCSS]() 插件,可使用类似 TailwindCSS 的原子样式快速开发,同时默认安装 icon-park-outline 图标库,只需引用类名即可得到 SVG 图标。这在路由菜单等需要动态渲染时非常有用,同时所有样式类和图标类都是按需打包的,示例:
|
||||
基于 [UnoCSS]() 插件,可使用类似 TailwindCSS 的原子样式快速开发,同时默认安装icon-park-outline 图标库,只需引用类名即可得到 SVG 图标。这在路由菜单等需要动态渲染时非常有用,同时所有样式类和图标类都是按需打包的,示例:
|
||||
|
||||
```html
|
||||
<i class="text-sm icon-park-outline-home" />
|
||||
|
|
@ -251,13 +262,13 @@ enum MediaEnum {
|
|||
const media = defineConstants([
|
||||
{
|
||||
value: MediaEnum.VIDEO,
|
||||
label: '视频',
|
||||
color: 'red',
|
||||
label: "视频",
|
||||
color: "red",
|
||||
},
|
||||
{
|
||||
value: MediaEnum.IMAGE,
|
||||
label: '图片',
|
||||
color: 'blue',
|
||||
label: "图片",
|
||||
color: "blue",
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
@ -277,7 +288,7 @@ media.val(); // [1, 2]
|
|||
<table ref="tableRef" v-bind="table" />
|
||||
</template>
|
||||
<script>
|
||||
import { Table, useTable } from '@/components';
|
||||
import { Table, useTable } from "@/components";
|
||||
|
||||
const table = useTable({
|
||||
// 数据源配置,可以是数组或返回对象的异步函数
|
||||
|
|
@ -288,8 +299,8 @@ media.val(); // [1, 2]
|
|||
// 表格列配置
|
||||
columns: [
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'name',
|
||||
title: "用户名",
|
||||
dataIndex: "name",
|
||||
},
|
||||
],
|
||||
|
||||
|
|
@ -302,21 +313,21 @@ media.val(); // [1, 2]
|
|||
search: {
|
||||
items: [
|
||||
{
|
||||
field: 'username',
|
||||
label: '用户名',
|
||||
type: 'input',
|
||||
field: "username",
|
||||
label: "用户名",
|
||||
type: "input",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// 新增表单弹窗的配置,类型为useFormModal的入参
|
||||
create: {
|
||||
title: '新增用户',
|
||||
title: "新增用户",
|
||||
items: [
|
||||
{
|
||||
field: 'username',
|
||||
label: '用户名',
|
||||
type: 'input',
|
||||
field: "username",
|
||||
label: "用户名",
|
||||
type: "input",
|
||||
},
|
||||
],
|
||||
submit: async ({ model }) => {
|
||||
|
|
@ -326,12 +337,12 @@ media.val(); // [1, 2]
|
|||
|
||||
// 修改表单弹窗的配置,类型为useFormModal的入参
|
||||
modify: {
|
||||
title: '修改用户',
|
||||
title: "修改用户",
|
||||
items: [
|
||||
{
|
||||
field: 'username',
|
||||
label: '用户名',
|
||||
type: 'input',
|
||||
field: "username",
|
||||
label: "用户名",
|
||||
type: "input",
|
||||
},
|
||||
],
|
||||
submit: async ({ model }) => {
|
||||
|
|
@ -341,7 +352,6 @@ media.val(); // [1, 2]
|
|||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
提示:以上每个参数都有类型提示,原组件每个参数都可透传,封装遵循扩展而非限制原则。
|
||||
|
||||
### 自动导入
|
||||
|
|
|
|||
77
index.html
77
index.html
|
|
@ -9,16 +9,13 @@
|
|||
</head>
|
||||
<body>
|
||||
<div id="app" class="dark:bg-slate-900 dark:text-slate-200">
|
||||
<div class="loading">
|
||||
<img
|
||||
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHN0eWxlPSJtYXJnaW46IGF1dG87IGJhY2tncm91bmQ6IHJnYigyNTUsIDI1NSwgMjU1KTsgZGlzcGxheTogYmxvY2s7IHNoYXBlLXJlbmRlcmluZzogYXV0bzsiIHdpZHRoPSIyMDBweCIgaGVpZ2h0PSIyMDBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1MCA1MCkiPjxnPjxhbmltYXRlVHJhbnNmb3JtIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIgdHlwZT0icm90YXRlIiB2YWx1ZXM9IjA7NDUiIGtleVRpbWVzPSIwOzEiIGR1cj0iMC4ycyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiPjwvYW5pbWF0ZVRyYW5zZm9ybT48cGF0aCBkPSJNMjkuNDkxNTI0MjA2MTE3MjU1IC01LjUgTDM3LjQ5MTUyNDIwNjExNzI1NSAtNS41IEwzNy40OTE1MjQyMDYxMTcyNTUgNS41IEwyOS40OTE1MjQyMDYxMTcyNTUgNS41IEEzMCAzMCAwIDAgMSAyNC43NDI3NDQwNTAxOTg3MzggMTYuOTY0NTY5NDU3MTQ2NzEyIEwyNC43NDI3NDQwNTAxOTg3MzggMTYuOTY0NTY5NDU3MTQ2NzEyIEwzMC4zOTk1OTgyOTk2OTExMTcgMjIuNjIxNDIzNzA2NjM5MDkyIEwyMi42MjE0MjM3MDY2MzkwOTYgMzAuMzk5NTk4Mjk5NjkxMTE0IEwxNi45NjQ1Njk0NTcxNDY3MTYgMjQuNzQyNzQ0MDUwMTk4NzM0IEEzMCAzMCAwIDAgMSA1LjUgMjkuNDkxNTI0MjA2MTE3MjU1IEw1LjUgMjkuNDkxNTI0MjA2MTE3MjU1IEw1LjUgMzcuNDkxNTI0MjA2MTE3MjU1IEwtNS40OTk5OTk5OTk5OTk5OTcgMzcuNDkxNTI0MjA2MTE3MjU1IEwtNS40OTk5OTk5OTk5OTk5OTcgMjkuNDkxNTI0MjA2MTE3MjU1IEEzMCAzMCAwIDAgMSAtMTYuOTY0NTY5NDU3MTQ2NzA1IDI0Ljc0Mjc0NDA1MDE5ODczOCBMLTE2Ljk2NDU2OTQ1NzE0NjcwNSAyNC43NDI3NDQwNTAxOTg3MzggTC0yMi42MjE0MjM3MDY2MzkwODUgMzAuMzk5NTk4Mjk5NjkxMTE3IEwtMzAuMzk5NTk4Mjk5NjkxMTE3IDIyLjYyMTQyMzcwNjYzOTA5MiBMLTI0Ljc0Mjc0NDA1MDE5ODczOCAxNi45NjQ1Njk0NTcxNDY3MTIgQTMwIDMwIDAgMCAxIC0yOS40OTE1MjQyMDYxMTcyNTUgNS41MDAwMDAwMDAwMDAwMDkgTC0yOS40OTE1MjQyMDYxMTcyNTUgNS41MDAwMDAwMDAwMDAwMDkgTC0zNy40OTE1MjQyMDYxMTcyNTUgNS41MDAwMDAwMDAwMDAwMSBMLTM3LjQ5MTUyNDIwNjExNzI1NSAtNS41MDAwMDAwMDAwMDAwMDEgTC0yOS40OTE1MjQyMDYxMTcyNTUgLTUuNTAwMDAwMDAwMDAwMDAyIEEzMCAzMCAwIDAgMSAtMjQuNzQyNzQ0MDUwMTk4NzM4IC0xNi45NjQ1Njk0NTcxNDY3MDUgTC0yNC43NDI3NDQwNTAxOTg3MzggLTE2Ljk2NDU2OTQ1NzE0NjcwNSBMLTMwLjM5OTU5ODI5OTY5MTExNyAtMjIuNjIxNDIzNzA2NjM5MDg1IEwtMjIuNjIxNDIzNzA2NjM5MDkyIC0zMC4zOTk1OTgyOTk2OTExMTcgTC0xNi45NjQ1Njk0NTcxNDY3MTIgLTI0Ljc0Mjc0NDA1MDE5ODczOCBBMzAgMzAgMCAwIDEgLTUuNTAwMDAwMDAwMDAwMDExIC0yOS40OTE1MjQyMDYxMTcyNTUgTC01LjUwMDAwMDAwMDAwMDAxMSAtMjkuNDkxNTI0MjA2MTE3MjU1IEwtNS41MDAwMDAwMDAwMDAwMTIgLTM3LjQ5MTUyNDIwNjExNzI1NSBMNS40OTk5OTk5OTk5OTk5OTggLTM3LjQ5MTUyNDIwNjExNzI1NSBMNS41IC0yOS40OTE1MjQyMDYxMTcyNTUgQTMwIDMwIDAgMCAxIDE2Ljk2NDU2OTQ1NzE0NjcwMiAtMjQuNzQyNzQ0MDUwMTk4NzQgTDE2Ljk2NDU2OTQ1NzE0NjcwMiAtMjQuNzQyNzQ0MDUwMTk4NzQgTDIyLjYyMTQyMzcwNjYzOTA4IC0zMC4zOTk1OTgyOTk2OTExMiBMMzAuMzk5NTk4Mjk5NjkxMTE3IC0yMi42MjE0MjM3MDY2MzkxIEwyNC43NDI3NDQwNTAxOTg3MzggLTE2Ljk2NDU2OTQ1NzE0NjcxNiBBMzAgMzAgMCAwIDEgMjkuNDkxNTI0MjA2MTE3MjU1IC01LjUwMDAwMDAwMDAwMDAxMyBNMCAtMjBBMjAgMjAgMCAxIDAgMCAyMCBBMjAgMjAgMCAxIDAgMCAtMjAiIGZpbGw9IiMwOWYiPjwvcGF0aD48L2c+PC9nPjwvc3ZnPgo="
|
||||
alt="loading"
|
||||
class="loading-image"
|
||||
width="64"
|
||||
height="64"
|
||||
/>
|
||||
<h1 class="loading-title">欢迎访问%VITE_TITLE%</h1>
|
||||
<div class="loading-tip">资源加载中, 请稍等...</div>
|
||||
<div class="cube">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<style>
|
||||
html,
|
||||
|
|
@ -39,27 +36,51 @@
|
|||
justify-content: center;
|
||||
user-select: none;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@keyframes cube {
|
||||
0% {
|
||||
transform: rotate(45deg) rotateX(-25deg) rotateY(25deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(45deg) rotateX(-385deg) rotateY(25deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(45deg) rotateX(-385deg) rotateY(385deg);
|
||||
}
|
||||
}
|
||||
.loading-image {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
.cube {
|
||||
animation: cube 2s infinite ease;
|
||||
height: 40px;
|
||||
transform-style: preserve-3d;
|
||||
width: 40px;
|
||||
}
|
||||
.loading-title {
|
||||
margin: 0;
|
||||
margin-top: 20px;
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
.cube div {
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
border: 2px solid #000;
|
||||
}
|
||||
.loading-tip {
|
||||
margin-top: 12px;
|
||||
line-height: 1;
|
||||
color: #889;
|
||||
.cube div:nth-of-type(1) {
|
||||
transform: translateZ(-20px) rotateY(180deg);
|
||||
}
|
||||
.cube div:nth-of-type(2) {
|
||||
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>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@
|
|||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"@vueuse/core": "^10.7.1",
|
||||
"arconify": "^0.0.2",
|
||||
"axios": "^1.6.5",
|
||||
"dayjs": "^1.11.10",
|
||||
"dplayer": "^1.27.1",
|
||||
|
|
|
|||
|
|
@ -46,10 +46,10 @@ const getBuildInfo = async () => {
|
|||
const version = commits ? `${latestTag}.${commits}` : `v${pkg.version}`;
|
||||
const content = `欢迎访问!版本: ${version} 标识: ${hash} 构建: ${time}`;
|
||||
const style = `"color: #09f; font-weight: 900;", "font-size: 12px; color: #09f; font-family: ''"`;
|
||||
const vString = `\n var __APP_VERSION__ = '${version}';\n`;
|
||||
const hString = ` var __APP_HASH__ = '${hash}';\n`;
|
||||
const dString = ` var __APP_DATE__ = '${time}';\n`;
|
||||
const lString = ` console.log(\`%c${LOGO} \n%c${content}\n\`, ${style});\n`;
|
||||
const vString = `var __APP_VERSION__ = '${version}';\n`;
|
||||
const hString = `var __APP_HASH__ = '${hash}';\n`;
|
||||
const dString = `var __APP_DATE__ = '${time}';\n`;
|
||||
const lString = `console.log(\`%c${LOGO} \n%c${content}\n\`, ${style});\n`;
|
||||
return vString + hString + dString + lString;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
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,6 +12,7 @@ export function addAuthInterceptor(axios: AxiosInstance) {
|
|||
if (userStore.accessToken) {
|
||||
config.headers.Authorization = `Bearer ${userStore.accessToken}`;
|
||||
}
|
||||
// throw Error('dd');
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BreadCrumb from './AnBreadcrumb.vue';
|
||||
import BreadCrumb from './bread-crumb.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from "dayjs";
|
||||
import { dayjs } from "@/libs/dayjs";
|
||||
import { PropType } from "vue";
|
||||
import { FontRender } from "../font";
|
||||
import { Date } from "./interface";
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs';
|
||||
import { PropType, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { dayjs } from '@/libs/dayjs';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import { FontRender } from '../font';
|
||||
import { Time } from './interface';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,155 @@
|
|||
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;
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
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;
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
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'
|
||||
>;
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
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 };
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
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,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
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,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
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;
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
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';
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
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,
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
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,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
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,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
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,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
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,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
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: '请输入',
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
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,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
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: [],
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
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: {},
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
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,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
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,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
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,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
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,
|
||||
};
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
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 });
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
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>;
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
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,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
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;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
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,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
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() {}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
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';
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
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();
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
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!] ?? '-';
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
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);
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
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!} />;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
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>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
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>
|
||||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
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([]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
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,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
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 };
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
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 { api } from '@/api';
|
||||
import { dayjs } from '@/plugins/dayjs';
|
||||
import { NProgress } from '@/plugins/nprogress';
|
||||
import { router } from '@/router';
|
||||
import { store } from '@/store';
|
||||
import { style } from '@/styles';
|
||||
import { createApp } from 'vue';
|
||||
import { dayjs } from '@/libs/dayjs';
|
||||
import { NProgress } from '@/libs/nprogress';
|
||||
import { api } from '@/api';
|
||||
|
||||
const run = async () => {
|
||||
const app = createApp(App);
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
import Image404 from './404.svg?raw';
|
||||
import Image404 from './image-404.svg?raw';
|
||||
|
||||
defineOptions({ name: 'AllUncatchedPage' });
|
||||
|
||||
|
|
|
|||
|
|
@ -11,16 +11,14 @@
|
|||
<password-modal></password-modal>
|
||||
</span>
|
||||
<template #content>
|
||||
<!-- <a-doption class="bg-transparent!">
|
||||
<a-doption class="bg-transparent!">
|
||||
<div class="w-[200px] flex items-center gap-2">
|
||||
<a-avatar :size="32">
|
||||
<img :src="userStore.avatar || 'https://github.com/juetan.png'" :alt="userStore.nickname" />
|
||||
</a-avatar>
|
||||
<div class="leading-4 text-base my-2">
|
||||
<div class="flex items-center gap-2">
|
||||
{{ userStore.nickname }}
|
||||
<a-tag color="red" size="small">管理员</a-tag>
|
||||
</div>
|
||||
{{ userStore.nickname }}
|
||||
<a-tag color="red" size="small" >管理员</a-tag>
|
||||
<div class="text-xs text-gray-400">
|
||||
<span class="text-gray-400">@{{ userStore.username }}</span>
|
||||
</div>
|
||||
|
|
@ -40,6 +38,7 @@
|
|||
</template>
|
||||
账号信息
|
||||
</a-doption>
|
||||
<!-- <a-divider :margin="4" class="border-gray-100!"></a-divider> -->
|
||||
<a-doption @click="router.push('/user')">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-config"></i>
|
||||
|
|
@ -52,7 +51,7 @@
|
|||
</template>
|
||||
关于
|
||||
</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">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-power"></i>
|
||||
|
|
@ -64,10 +63,10 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFormModal } from '@/components/AnForm';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { delConfirm, sleep } from '@/utils';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useFormModal } from 'arconify';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const route = useRoute();
|
||||
|
|
@ -86,12 +85,10 @@ const logout = async () => {
|
|||
});
|
||||
};
|
||||
|
||||
const PasswordModal = useFormModal({
|
||||
const { component: PasswordModal, open } = useFormModal({
|
||||
title: '修改密码',
|
||||
trigger: false,
|
||||
modalProps: {
|
||||
title: '修改密码',
|
||||
width: 500,
|
||||
},
|
||||
width: 500,
|
||||
items: [
|
||||
{
|
||||
field: 'password',
|
||||
|
|
|
|||
|
|
@ -2,12 +2,18 @@
|
|||
<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">
|
||||
<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">
|
||||
<img :src="appStore.logoUrl" alt="" width="24" height="24" class="" />
|
||||
<h1 class="relative text-[20px] leading-[22px] dark:text-white m-0 p-0 font-normal">
|
||||
<img src="/favicon.ico" alt="" width="24" height="24" class="" />
|
||||
<h1 class="relative text-[18px] leading-[22px] dark:text-white m-0 p-0 font-normal">
|
||||
{{ appStore.title }}
|
||||
<span class="absolute -right-10 -top-1 font-normal text-xs text-gray-400"> v0.0.1 </span>
|
||||
</h1>
|
||||
<!-- <span class="text-gray-400">{{ appStore.subtitle }}</span> -->
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
|
|
@ -50,6 +56,9 @@
|
|||
<a-layout class="layout-content flex-1">
|
||||
<a-layout-content class="overflow-x-auto">
|
||||
<a-spin :loading="appStore.pageLoding" class="block h-full w-full">
|
||||
<template #icon>
|
||||
<div class="loader"></div>
|
||||
</template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="menuStore.caches">
|
||||
<component v-if="hasAuth" :is="Component"></component>
|
||||
|
|
@ -66,11 +75,11 @@
|
|||
<script lang="tsx" setup>
|
||||
import { useAppStore } from '@/store/app';
|
||||
import { useMenuStore } from '@/store/menu';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useFullscreen } from '@vueuse/core';
|
||||
import Menu from './Menu.vue';
|
||||
import userDropdown from './UserDropdown.vue';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
defineOptions({ name: 'LayoutPage' });
|
||||
|
||||
|
|
|
|||
|
|
@ -12,14 +12,18 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center w-full overflow-hidden">
|
||||
<div 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-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="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-gray-500 mt-2.5">{{ meridiem }}好,欢迎访问 {{ appStore.title }} 系统!</div>
|
||||
<a-form ref="formRef" :model="model" :rules="formRules" layout="vertical" class="mt-6">
|
||||
<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>
|
||||
<i class="icon-park-outline-user" />
|
||||
</template>
|
||||
|
|
@ -52,10 +56,10 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { api } from '@/api';
|
||||
import { dayjs } from '@/libs/dayjs';
|
||||
import { useAppStore } from '@/store/app';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { FieldRule, Form, Message, Modal, Notification } from '@arco-design/web-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { reactive } from 'vue';
|
||||
|
||||
defineOptions({ name: 'LoginPage' });
|
||||
|
|
@ -73,7 +77,7 @@ const formRules: Record<string, FieldRule[]> = {
|
|||
username: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入账号',
|
||||
message: '请输入账号/手机号/邮箱',
|
||||
},
|
||||
],
|
||||
password: [
|
||||
|
|
@ -97,8 +101,8 @@ const onSubmitForm = async () => {
|
|||
if (await formRef.value?.validate()) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await api.auth.login(model);
|
||||
userStore.setAccessToken(res.data.data);
|
||||
Notification.success({
|
||||
|
|
@ -109,8 +113,9 @@ const onSubmitForm = async () => {
|
|||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message;
|
||||
message && Message.warning(`提示:${message}`);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||
import { listToTree } from '@/utils/listToTree';
|
||||
import { useTable } from 'arconify';
|
||||
|
||||
const CategoryTable = useTable({
|
||||
const { component: CategoryTable } = useTable({
|
||||
columns: [
|
||||
{
|
||||
title: '分类名称',
|
||||
|
|
@ -18,12 +18,14 @@ const CategoryTable = useTable({
|
|||
<div class="flex flex-col overflow-hidden">
|
||||
<span>
|
||||
{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>
|
||||
<div class="text-gray-400 text-xs truncate mt-0.5">{record.description}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
useCreateColumn(),
|
||||
useUpdateColumn(),
|
||||
{
|
||||
type: 'button',
|
||||
title: '操作',
|
||||
|
|
@ -46,10 +48,10 @@ const CategoryTable = useTable({
|
|||
],
|
||||
},
|
||||
],
|
||||
data: async model => {
|
||||
source: async model => {
|
||||
const res = await api.category.getCategories(model);
|
||||
const data = listToTree(res.data.data ?? []);
|
||||
return { data, total: (res.data as any).total };
|
||||
return { data: { data, total: (res.data as any).total } };
|
||||
},
|
||||
search: [
|
||||
{
|
||||
|
|
@ -61,9 +63,8 @@ const CategoryTable = useTable({
|
|||
},
|
||||
],
|
||||
create: {
|
||||
modalProps: {
|
||||
width: 580,
|
||||
},
|
||||
title: '添加分类',
|
||||
width: 580,
|
||||
items: [
|
||||
{
|
||||
field: 'title',
|
||||
|
|
@ -77,8 +78,8 @@ const CategoryTable = useTable({
|
|||
setter: 'input',
|
||||
required: true,
|
||||
setterProps: {
|
||||
placeholder: '只包含字母、小数和连字符',
|
||||
},
|
||||
placeholder: '只包含字母、小数和连字符'
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
|
|
@ -93,6 +94,7 @@ const CategoryTable = useTable({
|
|||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
title: '修改分类',
|
||||
submit: model => {
|
||||
return api.category.setCategory(model.id, model as any);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
|
||||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { useTable } from 'arconify';
|
||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||
|
||||
const CategoryTable = useTable({
|
||||
const { component: CategoryTable } = useTable({
|
||||
columns: [
|
||||
{
|
||||
title: '文章标题',
|
||||
|
|
@ -20,6 +20,8 @@ const CategoryTable = useTable({
|
|||
</div>
|
||||
),
|
||||
},
|
||||
useCreateColumn(),
|
||||
useUpdateColumn(),
|
||||
{
|
||||
type: 'button',
|
||||
title: '操作',
|
||||
|
|
@ -37,10 +39,7 @@ const CategoryTable = useTable({
|
|||
],
|
||||
},
|
||||
],
|
||||
data: async model => {
|
||||
const res = await api.post.getPosts(model);
|
||||
return { data: [], total: 100 };
|
||||
},
|
||||
source: async model => api.post.getPosts(model),
|
||||
search: [
|
||||
{
|
||||
field: 'nickname',
|
||||
|
|
@ -51,10 +50,8 @@ const CategoryTable = useTable({
|
|||
},
|
||||
],
|
||||
create: {
|
||||
modalProps: {
|
||||
title: '添加文章',
|
||||
width: 1080,
|
||||
},
|
||||
title: '添加文章',
|
||||
width: 1080,
|
||||
items: [
|
||||
{
|
||||
field: 'title',
|
||||
|
|
@ -84,6 +81,7 @@ const CategoryTable = useTable({
|
|||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
title: '修改文章',
|
||||
submit: model => {
|
||||
return api.post.updatePost(model.id, model);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
<template>
|
||||
<AnPage>
|
||||
<bread-page>
|
||||
<CategoryTable />
|
||||
</AnPage>
|
||||
</bread-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||
import { listToTree } from '@/utils/listToTree';
|
||||
import { useTable } from 'arconify';
|
||||
|
||||
const CategoryTable = useTable({
|
||||
const { component: CategoryTable } = useTable({
|
||||
columns: [
|
||||
{
|
||||
title: '分类名称',
|
||||
|
|
@ -17,18 +17,19 @@ const CategoryTable = useTable({
|
|||
render: ({ record }) => (
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span>
|
||||
{record.name ?? '无'}
|
||||
{record.name}
|
||||
<span class="text-orange-500 truncate ml-2">@{record.code}</span>
|
||||
</span>
|
||||
<div class="text-gray-400 text-xs truncate mt-0.5">{record.description}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
useCreateColumn(),
|
||||
useUpdateColumn(),
|
||||
{
|
||||
type: 'button',
|
||||
title: '操作',
|
||||
width: 120,
|
||||
align: 'right',
|
||||
buttons: [
|
||||
{
|
||||
type: 'modify',
|
||||
|
|
@ -44,10 +45,10 @@ const CategoryTable = useTable({
|
|||
],
|
||||
},
|
||||
],
|
||||
data: async model => {
|
||||
source: async model => {
|
||||
const res = await api.fileCategory.getFileCategorys(model);
|
||||
const data = listToTree(res.data.data ?? []);
|
||||
return [];
|
||||
return { data: { data, total: (res.data as any).total } };
|
||||
},
|
||||
search: [
|
||||
{
|
||||
|
|
@ -59,10 +60,8 @@ const CategoryTable = useTable({
|
|||
},
|
||||
],
|
||||
create: {
|
||||
modalProps: {
|
||||
title: '添加分类',
|
||||
width: 580,
|
||||
},
|
||||
title: '添加分类',
|
||||
width: 580,
|
||||
items: [
|
||||
{
|
||||
field: 'name',
|
||||
|
|
@ -92,6 +91,7 @@ const CategoryTable = useTable({
|
|||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
title: '修改分类',
|
||||
submit: model => {
|
||||
return api.fileCategory.setFileCategory(model.id, model as any);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
|
||||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||
import { listToTree } from '@/utils/listToTree';
|
||||
import { useTable } from 'arconify';
|
||||
|
||||
const CategoryTable = useTable({
|
||||
const { component: CategoryTable } = useTable({
|
||||
columns: [
|
||||
{
|
||||
title: '分类名称',
|
||||
|
|
@ -16,13 +16,14 @@ const CategoryTable = useTable({
|
|||
<div class="flex flex-col overflow-hidden">
|
||||
<span>
|
||||
{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>
|
||||
<div class="text-gray-400 text-xs truncate mt-0.5">{record.description}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
useCreateColumn(),
|
||||
useUpdateColumn(),
|
||||
{
|
||||
type: 'button',
|
||||
title: '操作',
|
||||
|
|
@ -42,10 +43,10 @@ const CategoryTable = useTable({
|
|||
],
|
||||
},
|
||||
],
|
||||
data: async model => {
|
||||
source: async model => {
|
||||
const res = await api.fileCategory.getFileCategorys(model);
|
||||
const data = listToTree(res.data.data ?? []);
|
||||
return [];
|
||||
return { data: { data, total: (res.data as any).total } };
|
||||
},
|
||||
search: [
|
||||
{
|
||||
|
|
@ -57,10 +58,8 @@ const CategoryTable = useTable({
|
|||
},
|
||||
],
|
||||
create: {
|
||||
modalProps: {
|
||||
title: '添加分类',
|
||||
width: 580,
|
||||
},
|
||||
title: '添加分类',
|
||||
width: 580,
|
||||
items: [
|
||||
{
|
||||
field: 'name',
|
||||
|
|
@ -74,8 +73,8 @@ const CategoryTable = useTable({
|
|||
setter: 'input',
|
||||
required: true,
|
||||
setterProps: {
|
||||
placeholder: '只包含字母、小数和连字符',
|
||||
},
|
||||
placeholder: '只包含字母、小数和连字符'
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
|
|
@ -90,6 +89,7 @@ const CategoryTable = useTable({
|
|||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
title: '修改分类',
|
||||
submit: model => {
|
||||
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="flex gap-2">
|
||||
<a-input-search allow-clear placeholder="分类名称" class="mb-2" @search="updateFileCategories"></a-input-search>
|
||||
<a-button @click="() => CategoryModal.open()">
|
||||
<a-button @click="() => open()">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-add"></i>
|
||||
</template>
|
||||
|
|
@ -12,7 +12,10 @@
|
|||
<a-spin :loading="loading" class="w-full h-full">
|
||||
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
|
||||
<ul v-if="list.length" class="pl-0 mt-0">
|
||||
<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">
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
<span class="flex-1 truncate">全部</span>
|
||||
|
|
@ -37,7 +40,7 @@
|
|||
</template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption @click="CategoryModal.open(item)">
|
||||
<a-doption @click="open(item)">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-edit"></i>
|
||||
</template>
|
||||
|
|
@ -62,9 +65,9 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { FileCategory, api } from '@/api';
|
||||
import { useFormModal } from '@/components/AnForm';
|
||||
import { delConfirm } from '@/utils';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useFormModal } from 'arconify';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
defineProps({
|
||||
|
|
@ -98,11 +101,10 @@ const onDeleteRow = async (row: FileCategory) => {
|
|||
Message.success(res.data.message);
|
||||
};
|
||||
|
||||
const CategoryModal = useFormModal({
|
||||
const { component: CategoryModal, open } = useFormModal({
|
||||
title: model => (!model.id ? '新建分类' : '修改分类'),
|
||||
trigger: false,
|
||||
modalProps: {
|
||||
width: 580,
|
||||
},
|
||||
width: 580,
|
||||
items: [
|
||||
{
|
||||
field: 'name',
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="bg-white p-4">
|
||||
<MaterialTable>
|
||||
<template #action>
|
||||
<AnUpload @success="() => MaterialTable.tableRef.value?.refresh()"></AnUpload>
|
||||
<AnUpload @success="() => tableRef?.refresh()"></AnUpload>
|
||||
</template>
|
||||
</MaterialTable>
|
||||
<AnPreview v-model:visible="viewer.visible" :type="viewer.type" :url="viewer.url"></AnPreview>
|
||||
|
|
@ -26,9 +26,9 @@
|
|||
|
||||
<script setup lang="tsx">
|
||||
import { FileCategory, api } from '@/api';
|
||||
import { useTable, useTableDelete, useUpdateColumn } from '@/components/AnTable';
|
||||
import { FileTypes } from '@/constants/file';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useTable } from 'arconify';
|
||||
import numeral from 'numeral';
|
||||
import AnCategory from './AnCategory.vue';
|
||||
import AnPreview from './AnPreview.vue';
|
||||
|
|
@ -46,11 +46,11 @@ const preview = (record: any) => {
|
|||
};
|
||||
|
||||
const onCategoryChange = (category: FileCategory) => {
|
||||
if (MaterialTable.tableRef.value?.search?.model) {
|
||||
MaterialTable.tableRef.value.search.model.categoryId = category.id;
|
||||
if (props.search?.model) {
|
||||
props.search.model.categoryId = category.id;
|
||||
}
|
||||
current.value = category;
|
||||
MaterialTable.tableRef.value?.refresh();
|
||||
tableRef.value?.refresh();
|
||||
};
|
||||
|
||||
const copyLink = (record: Recordable) => {
|
||||
|
|
@ -58,7 +58,12 @@ const copyLink = (record: Recordable) => {
|
|||
Message.success(`已复制 ${record.name} 的地址!`);
|
||||
};
|
||||
|
||||
const MaterialTable = useTable({
|
||||
const {
|
||||
component: MaterialTable,
|
||||
tableRef,
|
||||
props,
|
||||
} = useTable({
|
||||
plugins: [useTableDelete()],
|
||||
columns: [
|
||||
{
|
||||
title: '文件名称',
|
||||
|
|
@ -77,7 +82,10 @@ const MaterialTable = useTable({
|
|||
</div>
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="truncate hover:text-brand-500 hover:decoration-underline underline-offset-2 cursor-pointer" onClick={() => preview(record)}>
|
||||
<span
|
||||
class="truncate hover:text-brand-500 hover:decoration-underline underline-offset-2 cursor-pointer"
|
||||
onClick={() => preview(record)}
|
||||
>
|
||||
{record.name}
|
||||
</span>
|
||||
{/* <span
|
||||
|
|
@ -108,11 +116,12 @@ const MaterialTable = useTable({
|
|||
width: 150,
|
||||
render: ({ record }) => numeral(record.size).format('0 b'),
|
||||
},
|
||||
// useCreateColumn(),
|
||||
useUpdateColumn(),
|
||||
{
|
||||
type: 'button',
|
||||
title: '操作',
|
||||
width: 160,
|
||||
align: 'right',
|
||||
buttons: [
|
||||
{
|
||||
text: '下载',
|
||||
|
|
@ -135,8 +144,8 @@ const MaterialTable = useTable({
|
|||
],
|
||||
},
|
||||
],
|
||||
data: async model => {
|
||||
return [];
|
||||
source: async model => {
|
||||
return api.file.getFiles(model);
|
||||
},
|
||||
search: {
|
||||
hideSearch: false,
|
||||
|
|
@ -168,10 +177,8 @@ const MaterialTable = useTable({
|
|||
],
|
||||
},
|
||||
modify: {
|
||||
modalProps: {
|
||||
title: '修改素材',
|
||||
width: 580,
|
||||
},
|
||||
title: '修改素材',
|
||||
width: 580,
|
||||
items: [
|
||||
{
|
||||
field: 'name',
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<AnPage>
|
||||
<BreadPage>
|
||||
<CategoryTable />
|
||||
</AnPage>
|
||||
</BreadPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { useTable } from 'arconify';
|
||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||
|
||||
const CategoryTable = useTable({
|
||||
const { component: CategoryTable } = useTable({
|
||||
columns: [
|
||||
{
|
||||
title: '文章标题',
|
||||
|
|
@ -20,6 +20,8 @@ const CategoryTable = useTable({
|
|||
</div>
|
||||
),
|
||||
},
|
||||
useCreateColumn(),
|
||||
useUpdateColumn(),
|
||||
{
|
||||
type: 'button',
|
||||
title: '操作',
|
||||
|
|
@ -37,7 +39,7 @@ const CategoryTable = useTable({
|
|||
],
|
||||
},
|
||||
],
|
||||
data: async model => [],
|
||||
source: async model => api.post.getPosts(model),
|
||||
search: [
|
||||
{
|
||||
field: 'nickname',
|
||||
|
|
@ -48,10 +50,8 @@ const CategoryTable = useTable({
|
|||
},
|
||||
],
|
||||
create: {
|
||||
modalProps: {
|
||||
title: '添加文章',
|
||||
width: 1080,
|
||||
},
|
||||
title: '添加文章',
|
||||
width: 1080,
|
||||
items: [
|
||||
{
|
||||
field: 'title',
|
||||
|
|
@ -81,6 +81,7 @@ const CategoryTable = useTable({
|
|||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
title: '修改文章',
|
||||
submit: model => {
|
||||
return api.post.updatePost(model.id, model);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@
|
|||
<i class="icon-park-outline-delete text-xs"></i>
|
||||
</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">
|
||||
<div
|
||||
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>
|
||||
添加服务1
|
||||
</div>
|
||||
|
|
@ -57,7 +59,9 @@
|
|||
<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">
|
||||
<a-tag>{{ i }}</a-tag>
|
||||
<span class="flex-1 truncate hover:underline underline-offset-2 hover:text-brand-500 cursor-pointer"> 但是预测已加载的数据不足以 </span>
|
||||
<span class="flex-1 truncate hover:underline underline-offset-2 hover:text-brand-500 cursor-pointer">
|
||||
但是预测已加载的数据不足以
|
||||
</span>
|
||||
<span class="text-gray-400">3天前</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -67,7 +71,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { Editor } from '@/components/AnEditor';
|
||||
import { useTable } from '@/components/AnTable';
|
||||
import { TableColumnData } from '@arco-design/web-vue';
|
||||
import { useTable } from 'arconify';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
defineOptions({ name: 'SystemLoglPage' });
|
||||
|
|
@ -35,11 +35,9 @@ const useTwoRowsColumn = (tkey: string, bkey: string): TableColumnData['render']
|
|||
};
|
||||
};
|
||||
|
||||
const LoginLogTable = useTable({
|
||||
data: async model => {
|
||||
const res = await api.log.getLoginLogs(model);
|
||||
const { data, total = 10 } = res.data as any;
|
||||
return { data, total };
|
||||
const { component: LoginLogTable } = useTable({
|
||||
source: async model => {
|
||||
return api.log.getLoginLogs(model);
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
|
|
@ -50,7 +48,9 @@ const LoginLogTable = useTable({
|
|||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class={
|
||||
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'
|
||||
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'
|
||||
}
|
||||
></span>
|
||||
<div>
|
||||
|
|
@ -65,6 +65,7 @@ const LoginLogTable = useTable({
|
|||
title: '登陆地址',
|
||||
dataIndex: 'ip',
|
||||
width: 200,
|
||||
render: useTwoRowsColumn('addr', 'ip'),
|
||||
},
|
||||
{
|
||||
title: '操作系统',
|
||||
|
|
|
|||
|
|
@ -1,13 +1,55 @@
|
|||
<template>
|
||||
<BreadPage>
|
||||
|
||||
<OperationTable></OperationTable>
|
||||
</BreadPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { useTable } from '@/components/AnTable';
|
||||
import { Image } from '@arco-design/web-vue';
|
||||
|
||||
const data: any = []
|
||||
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>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<AnPage>
|
||||
<bread-page>
|
||||
<div>
|
||||
<h2 class="m-0 text-base">常规设置</h2>
|
||||
<p class="text-gray-500 mt-1">首次为你的帐户添加密码时,你需要前往密码重置页面,以便我们验证你的身份。</p>
|
||||
</div>
|
||||
<a-form :model="{}" label-align="left" class="space-y-6 mt-6 col-form divide-y divide-gray-100">
|
||||
<a-form :model="{}" label-align="left" class="space-y-6 mt-6 col-form divide-y">
|
||||
<a-form-item label="站点LOGO">
|
||||
<a-avatar :size="64">
|
||||
<img :src="appStore.logoUrl" alt="" />
|
||||
<img :src="appStore.logo" alt="" />
|
||||
<template #trigger-icon>
|
||||
<i class="icon-park-outline-edit"></i>
|
||||
</template>
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
<a-button type="primary">保存修改</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</AnPage>
|
||||
</bread-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<AnPage>
|
||||
<bread-page>
|
||||
<!-- <div>
|
||||
<div class="bg-white">
|
||||
<div v-for="t1 in types" :key="t1.label" class="flex items-center">
|
||||
|
|
@ -21,7 +21,6 @@
|
|||
</div>
|
||||
</div> -->
|
||||
<div class="grid">
|
||||
<a-radio></a-radio>
|
||||
<div class="mb-3">功能列表</div>
|
||||
<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">
|
||||
|
|
@ -41,7 +40,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AnPage>
|
||||
</bread-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<AnPage>
|
||||
<bread-page>
|
||||
<div class="flex">
|
||||
<div class="w-full">
|
||||
<div class="flex item-center justify-between gap-4">
|
||||
|
|
@ -74,7 +74,7 @@
|
|||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</AnPage>
|
||||
</bread-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
|||
|
|
@ -7,16 +7,15 @@
|
|||
|
||||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { useFormModal, useTable } from 'arconify';
|
||||
import { useFormModal } from '@/components/AnForm';
|
||||
import { TableColumnRender, useCreateColumn, useTable } from '@/components/AnTable';
|
||||
|
||||
defineOptions({ name: 'SystemDepartmentPage' });
|
||||
|
||||
const PasswordModal = useFormModal({
|
||||
const { component: PasswordModal, open } = useFormModal({
|
||||
title: '重置密码',
|
||||
trigger: false,
|
||||
modalProps: {
|
||||
title: '重置密码',
|
||||
width: 432,
|
||||
},
|
||||
width: 432,
|
||||
model: {
|
||||
id: undefined,
|
||||
nickname: undefined,
|
||||
|
|
@ -31,7 +30,7 @@ const PasswordModal = useFormModal({
|
|||
submit: model => api.user.setUser(model.id, model as any),
|
||||
});
|
||||
|
||||
const usernameRender = ({ record }) => (
|
||||
const usernameRender: TableColumnRender = ({ record }) => (
|
||||
<div class="flex items-center gap-4 w-full overflow-hidden">
|
||||
<a-avatar size={32} class="!bg-brand-500">
|
||||
{record.avatar?.startsWith('/') ? <img src={record.avatar} alt="" /> : record.nickname?.[0]}
|
||||
|
|
@ -55,13 +54,16 @@ const usernameRender = ({ record }) => (
|
|||
</div>
|
||||
);
|
||||
|
||||
const UserTable = useTable({
|
||||
const { component: UserTable } = useTable({
|
||||
columns: [
|
||||
{
|
||||
title: '用户昵称',
|
||||
dataIndex: 'username',
|
||||
render: usernameRender,
|
||||
},
|
||||
{
|
||||
...useCreateColumn(),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
type: 'button',
|
||||
|
|
@ -86,10 +88,8 @@ const UserTable = useTable({
|
|||
],
|
||||
},
|
||||
],
|
||||
data: async model => {
|
||||
const res = await api.user.getUsers(model);
|
||||
const { data, total } = res.data as any;
|
||||
return { data, total };
|
||||
source: model => {
|
||||
return api.user.getUsers(model);
|
||||
},
|
||||
search: [
|
||||
{
|
||||
|
|
@ -99,13 +99,9 @@ const UserTable = useTable({
|
|||
},
|
||||
],
|
||||
create: {
|
||||
modalProps: {
|
||||
title: '新建用户',
|
||||
width: 820,
|
||||
},
|
||||
formProps: {
|
||||
class: '!grid grid-cols-2 gap-x-6',
|
||||
},
|
||||
title: '新建用户',
|
||||
width: 820,
|
||||
formClass: '!grid grid-cols-2 gap-x-6',
|
||||
items: [
|
||||
{
|
||||
field: 'avatar',
|
||||
|
|
@ -160,7 +156,7 @@ const UserTable = useTable({
|
|||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
|
||||
title: '修改用户',
|
||||
submit: model => {
|
||||
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="flex gap-2">
|
||||
<a-input-search allow-clear placeholder="字典类型" class="mb-2"></a-input-search>
|
||||
<a-button @click="DictTypeModal.open()">
|
||||
<a-button @click="open()">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-add"></i>
|
||||
</template>
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
</template>
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption @click="DictTypeModal.open(item)">
|
||||
<a-doption @click="open(item)">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-edit"></i>
|
||||
</template>
|
||||
|
|
@ -53,9 +53,9 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { DictType, api } from '@/api';
|
||||
import { useFormModal } from '@/components/AnForm';
|
||||
import { delConfirm } from '@/utils';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useFormModal } from 'arconify';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
defineProps({
|
||||
|
|
@ -81,12 +81,10 @@ const onDeleteRow = async (row: DictType) => {
|
|||
Message.success(res.data.message);
|
||||
};
|
||||
|
||||
const DictTypeModal = useFormModal({
|
||||
const { component: DictTypeModal, open } = useFormModal({
|
||||
title: ({ model }) => (!model.id ? '新建字典类型' : '修改字典类型'),
|
||||
trigger: false,
|
||||
modalProps: {
|
||||
title: '字典类型',
|
||||
width: 580,
|
||||
},
|
||||
width: 580,
|
||||
items: [
|
||||
{
|
||||
field: 'name',
|
||||
|
|
|
|||
|
|
@ -27,17 +27,17 @@
|
|||
|
||||
<script setup lang="tsx">
|
||||
import { DictType, api } from '@/api';
|
||||
import { useTable } from 'arconify';
|
||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||
import AnGroup from './Group.vue';
|
||||
|
||||
defineOptions({ name: 'SystemDictPage' });
|
||||
const current = ref<DictType>();
|
||||
const onTypeChange = (item: DictType) => {
|
||||
current.value = item;
|
||||
DictTable.tableRef.value?.refresh();
|
||||
tableRef.value?.refresh();
|
||||
};
|
||||
|
||||
const DictTable = useTable({
|
||||
const { component: DictTable, tableRef } = useTable({
|
||||
columns: [
|
||||
{
|
||||
title: '字典项',
|
||||
|
|
@ -52,6 +52,8 @@ const DictTable = useTable({
|
|||
</div>
|
||||
),
|
||||
},
|
||||
useCreateColumn(),
|
||||
useUpdateColumn(),
|
||||
{
|
||||
title: '操作',
|
||||
type: 'button',
|
||||
|
|
@ -71,8 +73,8 @@ const DictTable = useTable({
|
|||
],
|
||||
},
|
||||
],
|
||||
data: search => {
|
||||
return [];
|
||||
source: search => {
|
||||
return api.dict.getDicts({ ...search, typeId: current.value?.id } as any);
|
||||
},
|
||||
search: {
|
||||
hideSearch: true,
|
||||
|
|
@ -87,10 +89,8 @@ const DictTable = useTable({
|
|||
],
|
||||
},
|
||||
create: {
|
||||
modalProps: {
|
||||
title: '新增字典',
|
||||
width: 580,
|
||||
},
|
||||
title: '新增字典',
|
||||
width: 580,
|
||||
items: [
|
||||
{
|
||||
field: 'name',
|
||||
|
|
@ -117,7 +117,7 @@ const DictTable = useTable({
|
|||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
|
||||
title: '修改字典',
|
||||
submit: model => {
|
||||
const data = { ...model, typeId: current.value?.id } as any;
|
||||
return api.dict.setDict(model.id, data);
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||
import { MenuType, MenuTypes } from '@/constants/menu';
|
||||
import { flatMenus } from '@/router';
|
||||
import { listToTree } from '@/utils/listToTree';
|
||||
import { useTable } from 'arconify';
|
||||
|
||||
defineOptions({ name: 'SystemMenuPage' });
|
||||
|
||||
|
|
@ -17,10 +17,10 @@ const menuArr = flatMenus.map(i => ({ label: i.title, value: i.id }));
|
|||
const expanded = ref(false);
|
||||
const toggleExpand = () => {
|
||||
expanded.value = !expanded.value;
|
||||
MenuTable.tableRef.value?.tableRef?.expandAll(expanded.value);
|
||||
tableRef.value?.tableRef?.expandAll(expanded.value);
|
||||
};
|
||||
|
||||
const MenuTable = useTable({
|
||||
const { component: MenuTable, tableRef } = useTable({
|
||||
columns: [
|
||||
{
|
||||
title: () => (
|
||||
|
|
@ -66,6 +66,8 @@ const MenuTable = useTable({
|
|||
</a-tag>
|
||||
),
|
||||
},
|
||||
useCreateColumn(),
|
||||
useUpdateColumn(),
|
||||
{
|
||||
title: '操作',
|
||||
type: 'button',
|
||||
|
|
@ -92,7 +94,7 @@ const MenuTable = useTable({
|
|||
],
|
||||
},
|
||||
],
|
||||
data: search => [],
|
||||
source: search => api.menu.getMenus({ ...search, tree: true, size: 0 }),
|
||||
search: [
|
||||
{
|
||||
field: 'name',
|
||||
|
|
@ -102,13 +104,9 @@ const MenuTable = useTable({
|
|||
},
|
||||
],
|
||||
create: {
|
||||
modalProps: {
|
||||
title: '新建菜单',
|
||||
width: 980,
|
||||
},
|
||||
formProps: {
|
||||
class: '!grid grid-cols-2 gap-x-4',
|
||||
},
|
||||
title: '新建菜单',
|
||||
width: 980,
|
||||
formClass: '!grid grid-cols-2 gap-x-4',
|
||||
items: [
|
||||
{
|
||||
field: 'parentId',
|
||||
|
|
@ -207,7 +205,7 @@ const MenuTable = useTable({
|
|||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
|
||||
title: '修改菜单',
|
||||
submit: model => {
|
||||
return api.menu.setMenu(model.id, model);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@
|
|||
|
||||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { useTable } from 'arconify';
|
||||
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
|
||||
|
||||
defineOptions({ name: 'SystemRolePage' });
|
||||
|
||||
const RoleTable = useTable({
|
||||
data: () => {
|
||||
return [];
|
||||
const { component: RoleTable } = useTable({
|
||||
source: () => {
|
||||
return api.role.getRoles();
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
|
|
@ -28,6 +28,8 @@ const RoleTable = useTable({
|
|||
</div>
|
||||
),
|
||||
},
|
||||
useCreateColumn(),
|
||||
useUpdateColumn(),
|
||||
{
|
||||
title: '操作',
|
||||
type: 'button',
|
||||
|
|
@ -61,10 +63,8 @@ const RoleTable = useTable({
|
|||
},
|
||||
],
|
||||
create: {
|
||||
modalProps: {
|
||||
title: '新建角色',
|
||||
width: 580,
|
||||
},
|
||||
title: '新建角色',
|
||||
width: 580,
|
||||
items: [
|
||||
{
|
||||
field: 'name',
|
||||
|
|
@ -90,6 +90,7 @@ const RoleTable = useTable({
|
|||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
title: '修改角色',
|
||||
submit: model => {
|
||||
return api.role.updateRole(model.id, model);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,16 +7,15 @@
|
|||
|
||||
<script setup lang="tsx">
|
||||
import { api } from '@/api';
|
||||
import { TableColumnRender, useFormModal, useTable } from 'arconify';
|
||||
import { useFormModal } from '@/components/AnForm';
|
||||
import { TableColumnRender, useTable } from '@/components/AnTable';
|
||||
|
||||
defineOptions({ name: 'SystemUserPage' });
|
||||
|
||||
const PasswordModal = useFormModal({
|
||||
const { component: PasswordModal, open } = useFormModal({
|
||||
title: '重置密码',
|
||||
trigger: false,
|
||||
modalProps: {
|
||||
title: '重置密码',
|
||||
width: 432,
|
||||
},
|
||||
width: 432,
|
||||
model: {
|
||||
id: undefined,
|
||||
nickname: undefined,
|
||||
|
|
@ -40,11 +39,12 @@ const usernameRender: TableColumnRender = ({ record }) => (
|
|||
<div>
|
||||
<span class="cursor-pointer ">{record.nickname}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const UserTable = useTable({
|
||||
const { component: UserTable } = useTable({
|
||||
columns: [
|
||||
{
|
||||
title: '用户昵称',
|
||||
|
|
@ -53,7 +53,7 @@ const UserTable = useTable({
|
|||
},
|
||||
{
|
||||
title: '创建',
|
||||
render: () => '3 天前',
|
||||
render: () => '3 天前'
|
||||
},
|
||||
// {
|
||||
// ...useCreateColumn(),
|
||||
|
|
@ -65,7 +65,6 @@ const UserTable = useTable({
|
|||
title: '操作',
|
||||
type: 'button',
|
||||
width: 200,
|
||||
align: 'right',
|
||||
buttons: [
|
||||
{
|
||||
text: '重置密码',
|
||||
|
|
@ -85,8 +84,8 @@ const UserTable = useTable({
|
|||
],
|
||||
},
|
||||
],
|
||||
data: model => {
|
||||
return [];
|
||||
source: model => {
|
||||
return api.user.getUsers(model);
|
||||
},
|
||||
search: [
|
||||
{
|
||||
|
|
@ -96,13 +95,9 @@ const UserTable = useTable({
|
|||
},
|
||||
],
|
||||
create: {
|
||||
modalProps: {
|
||||
title: '新建用户',
|
||||
width: 820,
|
||||
},
|
||||
formProps: {
|
||||
class: '!grid grid-cols-2 gap-x-6',
|
||||
},
|
||||
title: '新建用户',
|
||||
width: 820,
|
||||
formClass: '!grid grid-cols-2 gap-x-6',
|
||||
items: [
|
||||
{
|
||||
field: 'avatar',
|
||||
|
|
@ -157,6 +152,7 @@ const UserTable = useTable({
|
|||
},
|
||||
modify: {
|
||||
extend: true,
|
||||
title: '修改用户',
|
||||
submit: model => {
|
||||
return api.user.setUser(model.id, model as any);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
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) {
|
||||
if (route.name === APP_ROUTE_NAME) {
|
||||
route.children = appRoutes;
|
||||
app = route;
|
||||
route.children = appRoutes;
|
||||
}
|
||||
if ((route.name as string)?.startsWith(TOP_ROUTE_PREF)) {
|
||||
route.path = route.path.replace(TOP_ROUTE_PREF, '');
|
||||
|
|
|
|||
|
|
@ -1,40 +1,16 @@
|
|||
import { env } from '@/config/env';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export interface AppStore {
|
||||
/**
|
||||
* 站点标题
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* 站点副标题
|
||||
*/
|
||||
subtitle: string;
|
||||
/**
|
||||
* 图标地址
|
||||
*/
|
||||
logoUrl: string;
|
||||
/**
|
||||
* 是否为暗模式
|
||||
*/
|
||||
isDarkMode: boolean;
|
||||
/**
|
||||
* 页面是否加载中,用于路由首次加载
|
||||
*/
|
||||
pageLoding: boolean;
|
||||
}
|
||||
import { env } from "@/config/env";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useAppStore = defineStore({
|
||||
id: 'app',
|
||||
state: (): AppStore => {
|
||||
return {
|
||||
isDarkMode: false,
|
||||
title: env.title,
|
||||
logoUrl: '/favicon.ico',
|
||||
subtitle: env.subtitle,
|
||||
pageLoding: false,
|
||||
};
|
||||
},
|
||||
id: "app",
|
||||
state: (): AppStore => ({
|
||||
isDarkMode: false,
|
||||
title: env.title,
|
||||
logo: "/favicon.ico",
|
||||
subtitle: env.subtitle,
|
||||
pageLoding: false,
|
||||
pageTags: [],
|
||||
}),
|
||||
actions: {
|
||||
/**
|
||||
* 切换暗/亮模式
|
||||
|
|
@ -47,8 +23,8 @@ export const useAppStore = defineStore({
|
|||
* 切换为亮模式
|
||||
*/
|
||||
setLight() {
|
||||
document.body.setAttribute('arco-theme', 'light');
|
||||
document.body.classList.remove('dark');
|
||||
document.body.setAttribute("arco-theme", "light");
|
||||
document.body.classList.remove("dark");
|
||||
this.isDarkMode = false;
|
||||
},
|
||||
|
||||
|
|
@ -56,8 +32,8 @@ export const useAppStore = defineStore({
|
|||
* 切换为暗模式
|
||||
*/
|
||||
setDark() {
|
||||
document.body.setAttribute('arco-theme', 'dark');
|
||||
document.body.classList.add('dark');
|
||||
document.body.setAttribute("arco-theme", "dark");
|
||||
document.body.classList.add("dark");
|
||||
this.isDarkMode = true;
|
||||
},
|
||||
|
||||
|
|
@ -67,5 +43,66 @@ export const useAppStore = defineStore({
|
|||
setPageLoading(loading: boolean) {
|
||||
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,14 +63,13 @@ body {
|
|||
&.arco-menu-vertical .arco-menu-item {
|
||||
line-height: 36px;
|
||||
margin-top: 4px;
|
||||
color: rgba(0, 29, 59, .6);
|
||||
}
|
||||
&.arco-menu-vertical .arco-menu-group-title {
|
||||
line-height: 28px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
[class^="icon-"] {
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
.arco-menu-item {
|
||||
|
|
@ -79,12 +78,10 @@ body {
|
|||
background-color: var(--color-neutral-2);
|
||||
}
|
||||
&.arco-menu-selected {
|
||||
// color: #333;
|
||||
color: rgb(var(--primary-6));
|
||||
// background-color: rgb(var(--primary-2));
|
||||
background-color: var(--color-neutral-2);
|
||||
// color: #fff;
|
||||
// background-color: rgb(var(--primary-6));
|
||||
// color: @arcoblue-6;
|
||||
// background-color: rgb(var(--primary-1));
|
||||
color: #fff;
|
||||
background-color: rgb(var(--primary-6));
|
||||
.arco-menu-icon {
|
||||
color: inherit;
|
||||
}
|
||||
|
|
@ -165,6 +162,9 @@ body {
|
|||
|
||||
|
||||
.col-form {
|
||||
.arco-form-item-wrapper-col {
|
||||
// flex-direction: row;
|
||||
}
|
||||
.arco-form-item-content-wrapper {
|
||||
width: 450px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import 'arconify/es/style.css';
|
||||
import 'uno.css';
|
||||
import { Plugin } from 'vue';
|
||||
import './css-arco.less';
|
||||
import './css-base.less';
|
||||
import './css-transition.less';
|
||||
import './css-unocss.less';
|
||||
import "uno.css";
|
||||
import { Plugin } from "vue";
|
||||
import "./css-arco.less";
|
||||
import "./css-base.less";
|
||||
import "./css-transition.less";
|
||||
import "./css-unocss.less";
|
||||
|
||||
export const style: Plugin = {
|
||||
install(app) {},
|
||||
|
|
|
|||
|
|
@ -37,11 +37,9 @@ declare module 'vue' {
|
|||
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
|
||||
AModal: typeof import('@arco-design/web-vue')['Modal']
|
||||
AnAudio: typeof import('./../components/AnViewer/AnAudio.vue')['default']
|
||||
AnBreadcrumb: typeof import('./../components/AnBreadcrumb.vue')['default']
|
||||
AnEmpty: typeof import('./../components/AnEmpty.vue')['default']
|
||||
AnForbidden: typeof import('./../components/AnForbidden.vue')['default']
|
||||
AnPage: typeof import('./../components/AnPage.vue')['default']
|
||||
AnRoute: typeof import('./../components/AnRoute.vue')['default']
|
||||
AnEmpty: typeof import('./../components/AnEmpty/AnEmpty.vue')['default']
|
||||
AnForbidden: typeof import('./../components/AnForbidden/AnForbidden.vue')['default']
|
||||
AnPage: typeof import('./../components/AnPage/AnPage.vue')['default']
|
||||
AnToast: typeof import('./../components/AnToast/AnToast.vue')['default']
|
||||
AnViewer: typeof import('./../components/AnViewer/AnViewer.vue')['default']
|
||||
APagination: typeof import('@arco-design/web-vue')['Pagination']
|
||||
|
|
@ -61,6 +59,8 @@ declare module 'vue' {
|
|||
ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
|
||||
AUpload: typeof import('@arco-design/web-vue')['Upload']
|
||||
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']
|
||||
ContextMenu: typeof import('./../components/AnEditor/components/ContextMenu.vue')['default']
|
||||
ContextMenuList: typeof import('./../components/AnEditor/components/ContextMenuList.vue')['default']
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
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,6 +2,7 @@ import Vue from '@vitejs/plugin-vue';
|
|||
import VueJsx from '@vitejs/plugin-vue-jsx';
|
||||
import { resolve } from 'path';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import { presetIcons, presetUno } from 'unocss';
|
||||
import Unocss from 'unocss/vite';
|
||||
import AutoImport from 'unplugin-auto-import/vite';
|
||||
import { ArcoResolver } from 'unplugin-vue-components/resolvers';
|
||||
|
|
@ -9,9 +10,11 @@ import AutoComponent from 'unplugin-vue-components/vite';
|
|||
import router from 'unplugin-vue-router/vite';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
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 info from './scripts/vite/plugin-info';
|
||||
import { onRoutesGenerated } from './scripts/vite/plugin-pages';
|
||||
|
||||
/**
|
||||
* vite 配置
|
||||
|
|
@ -78,14 +81,52 @@ export default defineConfig(({ mode }) => {
|
|||
exclude: ['**/components/*', '**/*.*.*', '**/!(index).*'],
|
||||
importMode: 'sync',
|
||||
extensions: ['vue'],
|
||||
onRoutesGenerated: routes => onRoutesGenerated(routes, mode),
|
||||
onRoutesGenerated(routes) {
|
||||
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和图标的按需生成
|
||||
* @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}'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
/**
|
||||
* 提供产物分析报告
|
||||
|
|
@ -93,7 +134,7 @@ export default defineConfig(({ mode }) => {
|
|||
*/
|
||||
visualizer({
|
||||
title: `构建统计 | ${env.VITE_SUBTITLE}`,
|
||||
filename: 'dist/stat.html',
|
||||
filename: '.gitea/stat.html',
|
||||
}),
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue