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 # 接口前缀:参见 axios 的 baseURL
VITE_API = https://appnify.app.juetan.cn/ VITE_API = https://appnify.app.juetan.cn/
# 首页路径 # 首页路径
VITE_HOME_PATH = /home/home VITE_HOME_PATH = /home
# 路由模式web(路径) hash(锚点) # 路由模式web(路径) hash(锚点)
VITE_HISTORY = web VITE_HISTORY = web
@ -24,7 +24,7 @@ VITE_PORT = 3020
# 代理前缀 # 代理前缀
VITE_PROXY_PREFIX = /api,/upload VITE_PROXY_PREFIX = /api,/upload
# 代理地址 # 代理地址
VITE_PROXY = http://127.0.0.1:3030/ VITE_PROXY = https://appnify.app.juetan.cn/
# API文档 说明:需返回符合 OPENAPI 规范的json内容 # API文档 说明:需返回符合 OPENAPI 规范的json内容
VITE_OPENAPI = http://127.0.0.1:3030/openapi.json VITE_OPENAPI = http://127.0.0.1:3030/openapi.json
# 文件后缀 说明设为dev时会优先加载index.dev.vue文件否则回退至index.vue文件 # 文件后缀 说明设为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> <a-config-provider>
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<keep-alive :include="menuStore.cacheTopNames"> <keep-alive :include="menuStore.cacheTopNames">
<component v-if="hasAuth(route, Component)" :is="Component"></component> <component v-if="hasAuth(route)" :is="Component"></component>
<page-403 v-else></page-403> <AnForbidden v-else></AnForbidden>
</keep-alive> </keep-alive>
</router-view> </router-view>
</a-config-provider> </a-config-provider>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { RouteLocationNormalizedLoaded } from "vue-router"; import { RouteLocationNormalizedLoaded } from 'vue-router';
import { useUserStore } from "./store"; import { useUserStore } from './store';
import { useMenuStore } from "./store/menu"; import { useMenuStore } from './store/menu';
const userStore = useUserStore(); const userStore = useUserStore();
const menuStore = useMenuStore(); const menuStore = useMenuStore();
const hasAuth = (route: RouteLocationNormalizedLoaded, c: any) => { const hasAuth = (route: RouteLocationNormalizedLoaded) => {
const aAuth = route.meta.auth; const aAuth = route.meta.auth;
const uAuth = userStore.auth; const uAuth = userStore.auth;
if (!aAuth?.length) { if (!aAuth?.length) {
return true; return true;
} }
if (aAuth.some((i) => i === "*")) { if (aAuth.some(i => i === '*')) {
return true; return true;
} }
if (uAuth.some((i) => aAuth.some((j) => j === i))) { if (uAuth.some(i => aAuth.some(j => j === i))) {
return true; return true;
} }
return false; return false;

View File

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

View File

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

View File

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

View File

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

View File

@ -80,7 +80,7 @@ export const AnForm = defineComponent({
}, },
render() { render() {
return ( 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 => ( {this.items.map(item => (
<AnFormItem key={item.field} item={item} items={this.items} model={this.model}></AnFormItem> <AnFormItem key={item.field} item={item} items={this.items} model={this.model}></AnFormItem>
))} ))}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,24 +4,6 @@ import { ExtendFormItem } from './useSearchForm';
import { TableUseOptions } from './useTable'; import { TableUseOptions } from './useTable';
export type ModifyForm = Omit<FormModalUseOptions, 'items' | 'trigger'> & { 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 * @default

View File

@ -5,6 +5,7 @@ import { SearchForm, useSearchForm } from './useSearchForm';
import { TableColumn, useTableColumns } from './useTableColumn'; import { TableColumn, useTableColumns } from './useTableColumn';
import { AnTablePlugin, PluginContainer } from './useTablePlugin'; import { AnTablePlugin, PluginContainer } from './useTablePlugin';
import { UseCreateFormOptions } from './useCreateForm'; import { UseCreateFormOptions } from './useCreateForm';
import { FunctionalComponent } from 'vue';
export interface TableUseOptions extends Pick<AnTableProps, 'source' | 'tableProps' | 'paging'> { export interface TableUseOptions extends Pick<AnTableProps, 'source' | 'tableProps' | 'paging'> {
/** /**
@ -103,7 +104,7 @@ export function useTable(options: TableUseOptions) {
const rawProps = useTableProps(options); const rawProps = useTableProps(options);
const props = reactive(rawProps); const props = reactive(rawProps);
const AnTabler = () => ( const AnTabler: FunctionalComponent = (_, { slots }) => (
<AnTable <AnTable
ref={(el: any) => (tableRef.value = el)} ref={(el: any) => (tableRef.value = el)}
tableProps={props.tableProps} tableProps={props.tableProps}
@ -114,7 +115,9 @@ export function useTable(options: TableUseOptions) {
create={props.create as any} create={props.create as any}
modify={props.modify as any} modify={props.modify as any}
pluginer={pluginer} pluginer={pluginer}
></AnTable> >
{slots}
</AnTable>
); );
return { return {

View File

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

View File

@ -76,7 +76,13 @@ export class PluginContainer {
widgets: any[] = []; widgets: any[] = [];
constructor(private plugins: AnTablePlugin[]) { 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) { for (const plugin of plugins) {
const action = plugin.action?.(); const action = plugin.action?.();
if (action) { if (action) {

View File

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

View File

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

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 { TableColumnData as BaseColumn, TableData as BaseData, Table as BaseTable } from "@arco-design/web-vue";
import { merge } from "lodash-es"; import { merge } from "lodash-es";
import { PropType, computed, defineComponent, reactive, ref } from "vue"; 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]) { fmt<K extends T[number]['value']>(value: K, key?: keyof T[number]) {
return this.raw.find((item) => item.value === value)?.[key ?? ("label" as any)]; return this.raw.find(item => item.value === value)?.[key ?? ('label' as any)];
} }
/** /**
* *
* @param key value
* @returns
*/ */
val<K extends keyof T[number]>(key?: K) { 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) { 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 from 'dayjs';
import "dayjs/locale/zh-cn"; import 'dayjs/locale/zh-cn';
import localData from "dayjs/plugin/localeData"; import localData from 'dayjs/plugin/localeData';
import relativeTime from "dayjs/plugin/relativeTime"; 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.extend(localData);
/** /**
*
* *
*/ */
dayjs.DATETIME = DATETIME; dayjs.DATETIME = DATETIME;
@ -53,9 +52,13 @@ dayjs.DATE = DATE;
dayjs.TIME = TIME; dayjs.TIME = TIME;
/** /**
* formatformat使 *
*/ */
dayjs.prototype._format = dayjs.prototype.format; dayjs.prototype._format = dayjs.prototype.format;
/**
*
*/
dayjs.prototype.format = function (format?: string) { dayjs.prototype.format = function (format?: string) {
if (format) { if (format) {
return this._format(format); return this._format(format);
@ -64,4 +67,3 @@ dayjs.prototype.format = function (format?: string) {
}; };
export { DATE, DATETIME, TIME, dayjs }; export { DATE, DATETIME, TIME, dayjs };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { env } from "@/config/env"; import { env } from '@/config/env';
import { createWebHashHistory, createWebHistory } from "vue-router"; 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[] = [ export const baseRoutes: RouteRecordRaw[] = [
{ {
path: "/", path: '/',
name: APP_HOME_NAME, name: APP_HOME_NAME,
component: () => "Home Page", component: () => 'Home Page',
}, },
]; ];

View File

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

View File

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

View File

@ -17,6 +17,7 @@ declare module '@vue/runtime-core' {
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox'] ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup'] ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup']
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider'] AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
ADatePicker: typeof import('@arco-design/web-vue')['DatePicker']
ADivider: typeof import('@arco-design/web-vue')['Divider'] ADivider: typeof import('@arco-design/web-vue')['Divider']
ADoption: typeof import('@arco-design/web-vue')['Doption'] ADoption: typeof import('@arco-design/web-vue')['Doption']
ADrawer: typeof import('@arco-design/web-vue')['Drawer'] 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'] AMenu: typeof import('@arco-design/web-vue')['Menu']
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem'] AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
AModal: typeof import('@arco-design/web-vue')['Modal'] AModal: typeof import('@arco-design/web-vue')['Modal']
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'] APagination: typeof import('@arco-design/web-vue')['Pagination']
APopover: typeof import('@arco-design/web-vue')['Popover'] APopover: typeof import('@arco-design/web-vue')['Popover']
AProgress: typeof import('@arco-design/web-vue')['Progress'] 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'] ASpace: typeof import('@arco-design/web-vue')['Space']
ASpin: typeof import('@arco-design/web-vue')['Spin'] ASpin: typeof import('@arco-design/web-vue')['Spin']
ASwitch: typeof import('@arco-design/web-vue')['Switch'] 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'] ATag: typeof import('@arco-design/web-vue')['Tag']
ATextarea: typeof import('@arco-design/web-vue')['Textarea'] ATextarea: typeof import('@arco-design/web-vue')['Textarea']
ATooltip: typeof import('@arco-design/web-vue')['Tooltip'] 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'] InputTexter: typeof import('./../components/editor/components/InputTexter.vue')['default']
Marquee: typeof import('./../components/editor/blocks/text/marquee.vue')['default'] Marquee: typeof import('./../components/editor/blocks/text/marquee.vue')['default']
Option: typeof import('./../components/editor/blocks/date/option.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'] PanelHeader: typeof import('./../components/editor/components/PanelHeader.vue')['default']
PanelLeft: typeof import('./../components/editor/components/PanelLeft.vue')['default'] PanelLeft: typeof import('./../components/editor/components/PanelLeft.vue')['default']
PanelMain: typeof import('./../components/editor/components/PanelMain.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'] Render: typeof import('./../components/editor/blocks/date/render.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
'Temp.dev1': typeof import('./../components/breadcrumb/temp.dev1.vue')['default']
Toast: typeof import('./../components/toast/toast.vue')['default']
} }
} }