feat: 优化目录

master
luoer 2023-11-22 14:53:57 +08:00
parent c489f3b0cf
commit 8968692073
56 changed files with 546 additions and 1301 deletions

4
.env
View File

@ -10,7 +10,7 @@ VITE_BASE = /
# 接口前缀:参见 axios 的 baseURL
VITE_API = https://appnify.app.juetan.cn/
# 首页路径
VITE_HOME_PATH = /home/home
VITE_HOME_PATH = /home
# 路由模式web(路径) hash(锚点)
VITE_HISTORY = web
@ -24,7 +24,7 @@ VITE_PORT = 3020
# 代理前缀
VITE_PROXY_PREFIX = /api,/upload
# 代理地址
VITE_PROXY = http://127.0.0.1:3030/
VITE_PROXY = https://appnify.app.juetan.cn/
# API文档 说明:需返回符合 OPENAPI 规范的json内容
VITE_OPENAPI = http://127.0.0.1:3030/openapi.json
# 文件后缀 说明设为dev时会优先加载index.dev.vue文件否则回退至index.vue文件

File diff suppressed because one or more lines are too long

View File

@ -2,31 +2,31 @@
<a-config-provider>
<router-view v-slot="{ Component, route }">
<keep-alive :include="menuStore.cacheTopNames">
<component v-if="hasAuth(route, Component)" :is="Component"></component>
<page-403 v-else></page-403>
<component v-if="hasAuth(route)" :is="Component"></component>
<AnForbidden v-else></AnForbidden>
</keep-alive>
</router-view>
</a-config-provider>
</template>
<script setup lang="ts">
import { RouteLocationNormalizedLoaded } from "vue-router";
import { useUserStore } from "./store";
import { useMenuStore } from "./store/menu";
import { RouteLocationNormalizedLoaded } from 'vue-router';
import { useUserStore } from './store';
import { useMenuStore } from './store/menu';
const userStore = useUserStore();
const menuStore = useMenuStore();
const hasAuth = (route: RouteLocationNormalizedLoaded, c: any) => {
const hasAuth = (route: RouteLocationNormalizedLoaded) => {
const aAuth = route.meta.auth;
const uAuth = userStore.auth;
if (!aAuth?.length) {
return true;
}
if (aAuth.some((i) => i === "*")) {
if (aAuth.some(i => i === '*')) {
return true;
}
if (uAuth.some((i) => aAuth.some((j) => j === i))) {
if (uAuth.some(i => aAuth.some(j => j === i))) {
return true;
}
return false;

View File

@ -1,3 +1,3 @@
export * from "./instance/api";
export * from "./generated/Api";
export * from "./instance/useRequest";
export * from './instance/api';
export * from './generated/Api';
export * from './instance/useRequest';

View File

@ -1,8 +1,8 @@
import { Service } from "./service";
import { addToastInterceptor } from "../interceptors/toast";
import { addAuthInterceptor } from "../interceptors/auth";
import { addExceptionInterceptor } from "../interceptors/exception";
import { env } from "@/config/env";
import { Service } from './service';
import { addToastInterceptor } from '../interceptors/toast';
import { addAuthInterceptor } from '../interceptors/auth';
import { addExceptionInterceptor } from '../interceptors/exception';
import { env } from '@/config/env';
/**
* API

View File

@ -1,4 +1,4 @@
import { Api } from "../generated/Api";
import { Api } from '../generated/Api';
/**
* API

View File

@ -1,6 +1,6 @@
import { Notification } from "@arco-design/web-vue";
import { AxiosInstance } from "axios";
import { has, isString } from "lodash-es";
import { Notification } from '@arco-design/web-vue';
import { AxiosInstance } from 'axios';
import { has, isString } from 'lodash-es';
const successCodes = [2000];
const expiredCodes = [4050, 4051];
@ -15,33 +15,32 @@ let logoutTipShowing = false;
* @param axios Axios
*/
export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (...args: any[]) => any) {
axios.interceptors.request.use(null, (error) => {
axios.interceptors.request.use(null, error => {
const msg = error.response?.data?.message;
Notification.error({
title: "请求提示",
title: '请求提示',
content: msg ?? `发送请求失败,请检查参数或稍后重试!`,
});
return Promise.reject(error);
});
axios.interceptors.response.use(
(res) => {
res => {
const code = res.data?.code;
if (code && !successCodes.includes(code)) {
return Promise.reject(res);
}
return res;
},
(error) => {
// 服务端响应错误
error => {
if (error.response) {
const code = error.response.data?.code;
if (expiredCodes.includes(code)) {
if (!logoutTipShowing) {
logoutTipShowing = true;
Notification.warning({
title: "登陆提示",
content: "当前登陆已过期,请重新登陆!",
title: '登陆提示',
content: '当前登陆已过期,请重新登陆!',
onClose: () => (logoutTipShowing = false),
});
exipreHandler?.(error);
@ -50,10 +49,10 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (.
}
const resMsg = error.response?.data?.message;
let message: string | null = resMsg ?? resMessageTip;
if (error.config?.method === "get") {
if (error.config?.method === 'get') {
message = resGetMessage;
}
if (has(error.config, "resErrorTip")) {
if (has(error.config, 'resErrorTip')) {
const tip = error.config.resErrorTip;
if (tip) {
message = isString(tip) ? tip : message;
@ -63,18 +62,15 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (.
}
if (message) {
Notification.error({
title: "请求提示",
title: '请求提示',
content: message,
});
}
return Promise.reject(error);
}
// 客户端请求错误
if (error.request) {
} else if (error.request) {
const resMsg = error.response?.message;
let message: string | null = resMsg ?? reqMessageTip;
if (has(error.config, "reqErrorTip")) {
if (has(error.config, 'reqErrorTip')) {
const tip = error.config.reqErrorTip;
if (tip) {
message = isString(tip) ? tip : message;
@ -84,7 +80,7 @@ export function addExceptionInterceptor(axios: AxiosInstance, exipreHandler?: (.
}
if (message) {
Notification.error({
title: "请求提示",
title: '请求提示',
content: message,
});
}

View File

@ -1,4 +1,4 @@
import { IToastOptions, toast } from '@/components';
import { AnToastOptions, toast } from '@/components/AnToast';
import { Message } from '@arco-design/web-vue';
import { AxiosInstance } from 'axios';
@ -10,7 +10,7 @@ export function addToastInterceptor(axios: AxiosInstance) {
axios.interceptors.request.use(
config => {
if (config.toast) {
let options: IToastOptions = {};
let options: AnToastOptions = {};
if (typeof config.toast === 'string') {
options = { message: config.toast };
}

View File

@ -80,7 +80,7 @@ export const AnForm = defineComponent({
},
render() {
return (
<Form layout="vertical" {...this.$attrs} {...this.formProps} ref="formRef" model={this.model}>
<Form layout="vertical" {...this.$attrs} {...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>
))}

View File

@ -89,6 +89,7 @@ export const AnFormItem = defineComponent({
return (
<BaseFormItem
{...props.item.itemProps}
class="an-form-item"
label={props.item.label as string}
rules={rules.value}
disabled={disabled.value}

View File

@ -149,6 +149,8 @@ export const AnFormModal = defineComponent({
onClose,
};
provide(AnFormModalContextKey, context);
return context;
},
render() {
@ -160,9 +162,10 @@ export const AnFormModal = defineComponent({
closable={false}
{...this.$attrs}
{...this.modalProps}
v-model:visible={this.visible}
class="an-form-modal"
maskClosable={false}
onClose={this.onClose}
v-model:visible={this.visible}
>
{{
title: this.modalTitle,

View File

@ -1,6 +1,6 @@
import { sleep } from "@/utils";
import { Message } from "@arco-design/web-vue";
import { Ref } from "vue";
import { sleep } from '@/utils';
import { Message } from '@arco-design/web-vue';
import { Ref } from 'vue';
export function useModalSubmit(props: any, formRef: any, visible: Ref<boolean>) {
const loading = ref(false);
@ -12,7 +12,6 @@ export function useModalSubmit(props: any, formRef: any, visible: Ref<boolean>)
try {
loading.value = true;
const data = formRef.value?.getModel() ?? {};
await sleep(5000);
const res = await props.submit?.(data, props.items);
const msg = res?.data?.message;
msg && Message.success(msg);

View File

@ -1,22 +1,58 @@
import { merge } from 'lodash-es';
import { AnFormModal, AnFormModalProps } from '../components/FormModal';
import { useFormProps } from './useForm';
import { FormItem } from './useItems';
export type FormModalUseOptions = Partial<Omit<AnFormModalProps, 'items'>> & {
/**
*
* @description `modalProps.width` 便
* @example
* ```ts
* 580
* ```
*/
width?: 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,
formProps,
trigger,
title,
modalProps,
submit,
formProps,
modalProps,
};
}
@ -27,20 +63,18 @@ export function useFormModal(options: FormModalUseOptions) {
const rawProps = useFormModalProps(options);
const props = reactive(rawProps);
const component = () => {
return (
<AnFormModal
ref={(el: any) => (modalRef.value = el)}
title={props.title}
trigger={props.title}
modalProps={props.modalProps as any}
model={props.model}
items={props.items}
formProps={props.formProps}
submit={props.submit}
></AnFormModal>
);
};
const component = () => (
<AnFormModal
ref={(el: any) => (modalRef.value = el)}
title={props.title}
trigger={props.title}
modalProps={props.modalProps as any}
model={props.model}
items={props.items}
formProps={props.formProps}
submit={props.submit}
></AnFormModal>
);
return {
props,

View File

@ -1,5 +1,12 @@
import { AnForm, AnFormInstance, AnFormModal, AnFormModalInstance, AnFormModalProps, AnFormProps } from '@/components/AnForm';
import AniEmpty from '@/components/empty/AniEmpty.vue';
import {
AnForm,
AnFormInstance,
AnFormModal,
AnFormModalInstance,
AnFormModalProps,
AnFormProps,
} 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, defineComponent, ref } from 'vue';
@ -180,17 +187,17 @@ export const AnTable = defineComponent({
};
props.pluginer?.callSetupHook(context);
provide(AnTableContextKey, context);
return context;
},
render() {
return (
<div class="table w-full">
<div class="an-table table w-full">
<div class={`mb-3 flex gap-2 toolbar justify-between`}>
{this.create && <AnFormModal {...this.create} ref="createRef"></AnFormModal>}
{this.modify && <AnFormModal {...this.modify} trigger={false} ref="modifyRef"></AnFormModal>}
{this.$slots.action?.(this.renderData)}
{this.pluginer?.actions && (
<div class={`flex-1 flex gap-2 items-center`}>
{this.pluginer.actions.map(Action => (
@ -242,7 +249,7 @@ export const AnTable = defineComponent({
onPageSizeChange={this.onPageSizeChange}
>
{{
empty: () => <AniEmpty />,
empty: () => <AnEmpty />,
...this.$slots,
}}
</Table>

View File

@ -1,25 +1,6 @@
import { FormModalUseOptions, useFormModalProps } from '@/components/AnForm';
export type UseCreateFormOptions = FormModalUseOptions & {
/**
*
* @description `modalProps.width` 便
* @example
* ```ts
* 580
* ```
*/
width?: number;
/**
*
* @description `formProps.class` 便
* @example
* ```ts
* 'grid grid-cols-2'
* ```
*/
formClass?: unknown;
};
export type UseCreateFormOptions = FormModalUseOptions & {};
export function useCreateForm(options: UseCreateFormOptions) {
if (options.width) {

View File

@ -4,24 +4,6 @@ import { ExtendFormItem } from './useSearchForm';
import { TableUseOptions } from './useTable';
export type ModifyForm = Omit<FormModalUseOptions, 'items' | 'trigger'> & {
/**
*
* @description `modalProps.width` 便
* @example
* ```ts
* 580
* ```
*/
width?: number;
/**
*
* @description `formProps.class` 便
* @example
* ```ts
* 'grid grid-cols-2'
* ```
*/
formClass?: unknown;
/**
*
* @default

View File

@ -5,6 +5,7 @@ import { SearchForm, 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'> {
/**
@ -103,7 +104,7 @@ export function useTable(options: TableUseOptions) {
const rawProps = useTableProps(options);
const props = reactive(rawProps);
const AnTabler = () => (
const AnTabler: FunctionalComponent = (_, { slots }) => (
<AnTable
ref={(el: any) => (tableRef.value = el)}
tableProps={props.tableProps}
@ -114,7 +115,9 @@ export function useTable(options: TableUseOptions) {
create={props.create as any}
modify={props.modify as any}
pluginer={pluginer}
></AnTable>
>
{slots}
</AnTable>
);
return {

View File

@ -3,6 +3,10 @@ import { Divider, Link, TableColumnData } from '@arco-design/web-vue';
interface TableBaseColumn {
/**
*
* @example
* ```tsx
* 'delete'
* ```
*/
type?: undefined;
}

View File

@ -76,7 +76,13 @@ export class PluginContainer {
widgets: any[] = [];
constructor(private plugins: AnTablePlugin[]) {
this.plugins.unshift(useTableRefresh(), useColumnConfig(), useRowFormat(), useRowDelete(), useRowModify());
this.plugins.unshift(
useTableRefresh(),
useColumnConfig(),
useRowFormat(),
useRowDelete(),
useRowModify()
);
for (const plugin of plugins) {
const action = plugin.action?.();
if (action) {

View File

@ -47,12 +47,13 @@ export const TableColumnConfig = defineComponent({
dataIndex: column.dataIndex,
title: column.title,
enable: true,
autoWidth: !column.width,
autoWidth: false,
width: column.width ?? 60,
editable: !column.configable,
});
}
items.value = list;
onItemChange();
};
const onItemChange = () => {

View File

@ -8,18 +8,14 @@
</template>
<script setup lang="ts">
defineOptions({
name: "toast",
});
const props = defineProps({
message: {
type: String,
default: "正在操作中,请稍等...",
default: '正在操作中,请稍等...',
},
icon: {
type: String,
default: "icon-park-outline-loading-one",
default: 'icon-park-outline-loading-one',
},
iconRotate: {
type: Boolean,
@ -37,8 +33,8 @@ const props = defineProps({
const style = computed(() => {
return {
pointerEvents: props.cover ? "initial" : "none",
backgroundColor: props.mask ? "rgba(0, 0, 0, 0.2)" : "transparent",
pointerEvents: props.cover ? 'initial' : 'none',
backgroundColor: props.mask ? 'rgba(0, 0, 0, 0.2)' : 'transparent',
};
});
</script>
@ -54,8 +50,8 @@ const style = computed(() => {
display: flex;
justify-content: center;
align-items: center;
pointer-events: v-bind("style.pointerEvents");
background-color: v-bind("style.backgroundColor");
pointer-events: v-bind('style.pointerEvents');
background-color: v-bind('style.backgroundColor');
}
.toast-content {
display: flex;

View File

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

View File

@ -1,156 +0,0 @@
<template>
<bread-page>
<template #content>
<div class="h-full w-full grid grid-cols-[auto_1fr] gap-4 p-4">
<div class="bg-white w-[256px]">
<div class="flex items-center justify-between gap-2 px-4 h-14">
<span class="text-base">菜单列表</span>
<div>
<a-button>
<template #icon>
<i class="icon-park-outline-plus"></i>
</template>
</a-button>
</div>
</div>
<a-tree
:data="menus"
:default-expand-all="true"
:block-node="true"
:field-names="{
icon: undefined,
title: 'name',
key: 'id',
}"
>
<template #title="node">
<div class="group flex-1 flex items-center justify-between gap-2">
<div @click="onEdit(node)">
<!-- <a-tag :color="MenuTypes.fmt(node.type, 'color')" size="small" :bordered="true">
{{ MenuTypes.fmt(node.type) }}
</a-tag> -->
<i :class="node.icon" class="ml-2"></i>
{{ node.name }}
</div>
<div class="hidden group-hover:block">
<i
v-if="node.type === MenuType.MENU"
class="text-sm text-gray-400 hover:text-gray-700 icon-park-outline-plus"
></i>
<i class="text-sm text-gray-400 hover:text-gray-700 icon-park-outline-delete"></i>
</div>
</div>
</template>
</a-tree>
</div>
<div class="bg-white">
<a-card title="菜单信息" :bordered="false">
<Form ref="formRef" v-bind="form"></Form>
</a-card>
<a-divider :margin="0"></a-divider>
<div class="px-4 mt-4">
<btn-table></btn-table>
</div>
</div>
</div>
</template>
</bread-page>
</template>
<script setup lang="tsx">
import { Menu, api } from "@/api";
import { useForm, Form, useAniTable, FormInstance } from "@/components";
import { MenuType, MenuTypes } from "@/constants/menu";
const formRef = ref<FormInstance | null>(null);
const menus = ref<any[]>([]);
const treeEach = (tree: any[], fn: any) => {
for (const item of tree) {
if (item.children) {
treeEach(item.children, fn);
}
fn(item);
}
};
const onEdit = (row: any) => {
formRef.value?.setModel(row);
(btn.props as any).data = row.buttons;
};
onMounted(async () => {
const res = await api.menu.getMenus({ tree: true });
const data = res.data.data ?? [];
treeEach(data, (item: Menu) => {
if (item.type === MenuType.BUTTON) {
return;
}
if (item.type === MenuType.PAGE) {
(item as any).buttons = (item as any).children;
delete (item as any).children;
}
(item as any).iconRender = () => <i class={item.icon} />;
});
menus.value = data;
});
const form = useForm({
items: [
{
field: "name",
label: "菜单名称",
type: "input",
},
{
field: "icon",
label: "菜单图标",
type: "input",
},
],
async submit(arg) {
console.log(arg);
},
});
const [btnTable, btn] = useAniTable({
columns: [
{
title: " 名称",
dataIndex: "name",
},
{
title: "标识",
dataIndex: "code",
},
{
title: "操作",
type: "button",
width: 140,
buttons: [
{
type: "modify",
text: "修改",
},
{
text: "删除",
type: "delete",
},
],
},
],
create: {},
modify: {},
});
</script>
<style lang="less" scoped></style>
<route lang="json">
{
"meta": {
"sort": 10302,
"title": "菜单管理",
"icon": "icon-park-outline-add-subtract"
}
}
</route>

View File

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

View File

@ -1,4 +1,4 @@
import AniEmpty from "@/components/empty/AniEmpty.vue";
import AniEmpty from "@/components/AnEmpty/AnEmpty.vue";
import { TableColumnData as BaseColumn, TableData as BaseData, Table as BaseTable } from "@arco-design/web-vue";
import { merge } from "lodash-es";
import { PropType, computed, defineComponent, reactive, ref } from "vue";

View File

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

View File

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

View File

@ -9,30 +9,23 @@ class Constant<T extends Item[]> {
/**
*
* @param value
* @param key label
* @returns
*/
fmt<K extends T[number]["value"]>(value: K, key?: keyof T[number]) {
return this.raw.find((item) => item.value === value)?.[key ?? ("label" as any)];
fmt<K extends T[number]['value']>(value: K, key?: keyof T[number]) {
return this.raw.find(item => item.value === value)?.[key ?? ('label' as any)];
}
/**
*
* @param key value
* @returns
*/
val<K extends keyof T[number]>(key?: K) {
return this.raw.map((item) => item[key ?? ("value" as any)]);
return this.raw.map(item => item[key ?? ('value' as any)]);
}
/**
*
* @param value
* @returns
*/
get(value: any) {
return this.raw.find((item) => item.value === value);
return this.raw.find(item => item.value === value);
}
}

View File

@ -1,28 +1,28 @@
import dayjs from "dayjs";
import "dayjs/locale/zh-cn";
import localData from "dayjs/plugin/localeData";
import relativeTime from "dayjs/plugin/relativeTime";
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import localData from 'dayjs/plugin/localeData';
import relativeTime from 'dayjs/plugin/relativeTime';
/**
*
*
*/
const DATETIME = "YYYY-MM-DD HH:mm";
const DATETIME = 'YYYY-MM-DD HH:mm';
/**
*
*/
const DATE = "YYYY-MM-DD";
const DATE = 'YYYY-MM-DD';
/**
*
*/
const TIME = "HH:mm:ss";
const TIME = 'HH:mm:ss';
/**
*
*/
dayjs.locale("zh-cn");
dayjs.locale('zh-cn');
/**
*
@ -37,7 +37,6 @@ dayjs.extend(relativeTime);
dayjs.extend(localData);
/**
*
*
*/
dayjs.DATETIME = DATETIME;
@ -53,9 +52,13 @@ dayjs.DATE = DATE;
dayjs.TIME = TIME;
/**
* formatformat使
*
*/
dayjs.prototype._format = dayjs.prototype.format;
/**
*
*/
dayjs.prototype.format = function (format?: string) {
if (format) {
return this._format(format);
@ -64,4 +67,3 @@ dayjs.prototype.format = function (format?: string) {
};
export { DATE, DATETIME, TIME, dayjs };

View File

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

View File

@ -1,52 +1,45 @@
<template>
<BreadPage>
<Table v-bind="table"> </Table>
<CategoryTable />
</BreadPage>
</template>
<script setup lang="tsx">
import { api } from "@/api";
import { Table, createColumn, updateColumn, useTable } from "@/components";
import { listToTree } from "@/utils/listToTree";
import { api } from '@/api';
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
import { listToTree } from '@/utils/listToTree';
const table = useTable({
data: async (model, paging) => {
const res = await api.category.getCategories({ ...model, ...paging });
const data = listToTree(res.data.data ?? []);
return { data: { data, total: (res.data as any).total } };
},
const { component: CategoryTable } = useTable({
columns: [
{
title: "名称",
dataIndex: "title",
title: '名称',
dataIndex: 'title',
width: 240,
render({ record }) {
return (
<div class="flex flex-col overflow-hidden">
<span>{record.title}</span>
<span class="text-gray-400 text-xs truncate">@{record.slug}</span>
</div>
);
},
render: ({ record }) => (
<div class="flex flex-col overflow-hidden">
<span>{record.title}</span>
<span class="text-gray-400 text-xs truncate">#{record.slug}</span>
</div>
),
},
{
title: "描述",
dataIndex: "description",
title: '描述',
dataIndex: 'description',
},
createColumn,
updateColumn,
useCreateColumn(),
useUpdateColumn(),
{
type: "button",
title: "操作",
type: 'button',
title: '操作',
width: 120,
buttons: [
{
type: "modify",
text: "修改",
type: 'modify',
text: '修改',
},
{
type: "delete",
text: "删除",
type: 'delete',
text: '删除',
onClick({ record }) {
return api.category.delCategory(record.id);
},
@ -54,76 +47,52 @@ const table = useTable({
],
},
],
search: {
button: false,
items: [
{
field: "nickname",
label: "登陆账号",
type: "search",
required: false,
enableLoad: true,
nodeProps: {
placeholder: "分类名称",
} as any,
itemProps: {
hideLabel: true,
},
},
],
source: async model => {
const res = await api.category.getCategories(model);
const data = listToTree(res.data.data ?? []);
return { data: { data, total: (res.data as any).total } };
},
create: {
title: "添加分类",
modalProps: {
width: 580,
search: [
{
field: 'nickname',
label: '登陆账号',
setter: 'search',
enterable: true,
searchable: true,
},
],
create: {
title: '添加分类',
width: 580,
items: [
{
field: "parentId",
label: "父级分类",
type: "select",
options: async () => {
const res = await api.category.getCategories({ size: 0 });
return (res.data.data ?? []).map(({ id, title }: any) => ({ value: id, label: title }));
},
},
{
field: "title",
label: "分类名称",
type: "input",
field: 'title',
label: '分类名称',
setter: 'input',
required: true,
nodeProps: {
placeholder: "请输入分类名称",
},
},
{
field: "slug",
label: "分类别名",
type: "input",
field: 'slug',
label: '分类别名',
setter: 'input',
required: true,
nodeProps: {
placeholder: "请输入分类别名",
},
},
{
field: "description",
label: "描述",
type: "textarea",
field: 'description',
label: '描述',
setter: 'textarea',
required: false,
nodeProps: {
placeholder: "请输入描述",
},
},
],
submit: async ({ model }) => {
return api.category.addCategory(model);
submit: model => {
return api.category.addCategory(model as any);
},
},
modify: {
extend: true,
title: "修改分类",
submit: async ({ model }) => {
return api.category.setCategory(model.id, model);
title: '修改分类',
submit: model => {
return api.category.setCategory(model.id, model as any);
},
},
});

View File

@ -2,10 +2,6 @@
<div></div>
</template>
<script setup lang="tsx"></script>
<style lang="less"></style>
<route lang="json">
{
"meta": {

View File

@ -1,7 +1,7 @@
<template>
<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-input-search allow-clear placeholder="分类名称" class="mb-2" @search="updateFileCategories"></a-input-search>
<a-button @click="formCtx.open">
<template #icon>
<i class="icon-park-outline-add"></i>
@ -47,18 +47,18 @@
</div>
</li>
</ul>
<ani-empty v-else></ani-empty>
<an-empty v-else></an-empty>
</a-spin>
</a-scrollbar>
</div>
</template>
<script setup lang="ts">
import { FileCategory, api } from "@/api";
import { useAniFormModal } from "@/components";
import { delConfirm } from "@/utils";
import { Message } from "@arco-design/web-vue";
import { PropType } from "vue";
import { FileCategory, api } from '@/api';
import { useAniFormModal } from '@/components';
import { delConfirm } from '@/utils';
import { Message } from '@arco-design/web-vue';
import { PropType } from 'vue';
defineProps({
current: {
@ -66,7 +66,7 @@ defineProps({
},
});
const emit = defineEmits(["change"]);
const emit = defineEmits(['change']);
const list = ref<FileCategory[]>([]);
const loading = ref(false);
@ -75,8 +75,8 @@ const updateFileCategories = async () => {
loading.value = true;
const res = await api.fileCategory.getFileCategorys({ size: 0 });
list.value = res.data.data ?? [];
list.value.unshift({ id: undefined, name: "全部" } as any);
list.value.length && emit("change", list.value[0]);
list.value.unshift({ id: undefined, name: '全部' } as any);
list.value.length && emit('change', list.value[0]);
} catch {
// nothing to do
} finally {
@ -93,7 +93,7 @@ const onDeleteRow = async (row: FileCategory) => {
};
const [formModal, formCtx] = useAniFormModal({
title: ({ model }) => (!model.id ? "新建分类" : "修改分类"),
title: ({ model }) => (!model.id ? '新建分类' : '修改分类'),
trigger: false,
modalProps: {
width: 580,
@ -103,19 +103,19 @@ const [formModal, formCtx] = useAniFormModal({
},
items: [
{
field: "name",
label: "分类名称",
type: "input",
field: 'name',
label: '分类名称',
type: 'input',
},
{
field: "code",
label: "分类编码",
type: "input",
field: 'code',
label: '分类编码',
type: 'input',
},
{
field: "description",
label: "备注",
type: "textarea",
field: 'description',
label: '备注',
type: 'textarea',
},
],
submit: async ({ model }) => {

View File

@ -1,10 +1,15 @@
<template>
<a-button type="primary" @click="visible = true"> 上传文件 </a-button>
<a-button type="primary" @click="visible = true">
<template #icon>
<i class="icon-park-outline-upload"></i>
</template>
上传
</a-button>
<a-modal
v-model:visible="visible"
title="上传文件"
title-align="start"
:width="860"
:width="940"
:mask-closable="false"
:on-before-cancel="onBeforeCancel"
@close="onClose"
@ -22,7 +27,7 @@
@error="onUploadError"
>
<template #upload-button>
<a-button type="outline"> 选择文件 </a-button>
<a-button type="primary"> 选择文件 </a-button>
</template>
</a-upload>
<div class="flex-1 flex items-center text-gray-400">
@ -45,20 +50,21 @@
</div>
<div class="flex items-center justify-between gap-2 text-gray-400 mb-[-4px] mt-0.5">
<span class="text-xs text-gray-400">
{{ numeral(item.file?.size).format("0 b") }}
{{ numeral(item.file?.size).format('0 b') }}
</span>
<span class="text-xs">
<span v-if="item.status === 'init'"> </span>
<span v-else-if="item.status === 'uploading'">
<span class="text-xs">
速度{{ numeral(fileMap.get(item.uid)?.speed || 0).format("0 b") }}/s,
进度{{ Math.floor((item.percent || 0) * 100) }}%
速度{{ numeral(fileMap.get(item.uid)?.speed || 0).format('0 b') }}/s, 进度{{
Math.floor((item.percent || 0) * 100)
}}%
</span>
</span>
<span v-else-if="item.status === 'done'" class="text-green-600">
完成(
耗时{{ fileMap.get(item.uid)?.cost || 0 }},
平均{{ numeral(fileMap.get(item.uid)?.aspeed || 0).format("0 b") }}/s)
完成( 耗时{{ fileMap.get(item.uid)?.cost || 0 }}, 平均{{
numeral(fileMap.get(item.uid)?.aspeed || 0).format('0 b')
}}/s)
</span>
<span v-else="item.status === 'error'" class="text-red-500">
失败(原因{{ fileMap.get(item.uid)?.error }})
@ -77,7 +83,7 @@
</ul>
<div v-else class="h-[424px] flex items-center justify-center">
<ani-empty></ani-empty>
<an-empty></an-empty>
</div>
<template #footer>
@ -97,16 +103,16 @@
</template>
<script setup lang="ts">
import { RequestParams, api } from "@/api";
import { delConfirm } from "@/utils";
import { FileItem, Message, RequestOption, UploadInstance } from "@arco-design/web-vue";
import axios from "axios";
import numeral from "numeral";
import { getIcon } from "./util";
import { RequestParams, api } from '@/api';
import { delConfirm } from '@/utils';
import { FileItem, Message, RequestOption, UploadInstance } from '@arco-design/web-vue';
import axios from 'axios';
import numeral from 'numeral';
import { getIcon } from './util';
const emit = defineEmits<{
(event: "success", item: FileItem): void;
(event: "close", count: number): void;
(event: 'success', item: FileItem): void;
(event: 'close', count: number): void;
}>();
const visible = ref(false);
@ -137,10 +143,10 @@ const stat = computed(() => {
errorCount: 0,
};
for (const item of fileList.value) {
if (item.status === "init") result.initCount++;
if (item.status === "uploading") result.uploadingCount++;
if (item.status === "done") result.doneCount++;
if (item.status === "error") result.errorCount++;
if (item.status === 'init') result.initCount++;
if (item.status === 'uploading') result.uploadingCount++;
if (item.status === 'done') result.doneCount++;
if (item.status === 'error') result.errorCount++;
}
return result;
});
@ -160,7 +166,7 @@ const pauseItem = (item: FileItem) => {
uploadRef.value?.abort(item);
const file = fileMap.get(item.uid);
if (file) {
file.error = "手动中止";
file.error = '手动中止';
}
};
@ -169,7 +175,7 @@ const pauseItem = (item: FileItem) => {
* @param item 文件
*/
const removeItem = (item: FileItem) => {
const index = fileList.value.findIndex((i) => i.uid === item.uid);
const index = fileList.value.findIndex(i => i.uid === item.uid);
if (index > -1) {
fileList.value.splice(index, 1);
}
@ -188,7 +194,7 @@ const retryItem = (item: FileItem) => {
*/
const clearUploaded = async () => {
if (stat.value.doneCount !== fileList.value.length) {
await delConfirm("当前有未上传完成的文件,是否继续清空?");
await delConfirm('当前有未上传完成的文件,是否继续清空?');
}
fileList.value = [];
};
@ -198,7 +204,7 @@ const clearUploaded = async () => {
* @param item 文件
*/
const onUploadSuccess = (item: FileItem) => {
emit("success", item);
emit('success', item);
};
/**
@ -208,7 +214,7 @@ const onUploadSuccess = (item: FileItem) => {
const onUploadError = (item: FileItem) => {
const file = fileMap.get(item.uid);
if (file) {
file.error = item.response?.data?.message || "网络异常";
file.error = item.response?.data?.message || '网络异常';
}
};
@ -216,8 +222,8 @@ const onUploadError = (item: FileItem) => {
* 关闭前检测
*/
const onBeforeCancel = () => {
if (fileList.value.some((i) => i.status === "uploading")) {
Message.warning("提示:文件上传中,请稍后再试!");
if (fileList.value.some(i => i.status === 'uploading')) {
Message.warning('提示:文件上传中,请稍后再试!');
return false;
}
return true;
@ -229,7 +235,7 @@ const onBeforeCancel = () => {
const onClose = () => {
fileMap.clear();
fileList.value = [];
emit("close", stat.value.doneCount);
emit('close', stat.value.doneCount);
};
/**
@ -246,7 +252,7 @@ const upload = (option: RequestOption) => {
cost: 0,
speed: 0,
aspeed: 0,
error: "网络异常",
error: '网络异常',
});
}
const item = fileMap.get(fileItem.uid)!;
@ -262,7 +268,7 @@ const upload = (option: RequestOption) => {
const nowTime = Date.now();
const diff = (e.loaded - lastLoaded) / (nowTime - lastTime);
const speed = Math.floor(diff * 1000);
item.aspeed = (item.speed + speed) / 2
item.aspeed = (item.speed + speed) / 2;
item.speed = speed;
item.lastLoaded = e.loaded;
item.lastTime = nowTime;
@ -297,15 +303,15 @@ defineExpose({
});
// TODO
const group = ref("default");
const group = ref('default');
const groupOptions = [
{
label: "默认分类",
value: "default",
label: '默认分类',
value: 'default',
},
{
label: "视频分类",
value: "video",
label: '视频分类',
value: 'video',
},
];
</script>

View File

@ -2,16 +2,13 @@
<BreadPage>
<template #content>
<div class="overflow-hidden grid grid-cols-[auto_1fr] gap-2 m-4">
<ani-group class="bg-white p-4 w-[242px]" :current="current" @change="onCategoryChange"></ani-group>
<AnGroup class="bg-white p-4 w-[242px]" :current="current" @change="onCategoryChange"></AnGroup>
<div class="bg-white p-4">
<file-table>
<MaterialTable>
<template #action>
<ani-upload @close="onUploadClose"></ani-upload>
<a-button type="primary" status="danger" :disabled="!selected.length" @click="onDeleteMany">
批量删除
</a-button>
<AnUpload></AnUpload>
</template>
</file-table>
</MaterialTable>
<a-image-preview v-model:visible="visible" :src="image"></a-image-preview>
</div>
</div>
@ -20,70 +17,47 @@
</template>
<script setup lang="tsx">
import { FileCategory, api } from "@/api";
import { createColumn, updateColumn, useAniTable } from "@/components";
import { delConfirm } from "@/utils";
import { Message } from "@arco-design/web-vue";
import numeral from "numeral";
import AniGroup from "./components/group.vue";
import AniUpload from "./components/upload.vue";
import { getIcon } from "./components/util";
import { FileCategory, api } from '@/api';
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
import { getIcon } from './components/util';
import numeral from 'numeral';
import AnGroup from './components/AnGroup.vue';
import AnUpload from './components/AnUpload.vue';
const visible = ref(false);
const image = ref("");
const selected = ref<number[]>([]);
const current = ref<FileCategory>();
const image = ref('');
const preview = (record: any) => {
if (!record.mimetype.startsWith("image")) {
window.open(record.path, "_blank");
if (!record.mimetype.startsWith('image')) {
window.open(record.path, '_blank');
return;
}
image.value = record.path;
visible.value = true;
};
const onUploadClose = (count: number) => {
if (count) {
fileCtx.refresh();
}
};
const onDeleteMany = async () => {
await delConfirm();
const res = await api.file.delFiles(selected.value as any[]);
selected.value = [];
Message.success(res.data.message);
fileCtx.refresh();
};
const onCategoryChange = (category: FileCategory) => {
if (fileCtx.props.search?.model) {
fileCtx.props.search.model.categoryId = category.id;
if (props.search?.model) {
props.search.model.categoryId = category.id;
}
current.value = category;
fileCtx.refresh();
tableRef.value?.refresh();
};
const [fileTable, fileCtx] = useAniTable({
data: async (model, paging) => {
return api.file.getFiles({ ...model, ...paging });
},
tableProps: {
rowSelection: {
showCheckedAll: true,
},
onSelectionChange(rowKeys) {
selected.value = rowKeys as number[];
},
},
const {
component: MaterialTable,
tableRef,
props,
} = useTable({
columns: [
{
title: "文件名称",
dataIndex: "name",
title: '文件名称',
dataIndex: 'name',
render: ({ record }) => (
<div class="flex items-center gap-2">
<div class="w-8 flex justify-center">
{record.mimetype.startsWith("image") ? (
{record.mimetype.startsWith('image') ? (
<a-avatar size={26} shape="square">
<img src={record.path}></img>
</a-avatar>
@ -99,80 +73,80 @@ const [fileTable, fileCtx] = useAniTable({
{record.name}
</span>
<span class="text-gray-400 text-xs truncate">
{numeral(record.size).format("0 b")}
{numeral(record.size).format('0 b')}
<span class="ml-2">{record.category?.name}</span>
</span>
</div>
</div>
),
},
createColumn,
updateColumn,
useCreateColumn(),
useUpdateColumn(),
{
type: "button",
title: "操作",
width: 120,
type: 'button',
title: '操作',
width: 160,
buttons: [
{
type: "modify",
text: "修改",
text: '下载',
onClick: props => {
window.open(props.record.path, '_blank');
},
},
{
type: "delete",
text: "删除",
onClick({ record }) {
return api.file.delFile(record.id);
type: 'modify',
text: '修改',
},
{
type: 'delete',
text: '删除',
onClick: props => {
return api.file.delFile(props.record.id);
},
},
],
},
],
source: async model => {
return api.file.getFiles(model);
},
search: {
button: false,
hideSearch: false,
model: {
categoryId: undefined,
},
items: [
{
field: "name",
label: "文件名称",
type: "search",
field: 'name',
label: '素材名称',
setter: 'search',
searchable: true,
enterable: true,
itemProps: {
hideLabel: true,
},
nodeProps: {
placeholder: "素材名称",
},
},
],
},
modify: {
title: "修改素材",
modalProps: {
width: 580,
},
title: '修改素材',
width: 580,
items: [
{
field: "categoryId",
label: "分类",
type: "select",
options: () => api.fileCategory.getFileCategorys({ size: 0 }),
field: 'categoryId',
label: '分类',
setter: 'select',
options: () => api.fileCategory.getFileCategorys({ size: 0 }) as any,
},
{
field: "name",
label: "名称",
type: "input",
field: 'name',
label: '名称',
setter: 'input',
},
{
field: "description",
label: "描述",
type: "textarea",
field: 'description',
label: '描述',
setter: 'textarea',
},
],
submit: ({ model }) => {
console.log(model);
submit: model => {
return api.file.setFile(model.id, model);
},
},

View File

@ -1,131 +0,0 @@
<template>
<a-popover v-model:popup-visible="visible" position="br" trigger="click">
<a-button class="float-right">设置</a-button>
<template #content>
<div class="mb-1 leading-none border-b border-gray-100 pb-3">设置表格列</div>
<a-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">
<li
v-for="(item, index) in items"
:key="item.dataIndex"
class="group flex items-center justify-between py-2 pr-8 select-none"
>
<div class="flex gap-2">
<a-checkbox v-model="item.enable" :disabled="!item.editable" size="large" @change="onItemChange">
{{ item.dataIndex }}
</a-checkbox>
<span class="hidden group-hover:inline-block ml-4">
<i v-show="!item.editable" class="icon-park-outline-drag cursor-move"></i>
</span>
</div>
<div class="flex gap-2 items-center">
<a-checkbox v-model="item.autoWidth" :disabled="!item.editable">
<template #checkbox="{ checked }">
<a-tag :checked="checked" :checkable="item.editable" color="blue">自适应</a-tag>
</template>
</a-checkbox>
<a-divider direction="vertical" :margin="8"></a-divider>
<a-input-number
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>
</a-scrollbar>
<div class="mt-4 flex gap-2 items-center justify-between">
<div class="flex items-center">
<a-checkbox :indeterminate="indeterminate" v-model="checkAll" @change="onCheckAllChange"> </a-checkbox>
<span class="text-xs text-gray-400 ml-1">
({{ items.filter(i => i.enable).length }}/{{ items.length }})
</span>
</div>
<div class="space-x-2">
<a-button @click="onReset"></a-button>
<a-button type="primary" @click="onConfirm"></a-button>
</div>
</div>
</template>
</a-popover>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
interface Item {
dataIndex: string;
enable: boolean;
autoWidth: boolean;
width: number;
editable: boolean;
}
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;
});
onMounted(() => {
items.value.push({
dataIndex: '顺序',
enable: true,
autoWidth: false,
width: 80,
editable: false,
});
for (let i = 1; i <= 10; i++) {
items.value.push({
dataIndex: `测试${i}`,
enable: true,
autoWidth: false,
width: 80,
editable: true,
});
}
items.value.push({
dataIndex: '操作',
enable: true,
autoWidth: false,
width: 80,
editable: false,
});
});
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;
};
</script>
<style scoped></style>

View File

@ -1,205 +0,0 @@
<template>
<bread-page class="">
<a-card title="菜单权限">
<template #title>
菜单权限
<a-link>展开</a-link>
</template>
<template #extra>
<a-checkbox>全部选择</a-checkbox>
</template>
</a-card>
<a-modal v-model:visible="state.visible" :width="1280" :title="'选择素材'" title-align="start" :closable="false">
<div class="w-full h-[600px] flex gap-4">
<div class="w-64 p-2 pr-4 border">
<a-input-search placeholder="请输入关键字"></a-input-search>
<a-tree
:data="items"
:block-node="true"
:field-names="{ title: 'title' }"
:default-expand-all="true"
class="mt-2"
>
<template #extra="nodeData">
<div class="text-slate-400 mr-2">
10
</div>
</template>
</a-tree>
</div>
<div class="flex-1 h-full">
<Table v-bind="table"></Table>
</div>
</div>
</a-modal>
</bread-page>
</template>
<script setup lang="tsx">
import { api } from '@/api';
import { Table, useTable } from '@/components';
import { dayjs } from '@/libs/dayjs';
import { menus } from "@/router";
import { cloneDeep } from "lodash-es";
const items = cloneDeep(menus) as any;
for (const item of items) {
item.checked = false;
if (item.icon) {
const icon = item.icon;
item.icon = () => <i class={icon}></i>;
}
item.switcherIcon = () => null;
if (item.children) {
for (const child of item.children) {
if (child.icon) {
const icon = child.icon;
child.icon = () => <i class={icon}></i>;
}
child.checked = false;
}
}
}
const state = reactive({
menus: items,
visible: false
});
const indeter = (items: any[]) => {
if (!items) {
return false;
}
const checked = items.filter((item) => item.checked);
return checked.length > 0 && checked.length < items.length;
};
const onItemChange = (item: any, menu: any) => {
const checked = menu.children.filter((item: any) => item.checked);
if (checked === 0) {
menu.checked = false;
} else if (checked === menu.children.length) {
menu.checked = true;
}
};
const table = useTable({
data: items,
columns: [
{
title: "角色名称",
dataIndex: "title",
width: 180,
},
{
title: "类型",
dataIndex: "description",
render: () => <a-tag color="blue">菜单</a-tag>,
},
{
title: "创建时间",
dataIndex: "createdAt",
width: 200,
render: ({ record }) => dayjs(record.createdAt).format(),
},
{
title: "操作",
type: "button",
width: 184,
buttons: [
{
type: "modify",
text: "修改",
},
{
text: '分配权限',
onClick: ({ record }) => {
console.log(record);
},
},
{
text: "删除",
type: "delete",
onClick: ({ record }) => {
return api.role.delRole(record.id);
},
}
],
},
],
search: {
items: [
{
extend: "name",
required: false,
nodeProps: {
placeholder: '请输入角色名称'
},
itemProps: {
hideLabel: true,
}
},
],
},
create: {
title: "新建角色",
modalProps: {
width: 580,
maskClosable: false,
},
formProps: {
layout: "vertical",
},
items: [
{
field: "name",
label: "角色名称",
type: "input",
required: true,
},
{
field: "slug",
label: "角色标识",
type: "input",
},
{
field: "description",
label: "个人描述",
type: "textarea",
},
{
field: "permissionIds",
label: "关联权限",
type: "select",
options: () => api.role.getRoles(),
nodeProps: {
multiple: true,
},
},
],
submit: ({ model }) => {
return api.role.addRole(model);
},
},
modify: {
extend: true,
title: "修改角色",
submit: ({ model }) => {
return api.role.updateRole(model.id, model);
},
},
});
</script>
<style lang="less">
</style>
<route lang="json">
{
"meta": {
"sort": 10201,
"title": "表格组件",
"icon": "icon-park-outline-add-subtract"
}
}
</route>

View File

@ -1,198 +0,0 @@
<template>
<bread-page id="list-page">
<template #default>
<div class="flex justify-between items-end gap-4">
<a-button type="primary" @click="visible = true">
<template #icon>
<i class="icon-park-outline-add"></i>
</template>
添加
</a-button>
</div>
<AList class="mt-2 bg-white" :bordered="true">
<template #header>
<div class="flex gap-2 items-center justify-between text-sm bg-[#fbfbfc] px-5 py-2">
<div class="flex gap-4 my-1.5">
<ACheckbox> 全选 </ACheckbox>
</div>
<div class="flex items-center text-gray-500">
<ADropdown>
<a-button type="text">
上传者
<i class="icon-park-outline-down ml-1"></i>
</a-button>
<template #content>
<ADoption class="!hover:bg-transparent !px-0 flex">
<div class="border-b border-gray-200 w-full pb-1">
<AInput placeholder="用户名关键字" />
</div>
</ADoption>
<ADoption v-for="j in 10">
<div class="flex items-center gap-1 w-48">
<AAvatar :size="20" class="mr-1 bg-slate-50">
<img :src="`https://picsum.photo1s/seed/picsum/200/300?${Math.random()}`" alt="" />
</AAvatar>
绝弹土豆
</div>
</ADoption>
</template>
</ADropdown>
<ADropdown>
<a-button type="text">
排序默认
<i class="icon-park-outline-down ml-1"></i>
</a-button>
<template #content>
<ADoption>
<template #icon>
<i class="icon-park-outline-check"></i>
</template>
<div class="w-48">默认</div>
</ADoption>
<ADoption>
<template #icon> </template>
按创建时间升序
</ADoption>
<ADoption> 按创建时间降序 </ADoption>
<ADoption> 按文件大小升序 </ADoption>
<ADoption> 按文件大小降序 </ADoption>
</template>
</ADropdown>
<div class="space-x-1">
<a-button type="text">
<template #icon>
<i class="icon-park-outline-list"></i>
</template>
</a-button>
<a-button type="text">
<template #icon>
<i class="icon-park-outline-insert-table"></i>
</template>
</a-button>
<a-button type="text">
<template #icon>
<i class="icon-park-outline-refresh"></i>
</template>
</a-button>
</div>
</div>
</div>
</template>
<AListItem v-for="i in 10">
<AListItemMeta title="测试图片.png" description="image/png 1.2MB">
<template #avatar>
<ACheckbox class="mr-3"></ACheckbox>
<AImage
:src="`https://picsum.photos/200/300?${Math.random()}`"
height="32"
width="48"
class="bg-slate-50"
>
</AImage>
</template>
<template #title>
<span class="hover:text-blue-500 cursor-pointer">测试图片.png</span>
</template>
<template #description>
<div class="text-xs text-gray-400">image/png 1.2MB</div>
</template>
</AListItemMeta>
<template #actions>
<div class="flex items-center gap-6">
<span class="text-xs text-gray-400">
<i class="icon-park-outline-user !w-[14px] !h-[14px]"></i>
绝弹
</span>
<span class="text-xs text-gray-400">2023-08-17 17:00:01</span>
<ADropdown @select="onRowActionsSelect" position="br">
<a-button type="text">
<template #icon>
<i class="icon-park-outline-more"></i>
</template>
</a-button>
<template #content>
<ADoption value="detail">
<template #icon>
<i class="icon-park-outline-repair"></i>
</template>
<div>详情</div>
</ADoption>
<ADoption value="delete" class="!text-red-500 !hover-bg-red-50">
<template #icon>
<i class="icon-park-outline-delete"></i>
</template>
删除
</ADoption>
</template>
</ADropdown>
</div>
</template>
</AListItem>
</AList>
<div class="mt-4 flex justify-end">
<a-pagination :total="232" :show-total="true"></a-pagination>
</div>
<a-modal v-model:visible="visible" title="修改密码" :width="432" :footer="false" title-align="start">
<a-form :model="{}" layout="vertical">
<a-form-item label="原密码">
<a-input placeholder="请输入原密码"></a-input>
</a-form-item>
<a-form-item label="新密码">
<a-input placeholder="请输入新密码"></a-input>
</a-form-item>
<a-form-item label="确认新密码">
<a-input placeholder="请再次输入新密码"></a-input>
</a-form-item>
<a-button type="primary" class="w-full mt-2">修改密码</a-button>
</a-form>
</a-modal>
</template>
</bread-page>
</template>
<script setup lang="tsx">
import { Modal } from "@arco-design/web-vue";
const visible = ref(false);
const onRowActionsSelect = () => {
Modal.open({
title: "提示",
titleAlign: "start",
width: 432,
content: "确定删除该文件吗?该操作不可恢复。",
maskClosable: false,
closable: false,
okText: "确定删除",
okButtonProps: {
status: "danger",
},
});
};
</script>
<style lang="less">
#list-page {
.arco-list-header {
padding: 0;
}
}
.arco-list-medium .arco-list-content-wrapper .arco-list-content > .arco-list-item {
padding: 4px 20px;
}
button.arco-btn-text,
.arco-btn-text[type="button"],
.arco-btn-text[type="submit"] {
color: inherit;
}
</style>
<route lang="json">
{
"meta": {
"sort": 10202,
"title": "表单组件",
"icon": "icon-park-outline-aperture-priority"
}
}
</route>

View File

@ -66,7 +66,7 @@ const { component: DictTable, tableRef } = useTable({
],
},
],
source(search) {
source: search => {
return api.dict.getDicts({ ...search, typeId: current.value?.id } as any);
},
search: {
@ -84,19 +84,18 @@ const { component: DictTable, tableRef } = useTable({
create: {
title: '新增字典',
width: 580,
model: {
typeId: undefined,
},
items: [
{
field: 'name',
label: '字典名',
setter: 'input',
required: true,
},
{
field: 'code',
label: '字典值',
setter: 'input',
required: true,
},
{
field: 'description',
@ -105,14 +104,16 @@ const { component: DictTable, tableRef } = useTable({
},
],
submit: model => {
return api.dict.addDict({ ...model, typeId: current.value?.id } as any);
const data = { ...model, typeId: current.value?.id } as any;
return api.dict.addDict(data);
},
},
modify: {
extend: true,
title: '修改字典',
submit: model => {
return api.dict.setDict(model.id, { ...model, typeId: current.value?.id } as any);
const data = { ...model, typeId: current.value?.id } as any;
return api.dict.setDict(model.id, data);
},
},
});

View File

@ -1,6 +1,4 @@
<template><div></div></template>
<script setup lang="tsx"></script>
<style scoped></style>
<route lang="json">
{

View File

@ -1,31 +1,30 @@
<template>
<BreadPage>
<Table v-bind="table"> </Table>
<UserTable />
<pass-modal></pass-modal>
</BreadPage>
</template>
<script setup lang="tsx">
import { api } from "@/api";
import { Table, createColumn, updateColumn, useTable } from "@/components";
import InputAvatar from "./components/avatar.vue";
import { usePassworModal } from "./components/password";
import { api } from '@/api';
import { useCreateColumn, useTable, useUpdateColumn } from '@/components/AnTable';
import { usePassworModal } from './components/password';
defineOptions({ name: "SystemUserPage" });
defineOptions({ name: 'SystemUserPage' });
const [passModal, passCtx] = usePassworModal();
const table = useTable({
data: async (model, paging) => {
return api.user.getUsers({ ...model, ...paging });
const { component: UserTable } = useTable({
source: async model => {
return api.user.getUsers(model);
},
columns: [
{
title: "用户昵称",
dataIndex: "username",
title: '用户昵称',
dataIndex: 'username',
render: ({ record }) => (
<div class="flex items-center">
<a-avatar size={32} class="!bg-brand-500">
{record.avatar?.startsWith("/") ? <img src={record.avatar} alt="" /> : record.nickname?.[0]}
{record.avatar?.startsWith('/') ? <img src={record.avatar} alt="" /> : record.nickname?.[0]}
</a-avatar>
<span class="ml-2 flex-1 flex flex-col overflow-hidden">
<span>{record.nickname}</span>
@ -35,30 +34,30 @@ const table = useTable({
),
},
{
title: "用户邮箱",
dataIndex: "email",
title: '用户邮箱',
dataIndex: 'email',
width: 200,
},
createColumn,
updateColumn,
useCreateColumn(),
useUpdateColumn(),
{
title: "操作",
type: "button",
title: '操作',
type: 'button',
width: 200,
buttons: [
{
type: "modify",
text: "修改",
type: 'modify',
text: '修改',
},
{
text: "设置密码",
text: '设置密码',
onClick({ record }) {
passCtx.open(record);
},
},
{
type: "delete",
text: "删除",
type: 'delete',
text: '删除',
onClick: async ({ record }) => {
return api.user.delUser(record.id, { toast: true });
},
@ -67,115 +66,80 @@ const table = useTable({
},
],
search: {
button: true,
hideSearch: true,
items: [
{
field: "nickname",
label: "用户昵称",
type: "input",
},
{
field: "nickname",
label: "用户昵称",
type: "input",
},
{
field: "nickname",
label: "用户昵称",
type: "input",
},
{
field: "nickname",
label: "用户昵称",
type: "input",
},
{
field: "nickname",
label: "用户昵称",
type: "input",
},
{
field: "nickname",
label: "用户昵称",
type: "input",
field: 'nickname',
label: '用户昵称',
setter: 'input',
},
],
},
create: {
title: "新建用户",
modalProps: {
width: 820,
maskClosable: false,
},
formProps: {
layout: "vertical",
class: "!grid grid-cols-2 gap-x-6",
},
model: {},
title: '新建用户',
width: 820,
formClass: '!grid grid-cols-2 gap-x-6',
items: [
{
field: "avatar",
label: "用户头像",
type: "custom",
itemProps: {
class: "col-span-2",
},
component({ model }) {
return <InputAvatar v-model={model.avatar}></InputAvatar>;
field: 'avatar',
label: '用户头像',
setter: 'input',
setterProps: {
class: 'col-span-2',
},
},
{
field: "username",
label: "登录账号",
type: "input",
field: 'username',
label: '登录账号',
setter: 'input',
required: true,
nodeProps: {
placeholder: "英文字母+数组组成5~10位",
setterProps: {
placeholder: '英文字母+数组组成5~10位',
},
},
{
field: "password",
label: "登陆密码",
type: "input",
nodeProps: {
placeholder: "包含大小写长度6 ~ 12位",
field: 'password',
label: '登陆密码',
setter: 'input',
setterProps: {
placeholder: '包含大小写长度6 ~ 12位',
},
},
{
field: "nickname",
label: "用户昵称",
type: "input",
field: 'nickname',
label: '用户昵称',
setter: 'input',
},
{
field: "roleIds",
label: "关联角色",
type: "select",
options: () => api.role.getRoles(),
nodeProps: {
field: 'roleIds',
label: '关联角色',
setter: 'select',
options: () => api.role.getRoles() as any,
setterProps: {
multiple: true,
},
},
{
field: "description",
label: "个人描述",
type: "textarea",
field: 'description',
label: '个人描述',
setter: 'textarea',
itemProps: {
class: "col-span-2",
class: 'col-span-2',
},
nodeProps: {
class: "h-[96px]",
setterProps: {
class: 'h-[96px]',
},
},
],
submit: ({ model }) => {
return api.user.addUser(model);
submit: model => {
return api.user.addUser(model as any);
},
},
modify: {
extend: true,
title: "修改用户",
submit: ({ model }) => {
return api.user.setUser(model.id, model);
title: '修改用户',
submit: model => {
return api.user.setUser(model.id, model as any);
},
},
});

View File

@ -19,11 +19,23 @@
<template #help> 支持 5MB 以内大小, png jpg 格式的图片 </template>
</a-form-item>
<a-form-item label="用户昵称">
<a-input v-model="user.nickname" placeholder="请输入" class="!w-[432px]" :max-length="24" :show-word-limit="true"></a-input>
<a-input
v-model="user.nickname"
placeholder="请输入"
class="!w-[432px]"
:max-length="24"
:show-word-limit="true"
></a-input>
<template #help> 用作系统内显示的名称可在后台修改 </template>
</a-form-item>
<a-form-item label="个人描述">
<a-textarea v-model="user.description" placeholder="请输入" class="!w-[432px] h-24" :max-length="140" :show-word-limit="true"></a-textarea>
<a-textarea
v-model="user.description"
placeholder="请输入"
class="!w-[432px] h-24"
:max-length="140"
:show-word-limit="true"
></a-textarea>
</a-form-item>
<a-form-item label="性别">
<a-radio-group v-model="user.gender" type="button">
@ -173,16 +185,16 @@
</template>
<script setup lang="tsx">
import { reactive } from "vue";
import { reactive } from 'vue';
const user = reactive({
nickname: "绝弹",
description: "选择在公开个人资料中显示私有项目的贡献,但不显示任何项目,仓库或组织信息",
theme: "dark",
email: "810335188@qq.com",
nickname: '绝弹',
description: '选择在公开个人资料中显示私有项目的贡献,但不显示任何项目,仓库或组织信息',
theme: 'dark',
email: '810335188@qq.com',
msg: [2],
gender: 1,
birth: "1988-12-18",
birth: '1988-12-18',
});
</script>

View File

@ -1,16 +1,16 @@
import { api } from "@/api";
import { store, useUserStore } from "@/store";
import { useMenuStore } from "@/store/menu";
import { treeEach, treeFilter, treeFind } from "@/utils/listToTree";
import { Notification } from "@arco-design/web-vue";
import { Router } from "vue-router";
import { MenuItem, menus } from "../menus";
import { APP_HOME_NAME } from "../routes/base";
import { APP_ROUTE_NAME, routes } from "../routes/page";
import { env } from "@/config/env";
import { api } from '@/api';
import { store, useUserStore } from '@/store';
import { useMenuStore } from '@/store/menu';
import { treeEach, treeFilter, treeFind } from '@/utils/listToTree';
import { Notification } from '@arco-design/web-vue';
import { Router } from 'vue-router';
import { menus } from '../menus';
import { APP_HOME_NAME } from '../routes/base';
import { APP_ROUTE_NAME, routes } from '../routes/page';
import { env } from '@/config/env';
const WHITE_LIST = ["/:all(.*)*"];
const UNSIGNIN_LIST = ["/login"];
const WHITE_LIST = ['/:all(.*)*'];
const UNSIGNIN_LIST = ['/login'];
/**
*
@ -23,7 +23,7 @@ export function useAuthGuard(router: Router) {
const userStore = useUserStore(store);
const redirect = router.currentRoute.value.path;
userStore.clearUser();
router.push({ path: "/login", query: { redirect } });
router.push({ path: '/login', query: { redirect } });
};
router.beforeEach(async function (to, from) {
@ -31,7 +31,7 @@ export function useAuthGuard(router: Router) {
const menuStore = useMenuStore(store);
// 手动指定直接通过
if (to.meta.auth?.some((i) => i === "*")) {
if (to.meta.auth?.some(i => i === '*')) {
return true;
}
@ -49,13 +49,13 @@ export function useAuthGuard(router: Router) {
// 已登陆进行提示
Notification.warning({
title: "跳转提示",
title: '跳转提示',
content: `您已登陆,如需重新登陆请退出后再操作!`,
});
// 不是从路由跳转的,跳转回首页
if (!from.matched.length) {
return "/";
return '/';
}
// 已登陆不允许
@ -64,15 +64,15 @@ export function useAuthGuard(router: Router) {
// 未登录跳转到登陆页面
if (!userStore.accessToken) {
return { path: "/login", query: { redirect: to.path } };
return { path: '/login', query: { redirect: to.path } };
}
// 未获取菜单进行获取
if (!menuStore.menus.length) {
// 菜单处理
const authMenus = treeFilter(menus, (item) => {
const authMenus = treeFilter(menus, item => {
if (item.path === env.homePath) {
item.path = "/";
item.path = '/';
}
return true;
});
@ -101,9 +101,9 @@ export function useAuthGuard(router: Router) {
menuStore.setCacheAppNames(appNames);
// 首页处理
const home = treeFind(routes, (i) => i.path === menuStore.home);
const home = treeFind(routes, i => i.path === menuStore.home);
if (home) {
const route = { ...home, name: APP_HOME_NAME, alias: "/" };
const route = { ...home, name: APP_HOME_NAME, alias: '/' };
router.removeRoute(home.name!);
router.addRoute(APP_ROUTE_NAME, route);
return router.replace(to.path);

View File

@ -1,6 +1,6 @@
import { NProgress } from "@/libs/nprogress";
import { useAppStore } from "@/store";
import { Router } from "vue-router";
import { NProgress } from '@/libs/nprogress';
import { useAppStore } from '@/store';
import { Router } from 'vue-router';
const routeMap = new Map<string, boolean>();

View File

@ -1,5 +1,5 @@
import { store, useAppStore } from "@/store";
import { Router } from "vue-router";
import { store, useAppStore } from '@/store';
import { Router } from 'vue-router';
export function useTitleGuard(router: Router) {
router.beforeEach(function (to) {

View File

@ -1,4 +1,3 @@
export * from "./menus";
export * from "./router";
export * from "./routes/page";
export * from './menus';
export * from './router';
export * from './routes/page';

View File

@ -1,5 +1,5 @@
import { RouteRecordRaw } from "vue-router";
import { appRoutes } from "../routes/page";
import { RouteRecordRaw } from 'vue-router';
import { appRoutes } from '../routes/page';
/**
*
@ -20,7 +20,6 @@ export interface MenuItem {
/**
*
* @param routes
* @returns
*/
function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
const items: MenuItem[] = [];
@ -29,8 +28,8 @@ function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
const { meta = {}, parentMeta, path } = route as any;
const { title, sort, icon, keepAlive = false, name } = meta;
let id = path;
let paths = route.path.split("/");
let parentId = paths.slice(0, -1).join("/");
let paths = route.path.split('/');
let parentId = paths.slice(0, -1).join('/');
if (parentMeta) {
const { title, icon, sort } = parentMeta;
id = `${path}/index`;
@ -42,11 +41,11 @@ function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
path,
id: path,
keepAlive: false,
parentId: paths.slice(0, -1).join("/"),
parentId: paths.slice(0, -1).join('/'),
});
} else {
const p = paths.slice(0, -1).join("/");
if (routes.some((i) => i.path === p) && parentMeta) {
const p = paths.slice(0, -1).join('/');
if (routes.some(i => i.path === p) && parentMeta) {
parentId = p;
}
}
@ -59,7 +58,6 @@ function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
/**
*
* @param list
* @returns
*/
function listToTree(list: MenuItem[]) {
const map: Record<string, MenuItem> = {};
@ -85,9 +83,8 @@ function listToTree(list: MenuItem[]) {
*
* @param routes
* @param key
* @returns
*/
function sort<T extends { children?: T[]; [key: string]: any }>(routes: T[], key = "sort") {
function sort<T extends { children?: T[]; [key: string]: any }>(routes: T[], key = 'sort') {
return routes.sort((a, b) => {
if (Array.isArray(a.children)) {
a.children = sort(a.children);

View File

@ -1,10 +1,10 @@
import { createRouter } from "vue-router";
import { useAuthGuard } from "../guards/auth";
import { useProgressGard } from "../guards/progress";
import { useTitleGuard } from "../guards/title";
import { baseRoutes } from "../routes/base";
import { historyMode } from "./util";
import { routes } from "../routes/page";
import { createRouter } from 'vue-router';
import { useAuthGuard } from '../guards/auth';
import { useProgressGard } from '../guards/progress';
import { useTitleGuard } from '../guards/title';
import { baseRoutes } from '../routes/base';
import { historyMode } from './util';
import { routes } from '../routes/page';
/**
*

View File

@ -1,5 +1,5 @@
import { env } from "@/config/env";
import { createWebHashHistory, createWebHistory } from "vue-router";
import { env } from '@/config/env';
import { createWebHashHistory, createWebHistory } from 'vue-router';
/**
*

View File

@ -1,14 +1,14 @@
import { RouteRecordRaw } from "vue-router";
import { RouteRecordRaw } from 'vue-router';
export const APP_HOME_NAME = "__APP_HOME__";
export const APP_HOME_NAME = '__APP_HOME__';
/**
*
*/
export const baseRoutes: RouteRecordRaw[] = [
{
path: "/",
path: '/',
name: APP_HOME_NAME,
component: () => "Home Page",
component: () => 'Home Page',
},
];

View File

@ -1,8 +1,8 @@
import generatedRoutes from "virtual:generated-pages";
import { RouteRecordRaw } from "vue-router";
import generatedRoutes from 'virtual:generated-pages';
import { RouteRecordRaw } from 'vue-router';
export const TOP_ROUTE_PREF = "_";
export const APP_ROUTE_NAME = "_layout";
export const TOP_ROUTE_PREF = '_';
export const APP_ROUTE_NAME = '_layout';
/**
*
@ -17,7 +17,7 @@ const transformRoutes = (routes: RouteRecordRaw[]) => {
if (route.name === APP_ROUTE_NAME) {
route.children = appRoutes;
}
route.path = route.path.replace(TOP_ROUTE_PREF, "");
route.path = route.path.replace(TOP_ROUTE_PREF, '');
topRoutes.push(route);
continue;
}

View File

@ -1,4 +1,3 @@
export * from "./app";
export * from "./store";
export * from "./user";
export * from './app';
export * from './store';
export * from './user';

View File

@ -17,6 +17,7 @@ declare module '@vue/runtime-core' {
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup']
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
ADatePicker: typeof import('@arco-design/web-vue')['DatePicker']
ADivider: typeof import('@arco-design/web-vue')['Divider']
ADoption: typeof import('@arco-design/web-vue')['Doption']
ADrawer: typeof import('@arco-design/web-vue')['Drawer']
@ -42,7 +43,10 @@ declare module '@vue/runtime-core' {
AMenu: typeof import('@arco-design/web-vue')['Menu']
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
AModal: typeof import('@arco-design/web-vue')['Modal']
AniEmpty: typeof import('./../components/empty/AniEmpty.vue')['default']
AnEmpty: typeof import('./../components/AnEmpty/AnEmpty.vue')['default']
AnForbidden: typeof import('./../components/AnForbidden/AnForbidden.vue')['default']
AnForbiden: typeof import('./../components/AnForbidden/AnForbiden.vue')['default']
AnToast: typeof import('./../components/AnToast/AnToast.vue')['default']
APagination: typeof import('@arco-design/web-vue')['Pagination']
APopover: typeof import('@arco-design/web-vue')['Popover']
AProgress: typeof import('@arco-design/web-vue')['Progress']
@ -53,6 +57,8 @@ declare module '@vue/runtime-core' {
ASpace: typeof import('@arco-design/web-vue')['Space']
ASpin: typeof import('@arco-design/web-vue')['Spin']
ASwitch: typeof import('@arco-design/web-vue')['Switch']
ATabPane: typeof import('@arco-design/web-vue')['TabPane']
ATabs: typeof import('@arco-design/web-vue')['Tabs']
ATag: typeof import('@arco-design/web-vue')['Tag']
ATextarea: typeof import('@arco-design/web-vue')['Textarea']
ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
@ -71,7 +77,7 @@ declare module '@vue/runtime-core' {
InputTexter: typeof import('./../components/editor/components/InputTexter.vue')['default']
Marquee: typeof import('./../components/editor/blocks/text/marquee.vue')['default']
Option: typeof import('./../components/editor/blocks/date/option.vue')['default']
Page403: typeof import('./../components/error/page-403.vue')['default']
Page403: typeof import('./../components/AnForbiden/page-403.vue')['default']
PanelHeader: typeof import('./../components/editor/components/PanelHeader.vue')['default']
PanelLeft: typeof import('./../components/editor/components/PanelLeft.vue')['default']
PanelMain: typeof import('./../components/editor/components/PanelMain.vue')['default']
@ -81,7 +87,5 @@ declare module '@vue/runtime-core' {
Render: typeof import('./../components/editor/blocks/date/render.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
'Temp.dev1': typeof import('./../components/breadcrumb/temp.dev1.vue')['default']
Toast: typeof import('./../components/toast/toast.vue')['default']
}
}