refactor(projects): 精简版+动态路由权限初步

This commit is contained in:
Soybean
2022-01-03 22:20:10 +08:00
parent 7a0648dba5
commit de2057f141
354 changed files with 2053 additions and 22117 deletions

View File

@ -1,17 +0,0 @@
<template>
<n-card title="开发环境依赖" :bordered="false" size="small" class="rounded-16px shadow-sm">
<n-descriptions label-placement="left" bordered size="small">
<n-descriptions-item v-for="item in dependencies" :key="item.name" :label="item.name">
{{ item.version }}
</n-descriptions-item>
</n-descriptions>
</n-card>
</template>
<script setup lang="ts">
import { NCard, NDescriptions, NDescriptionsItem } from 'naive-ui';
import { packageJson } from '@/utils';
const dependencies = packageJson.devDependencies;
</script>
<style scoped></style>

View File

@ -1,17 +0,0 @@
<template>
<n-card title="生产环境依赖" :bordered="false" size="small" class="rounded-16px shadow-sm">
<n-descriptions label-placement="left" bordered size="small">
<n-descriptions-item v-for="item in dependencies" :key="item.name" :label="item.name">
{{ item.version }}
</n-descriptions-item>
</n-descriptions>
</n-card>
</template>
<script setup lang="ts">
import { NCard, NDescriptions, NDescriptionsItem } from 'naive-ui';
import { packageJson } from '@/utils';
const { dependencies } = packageJson;
</script>
<style scoped></style>

View File

@ -1,27 +0,0 @@
<template>
<n-card title="项目信息" :bordered="false" size="small" class="rounded-16px shadow-sm">
<n-descriptions label-placement="left" bordered size="small" :column="2">
<n-descriptions-item label="版本">
<n-tag type="primary">{{ version }}</n-tag>
</n-descriptions-item>
<n-descriptions-item label="最后编译时间">
<n-tag type="primary">{{ lastestBuildTime }}</n-tag>
</n-descriptions-item>
<n-descriptions-item label="Github地址">
<a class="text-primary" href="https://github.com/honghuangdc/soybean-admin" target="_blank">Github地址</a>
</n-descriptions-item>
<n-descriptions-item label="预览地址">
<a class="text-primary" href="https://soybean.pro" target="_blank">预览地址</a>
</n-descriptions-item>
</n-descriptions>
</n-card>
</template>
<script setup lang="ts">
import { NCard, NDescriptions, NDescriptionsItem, NTag } from 'naive-ui';
import { packageJson } from '@/utils';
const { version } = packageJson;
const lastestBuildTime = PROJECT_BUILD_TIME;
</script>
<style scoped></style>

View File

@ -1,13 +0,0 @@
<template>
<n-card title="关于" :bordered="false" size="large" class="rounded-16px shadow-sm">
<p class="leading-24px">
Soybean Admin 是一个基于 Vue3ViteNaive UITypeScript
的中后台解决方案它使用了最新的前端技术栈并提炼了典型的业务模型页面包括二次封装组件动态菜单权限校验粒子化权限控制等功能它可以帮助你快速搭建企业级中后台项目相信不管是从新技术使用还是其他方面都能帮助到你
</p>
</n-card>
</template>
<script setup lang="ts">
import { NCard } from 'naive-ui';
</script>
<style scoped></style>

View File

@ -1,6 +0,0 @@
import ProjectIntroduction from './ProjectIntroduction/index.vue';
import ProjectInfo from './ProjectInfo/index.vue';
import ProDependency from './ProDependency/index.vue';
import DevDependency from './DevDependency/index.vue';
export { ProjectIntroduction, ProjectInfo, ProDependency, DevDependency };

View File

@ -1,3 +1,3 @@
import About from './index.vue';
const About = () => import('./index.vue');
export { About };

View File

@ -1,14 +1,9 @@
<template>
<n-space :vertical="true" :size="16">
<project-introduction />
<project-info />
<pro-dependency />
<dev-dependency />
</n-space>
<div>
<h3>about</h3>
<router-link to="/">analysis</router-link>
</div>
</template>
<script setup lang="ts">
import { NSpace } from 'naive-ui';
import { ProjectIntroduction, ProjectInfo, ProDependency, DevDependency } from './components';
</script>
<script setup lang="ts"></script>
<style scoped></style>

View File

@ -1,576 +0,0 @@
<template>
<div>
<n-card title="按钮" class="h-full shadow-sm rounded-16px">
<n-grid cols="s:1 m:2" responsive="screen" :x-gap="16" :y-gap="16">
<n-grid-item v-for="item in buttonExample" :key="item.id">
<n-card :title="item.label" class="min-h-180px">
<p v-if="item.desc" class="pb-16px">{{ item.desc }}</p>
<n-space>
<n-button
v-for="button in item.buttons"
:key="button.id"
v-bind="button.props"
:style="`--icon-margin: ${button.props.circle ? 0 : 6}px`"
>
<template v-if="button.icon" #icon>
<Icon :icon="button.icon" />
</template>
{{ button.label }}
</n-button>
</n-space>
</n-card>
</n-grid-item>
<n-grid-item class="h-180px">
<n-card title="加载中" class="h-full">
<p class="pb-16px">按钮有加载状态</p>
<n-space>
<n-button :loading="loading" type="primary" @click="startLoading">开始加载</n-button>
<n-button @click="endLoading">取消加载</n-button>
</n-space>
</n-card>
</n-grid-item>
</n-grid>
</n-card>
</div>
</template>
<script setup lang="ts">
import { NCard, NGrid, NGridItem, NSpace, NButton } from 'naive-ui';
import type { ButtonProps } from 'naive-ui';
import { Icon } from '@iconify/vue';
import { useLoading } from '@/hooks';
interface ButtonDetail {
id: number;
props: ButtonProps & { href?: string; target?: string };
label?: string;
icon?: string;
}
interface ButtonExample {
id: number;
label: string;
buttons: ButtonDetail[];
desc?: string;
}
const { loading, startLoading, endLoading } = useLoading();
const buttonExample: ButtonExample[] = [
{
id: 0,
label: '基础',
buttons: [
{
id: 0,
props: {},
label: 'Default'
},
{
id: 1,
props: { type: 'tertiary' },
label: 'Tertiary'
},
{
id: 2,
props: { type: 'primary' },
label: 'Primary'
},
{
id: 3,
props: { type: 'info' },
label: 'Info'
},
{
id: 4,
props: { type: 'success' },
label: 'Success'
},
{
id: 5,
props: { type: 'warning' },
label: 'Warning'
},
{
id: 6,
props: { type: 'error' },
label: 'Error'
}
],
desc: '按钮的 type 分别为 default、primary、info、success、warning 和 error。'
},
{
id: 1,
label: '次要按钮',
buttons: [
{
id: 0,
props: { strong: true, secondary: true },
label: 'Default'
},
{
id: 1,
props: { strong: true, secondary: true, type: 'tertiary' },
label: 'Tertiary'
},
{
id: 2,
props: { strong: true, secondary: true, type: 'primary' },
label: 'Primary'
},
{
id: 3,
props: { strong: true, secondary: true, type: 'info' },
label: 'Info'
},
{
id: 4,
props: { strong: true, secondary: true, type: 'success' },
label: 'Success'
},
{
id: 5,
props: { strong: true, secondary: true, type: 'warning' },
label: 'Warning'
},
{
id: 6,
props: { strong: true, secondary: true, type: 'error' },
label: 'Error'
},
{
id: 7,
props: { strong: true, secondary: true, round: true },
label: 'Default'
},
{
id: 8,
props: { strong: true, secondary: true, round: true, type: 'tertiary' },
label: 'Tertiary'
},
{
id: 9,
props: { strong: true, secondary: true, round: true, type: 'primary' },
label: 'Primary'
},
{
id: 10,
props: { strong: true, secondary: true, round: true, type: 'info' },
label: 'Info'
},
{
id: 11,
props: { strong: true, secondary: true, round: true, type: 'success' },
label: 'Success'
},
{
id: 12,
props: { strong: true, secondary: true, round: true, type: 'warning' },
label: 'Warning'
},
{
id: 13,
props: { strong: true, secondary: true, round: true, type: 'error' },
label: 'Error'
}
]
},
{
id: 2,
label: '次次要按钮',
buttons: [
{
id: 0,
props: { tertiary: true },
label: 'Default'
},
{
id: 1,
props: { tertiary: true, type: 'primary' },
label: 'Primary'
},
{
id: 2,
props: { tertiary: true, type: 'info' },
label: 'Info'
},
{
id: 3,
props: { tertiary: true, type: 'success' },
label: 'Success'
},
{
id: 4,
props: { tertiary: true, type: 'warning' },
label: 'Warning'
},
{
id: 5,
props: { tertiary: true, type: 'error' },
label: 'Error'
},
{
id: 6,
props: { tertiary: true, round: true },
label: 'Default'
},
{
id: 7,
props: { tertiary: true, round: true, type: 'primary' },
label: 'Primary'
},
{
id: 8,
props: { tertiary: true, round: true, type: 'info' },
label: 'Info'
},
{
id: 9,
props: { tertiary: true, round: true, type: 'success' },
label: 'Success'
},
{
id: 10,
props: { tertiary: true, round: true, type: 'warning' },
label: 'Warning'
},
{
id: 11,
props: { tertiary: true, round: true, type: 'error' },
label: 'Error'
}
]
},
{
id: 3,
label: '次次次要按钮',
buttons: [
{
id: 0,
props: { quaternary: true },
label: 'Default'
},
{
id: 1,
props: { quaternary: true, type: 'primary' },
label: 'Primary'
},
{
id: 2,
props: { quaternary: true, type: 'info' },
label: 'Info'
},
{
id: 3,
props: { quaternary: true, type: 'success' },
label: 'Success'
},
{
id: 4,
props: { quaternary: true, type: 'warning' },
label: 'Warning'
},
{
id: 5,
props: { quaternary: true, type: 'error' },
label: 'Error'
},
{
id: 6,
props: { quaternary: true, round: true },
label: 'Default'
},
{
id: 7,
props: { quaternary: true, round: true, type: 'primary' },
label: 'Primary'
},
{
id: 8,
props: { quaternary: true, round: true, type: 'info' },
label: 'Info'
},
{
id: 9,
props: { quaternary: true, round: true, type: 'success' },
label: 'Success'
},
{
id: 10,
props: { quaternary: true, round: true, type: 'warning' },
label: 'Warning'
},
{
id: 11,
props: { quaternary: true, round: true, type: 'error' },
label: 'Error'
}
]
},
{
id: 4,
label: '虚线按钮',
buttons: [
{
id: 0,
props: { dashed: true },
label: 'Default'
},
{
id: 1,
props: { dashed: true, type: 'tertiary' },
label: 'Tertiary'
},
{
id: 2,
props: { dashed: true, type: 'primary' },
label: 'Primary'
},
{
id: 3,
props: { dashed: true, type: 'info' },
label: 'Info'
},
{
id: 4,
props: { dashed: true, type: 'success' },
label: 'Success'
},
{
id: 5,
props: { dashed: true, type: 'warning' },
label: 'Warning'
},
{
id: 6,
props: { dashed: true, type: 'error' },
label: 'Error'
}
]
},
{
id: 5,
label: '尺寸',
buttons: [
{
id: 0,
props: { size: 'tiny', strong: true },
label: '小小'
},
{
id: 1,
props: { size: 'small', strong: true },
label: '小'
},
{
id: 2,
props: { size: 'medium', strong: true },
label: '不小'
},
{
id: 3,
props: { size: 'large', strong: true },
label: '不不小'
}
]
},
{
id: 6,
label: '文本按钮',
buttons: [
{
id: 0,
props: { text: true },
label: '那车头依然吐着烟',
icon: 'mdi:train'
}
]
},
{
id: 7,
label: '自定义标签按钮',
buttons: [
{
id: 0,
props: {
text: true,
tag: 'a',
href: 'https://github.com/honghuangdc/soybean-admin',
target: '_blank',
type: 'primary'
},
label: 'soybean-admin'
}
],
desc: '你可以把按钮渲染成不同的标签,比如 a标签 。'
},
{
id: 8,
label: '按钮禁用',
buttons: [
{
id: 0,
props: {
disabled: true
},
label: '不许点'
}
],
desc: '按钮可以被禁用'
},
{
id: 9,
label: '图标按钮',
buttons: [
{
id: 0,
props: {
secondary: true,
strong: true
},
label: '+100元',
icon: 'mdi:cash-100'
},
{
id: 0,
props: {
iconPlacement: 'right',
secondary: true,
strong: true
},
label: '+100元',
icon: 'mdi:cash-100'
}
],
desc: '在按钮上使用图标。'
},
{
id: 10,
label: '不同形状按钮',
buttons: [
{
id: 0,
props: {
circle: true
},
icon: 'mdi:cash-100'
},
{
id: 1,
props: {
round: true
},
label: '圆角'
},
{
id: 2,
props: {},
label: '方'
}
],
desc: '按钮拥有不同的形状。'
},
{
id: 11,
label: '透明背景按钮',
buttons: [
{
id: 0,
props: { ghost: true },
label: 'Default'
},
{
id: 1,
props: { ghost: true, type: 'tertiary' },
label: 'Tertiary'
},
{
id: 2,
props: { ghost: true, type: 'primary' },
label: 'Primary'
},
{
id: 3,
props: { ghost: true, type: 'info' },
label: 'Info'
},
{
id: 4,
props: { ghost: true, type: 'success' },
label: 'Success'
},
{
id: 5,
props: { ghost: true, type: 'warning' },
label: 'Warning'
},
{
id: 6,
props: { ghost: true, type: 'error' },
label: 'Error'
}
],
desc: 'Ghost 按钮有透明的背景。'
},
{
id: 12,
label: '自定义颜色',
buttons: [
{
id: 0,
props: {
color: '#8a2be2'
},
label: '#8a2be2',
icon: 'ic:baseline-color-lens'
},
{
id: 1,
props: {
color: '#ff69b4'
},
label: '#ff69b4',
icon: 'ic:baseline-color-lens'
},
{
id: 2,
props: {
color: '#8a2be2',
ghost: true
},
label: '#8a2be2',
icon: 'ic:baseline-color-lens'
},
{
id: 3,
props: {
color: '#ff69b4',
ghost: true
},
label: '#ff69b4',
icon: 'ic:baseline-color-lens'
},
{
id: 4,
props: {
color: '#8a2be2',
text: true
},
label: '#8a2be2',
icon: 'ic:baseline-color-lens'
},
{
id: 5,
props: {
color: '#ff69b4',
text: true
},
label: '#ff69b4',
icon: 'ic:baseline-color-lens'
}
],
desc: '这两个颜色看起来像毒蘑菇。'
}
];
</script>
<style scoped></style>

View File

@ -1,43 +0,0 @@
<template>
<div>
<n-card title="卡片" class="h-full shadow-sm rounded-16px">
<n-space :vertical="true">
<n-card title="基本用法">
<p class="pb-16px">基础卡片</p>
<n-card title="卡片">卡片内容</n-card>
</n-card>
<n-card title="尺寸">
<p class="pb-16px">卡片有 smallmediumlargehuge 尺寸</p>
<n-space vertical>
<n-card title="小卡片" size="small">卡片内容</n-card>
<n-card title="中卡片" size="medium">卡片内容</n-card>
<n-card title="大卡片" size="large">卡片内容</n-card>
<n-card title="超大卡片" size="huge">卡片内容</n-card>
</n-space>
</n-card>
<n-card title="文本按钮">
<p class="pb-16px">
content footer 可以被 hard soft 分段action 可以被分段分段分割线会在区域的上方出现
</p>
<n-card
title="卡片分段示例"
:segmented="{
content: true,
footer: 'soft'
}"
>
<template #header-extra>#header-extra</template>
卡片内容
<template #footer>#footer</template>
<template #action>#action</template>
</n-card>
</n-card>
</n-space>
</n-card>
</div>
</template>
<script setup lang="ts">
import { NCard, NSpace } from 'naive-ui';
</script>
<style scoped></style>

View File

@ -1,5 +0,0 @@
import ComponentButton from './button/index.vue';
import ComponentCard from './card/index.vue';
import ComponentTable from './table/index.vue';
export { ComponentButton, ComponentCard, ComponentTable };

View File

@ -1,85 +0,0 @@
<template>
<div>
<n-card title="表格" class="h-full shadow-sm rounded-16px">
<n-space :vertical="true">
<n-space>
<n-button @click="getDataSource">有数据</n-button>
<n-button @click="getEmptyDataSource">空数据</n-button>
</n-space>
<loading-empty-wrapper class="h-480px" :loading="loading" :empty="empty">
<n-data-table :columns="columns" :data="dataSource" :flex-height="true" class="h-480px" />
</loading-empty-wrapper>
</n-space>
</n-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { NCard, NSpace, NButton, NDataTable } from 'naive-ui';
import type { DataTableColumn } from 'naive-ui';
import { LoadingEmptyWrapper } from '@/components';
import { useLoadingEmpty } from '@/hooks';
import { getRandomInterger } from '@/utils';
interface DataSource {
name: string;
age: number;
address: string;
}
const { loading, startLoading, endLoading, empty, setEmpty } = useLoadingEmpty();
const columns: DataTableColumn[] = [
{
title: 'Name',
key: 'name',
align: 'center'
},
{
title: 'Age',
key: 'age'
},
{
title: 'Address',
key: 'address'
}
];
const dataSource = ref<DataSource[]>([]);
function createDataSource(): DataSource[] {
return Array(100)
.fill(1)
.map((item, index) => {
return {
name: `Name${index}`,
age: getRandomInterger(30, 20),
address: '中国'
};
});
}
function getDataSource() {
startLoading();
setTimeout(() => {
dataSource.value = createDataSource();
endLoading();
setEmpty(!dataSource.value.length);
}, 1000);
}
function getEmptyDataSource() {
startLoading();
setTimeout(() => {
dataSource.value = [];
endLoading();
setEmpty(!dataSource.value.length);
}, 1000);
}
onMounted(() => {
getDataSource();
});
</script>
<style scoped></style>

View File

@ -1,137 +0,0 @@
<template>
<n-grid :x-gap="16" :y-gap="16" :item-responsive="true" responsive="screen">
<n-grid-item span="s:24 m:8">
<n-card title="时间线" :bordered="false" class="rounded-16px shadow-sm">
<div class="h-360px">
<n-timeline>
<n-timeline-item v-for="item in timelines" :key="item.type" v-bind="item" />
</n-timeline>
</div>
</n-card>
</n-grid-item>
<n-grid-item span="s:24 m:16">
<n-card title="表格" :bordered="false" class="rounded-16px shadow-sm">
<div class="h-360px">
<n-data-table size="small" :columns="columns" :data="tableData" />
</div>
</n-card>
</n-grid-item>
</n-grid>
</template>
<script setup lang="ts">
import { h } from 'vue';
import { NGrid, NGridItem, NCard, NTimeline, NTimelineItem, NDataTable, NTag } from 'naive-ui';
interface TimelineData {
type: 'default' | 'info' | 'success' | 'warning' | 'error';
title: string;
content: string;
time: string;
}
interface TableData {
key: number;
name: string;
age: number;
address: string;
tags: string[];
}
const timelines: TimelineData[] = [
{ type: 'default', title: '啊', content: '', time: '2021-10-10 20:46' },
{ type: 'success', title: '成功', content: '哪里成功', time: '2021-10-10 20:46' },
{ type: 'error', title: '错误', content: '哪里错误', time: '2021-10-10 20:46' },
{ type: 'warning', title: '警告', content: '哪里警告', time: '2021-10-10 20:46' },
{ type: 'info', title: '信息', content: '是的', time: '2021-10-10 20:46' }
];
const columns = [
{
title: 'Name',
key: 'name'
},
{
title: 'Age',
key: 'age'
},
{
title: 'Address',
key: 'address'
},
{
title: 'Tags',
key: 'tags',
render(row: TableData) {
const tags = row.tags.map(tagKey => {
return h(
NTag,
{
style: {
marginRight: '6px'
},
type: 'info'
},
{
default: () => tagKey
}
);
});
return tags;
}
}
];
const tableData: TableData[] = [
{
key: 0,
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
tags: ['nice', 'developer']
},
{
key: 1,
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
tags: ['wow']
},
{
key: 2,
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
tags: ['cool', 'teacher']
},
{
key: 3,
name: 'Soybean',
age: 25,
address: 'China Shenzhen',
tags: ['handsome', 'programmer']
},
{
key: 4,
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
tags: ['nice', 'developer']
},
{
key: 5,
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
tags: ['wow']
},
{
key: 6,
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
tags: ['cool', 'teacher']
}
];
</script>
<style scoped></style>

View File

@ -1,28 +0,0 @@
<template>
<div class="bg-gradient p-16px rounded-16px text-white">
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
/** 渐变开始的颜色 */
startColor?: string;
/** 渐变结束的颜色 */
endColor?: string;
}
const props = withDefaults(defineProps<Props>(), {
startColor: '#56cdf3',
endColor: '#719de3'
});
const gradientStyle = computed(() => `linear-gradient(to bottom right, ${props.startColor}, ${props.endColor})`);
</script>
<style scoped>
.bg-gradient {
background-image: v-bind(gradientStyle);
}
</style>

View File

@ -1,3 +0,0 @@
import GradientBg from './GradientBg.vue';
export { GradientBg };

View File

@ -1,70 +0,0 @@
<template>
<n-grid cols="s:1 m:2 l:4" responsive="screen" :x-gap="16" :y-gap="16">
<n-grid-item v-for="item in cardData" :key="item.id">
<gradient-bg class="h-100px" :start-color="item.colors[0]" :end-color="item.colors[1]">
<h3 class="text-16px">{{ item.title }}</h3>
<div class="flex justify-between pt-12px">
<Icon :icon="item.icon" class="text-32px" />
<count-to
:prefix="item.unit"
:start-value="1"
:end-value="item.value"
class="text-30px text-white dark:text-dark"
/>
</div>
</gradient-bg>
</n-grid-item>
</n-grid>
</template>
<script setup lang="ts">
import { NGrid, NGridItem } from 'naive-ui';
import { Icon } from '@iconify/vue';
import { CountTo } from '@/components';
import { GradientBg } from './components';
interface CardData {
id: string;
title: string;
value: number;
unit: string;
colors: [string, string];
icon: string;
}
const cardData: CardData[] = [
{
id: 'visit',
title: '访问量',
value: 1000000,
unit: '',
colors: ['#ec4786', '#b955a4'],
icon: 'ant-design:bar-chart-outlined'
},
{
id: 'amount',
title: '成交额',
value: 234567.89,
unit: '$',
colors: ['#865ec0', '#5144b4'],
icon: 'ant-design:money-collect-outlined'
},
{
id: 'download',
title: '下载数',
value: 666666,
unit: '',
colors: ['#56cdf3', '#719de3'],
icon: 'carbon:document-download'
},
{
id: 'trade',
title: '成交数',
value: 999999,
unit: '',
colors: ['#fcbc25', '#f68057'],
icon: 'ant-design:trademark-circle-outlined'
}
];
</script>
<style scoped></style>

View File

@ -1,152 +0,0 @@
[
{
"date": "2021/10/1",
"type": "下载量",
"value": 4623
},
{
"date": "2021/10/1",
"type": "注册数",
"value": 2208
},
{
"date": "2021/10/2",
"type": "下载量",
"value": 6145
},
{
"date": "2021/10/2",
"type": "注册数",
"value": 2016
},
{
"date": "2021/10/3",
"type": "下载量",
"value": 508
},
{
"date": "2021/10/3",
"type": "注册数",
"value": 2916
},
{
"date": "2021/10/4",
"type": "下载量",
"value": 6268
},
{
"date": "2021/10/4",
"type": "注册数",
"value": 4512
},
{
"date": "2021/10/5",
"type": "下载量",
"value": 6411
},
{
"date": "2021/10/5",
"type": "注册数",
"value": 8281
},
{
"date": "2021/10/6",
"type": "下载量",
"value": 1890
},
{
"date": "2021/10/6",
"type": "注册数",
"value": 2008
},
{
"date": "2021/10/7",
"type": "下载量",
"value": 4251
},
{
"date": "2021/10/7",
"type": "注册数",
"value": 1963
},
{
"date": "2021/10/8",
"type": "下载量",
"value": 2978
},
{
"date": "2021/10/8",
"type": "注册数",
"value": 2367
},
{
"date": "2021/10/9",
"type": "下载量",
"value": 3880
},
{
"date": "2021/10/9",
"type": "注册数",
"value": 2956
},
{
"date": "2021/10/10",
"type": "下载量",
"value": 3606
},
{
"date": "2021/10/10",
"type": "注册数",
"value": 678
},
{
"date": "2021/10/11",
"type": "下载量",
"value": 4311
},
{
"date": "2021/10/11",
"type": "注册数",
"value": 3188
},
{
"date": "2021/10/12",
"type": "下载量",
"value": 4116
},
{
"date": "2021/10/12",
"type": "注册数",
"value": 3491
},
{
"date": "2021/10/13",
"type": "下载量",
"value": 6419
},
{
"date": "2021/10/13",
"type": "注册数",
"value": 2852
},
{
"date": "2021/10/14",
"type": "下载量",
"value": 1643
},
{
"date": "2021/10/14",
"type": "注册数",
"value": 4788
},
{
"date": "2021/10/15",
"type": "下载量",
"value": 445
},
{
"date": "2021/10/15",
"type": "注册数",
"value": 4319
}
]

View File

@ -1,129 +0,0 @@
<template>
<n-grid :x-gap="16" :y-gap="16" :item-responsive="true" responsive="screen">
<n-grid-item span="s:24 m:16">
<n-card :bordered="false" class="rounded-16px shadow-sm">
<div class="flex w-full h-360px">
<div class="w-200px h-full py-12px">
<h3 class="text-16px font-bold">Dashboard</h3>
<p class="text-[#aaa]">Overview Of Lasted Month</p>
<h3 class="pt-36px text-24px font-bold">
<count-to prefix="$" :start-value="0" :end-value="7754" />
</h3>
<p class="text-[#aaa]">Current Month Earnings</p>
<h3 class="pt-36px text-24px font-bold">
<count-to :start-value="0" :end-value="1234" />
</h3>
<p class="text-[#aaa]">Current Month Sales</p>
<n-button class="mt-24px" type="primary">Last Month Summary</n-button>
</div>
<div class="flex-1-hidden h-full">
<div ref="lineRef" class="wh-full"></div>
</div>
</div>
</n-card>
</n-grid-item>
<n-grid-item span="s:24 m:8">
<n-card :bordered="false" class="rounded-16px shadow-sm">
<div ref="pieRef" class="w-full h-360px"></div>
</n-card>
</n-grid-item>
</n-grid>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { NGrid, NGridItem, NCard, NButton } from 'naive-ui';
import { Line, Pie } from '@antv/g2plot';
import { CountTo } from '@/components';
import data from './data.json';
const lineRef = ref<HTMLElement | null>(null);
const line = ref<Line | null>(null);
const pieRef = ref<HTMLElement | null>(null);
const pie = ref<Pie | null>(null);
function renderLineChart() {
line.value = new Line(lineRef.value!, {
data,
autoFit: true,
xField: 'date',
yField: 'value',
seriesField: 'type',
lineStyle: {
lineWidth: 4
},
area: {
style: {
fill: 'l(270) 0:#ffffff 0.5:#7ec2f3 1:#1890ff'
}
},
smooth: true,
animation: {
appear: {
animation: 'wave-in',
duration: 2000
}
}
});
line.value.render();
}
function renderPieChart() {
const data = [
{ type: '学习', value: 20 },
{ type: '娱乐', value: 10 },
{ type: '工作', value: 30 },
{ type: '休息', value: 40 }
];
pie.value = new Pie(pieRef.value!, {
appendPadding: 10,
data,
angleField: 'value',
colorField: 'type',
radius: 0.8,
innerRadius: 0.65,
meta: {
value: {
formatter: v => `${v}%`
}
},
label: {
type: 'inner',
autoRotate: false,
formatter: ({ percent }) => `${(percent * 100).toFixed(0)}%`
},
statistic: undefined,
pieStyle: {
radius: [20]
},
color: ['#025DF4', '#DB6BCF', '#2498D1', '#FF745A', '#007E99', '#FFA8A8', '#2391FF'],
legend: {
position: 'bottom'
},
interactions: [
{ type: 'element-selected' },
{ type: 'element-active' },
{
type: 'pie-statistic-active',
cfg: {
start: [
{ trigger: 'element:mouseenter', action: 'pie-statistic:change' },
{ trigger: 'legend-item:mouseenter', action: 'pie-statistic:change' }
],
end: [
{ trigger: 'element:mouseleave', action: 'pie-statistic:reset' },
{ trigger: 'legend-item:mouseleave', action: 'pie-statistic:reset' }
]
}
}
]
});
pie.value.render();
}
onMounted(() => {
renderLineChart();
renderPieChart();
});
</script>
<style scoped></style>

View File

@ -1,5 +0,0 @@
import TopChart from './TopChart/index.vue';
import DataCard from './DataCard/index.vue';
import BottomPart from './BottomPart/index.vue';
export { TopChart, DataCard, BottomPart };

View File

@ -1,13 +1,9 @@
<template>
<n-space :vertical="true" :size="16">
<top-chart />
<data-card />
<bottom-part />
</n-space>
<div>
<h3>DashboardAnalysis</h3>
<router-link to="/about">about</router-link>
</div>
</template>
<script lang="ts" setup>
import { NSpace } from 'naive-ui';
import { TopChart, DataCard, BottomPart } from './components';
</script>
<script setup lang="ts"></script>
<style scoped></style>

View File

@ -1,4 +1,4 @@
import DashboardAnalysis from './analysis/index.vue';
import DashboardWorkbench from './workbench/index.vue';
const DashboardAnalysis = () => import('./analysis/index.vue');
const DashboardWorkbench = () => import('./workbench/index.vue');
export { DashboardAnalysis, DashboardWorkbench };

View File

@ -1,48 +0,0 @@
<template>
<n-card :bordered="false" class="rounded-16px shadow-sm">
<div class="flex-y-center justify-between">
<div class="flex-y-center">
<img src="@/assets/svg/avatar/avatar01.svg" alt="" class="w-70px h-70px" />
<div class="pl-12px">
<h3 class="text-18px font-semibold">早安{{ auth.userInfo.userName }}, 今天又是充满活力的一天</h3>
<p class="leading-30px text-[#999]">今日多云转晴20 - 25</p>
</div>
</div>
<n-space :size="24" :wrap="false">
<n-statistic v-for="item in statisticData" :key="item.id" class="whitespace-nowrap" v-bind="item"></n-statistic>
</n-space>
</div>
</n-card>
</template>
<script setup lang="ts">
import { NCard, NSpace, NStatistic } from 'naive-ui';
import { useAuthStore } from '@/store';
interface StatisticData {
id: number;
label: string;
value: string;
}
const auth = useAuthStore();
const statisticData: StatisticData[] = [
{
id: 0,
label: '项目数',
value: '25'
},
{
id: 1,
label: '待办',
value: '4/16'
},
{
id: 2,
label: '消息',
value: '12'
}
];
</script>
<style scoped></style>

View File

@ -1,24 +0,0 @@
<template>
<div
class="flex-col-center p-12px border-1px border-[#efeff5] dark:border-[#ffffff17] rounded-4px hover:shadow-sm cursor-pointer"
>
<Icon :icon="icon" :style="{ color: iconColor }" class="text-30px" />
<p class="py-8px text-16px">{{ label }}</p>
</div>
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue';
interface Props {
/** 快捷操作名称 */
label: string;
/** 图标 */
icon: string;
/** 图标颜色 */
iconColor: string;
}
defineProps<Props>();
</script>
<style scoped></style>

View File

@ -1,41 +0,0 @@
<template>
<div
class="p-4px border-1px border-[#efeff5] dark:border-[#ffffff17] rounded-4px hover:shadow-sm cursor-pointer"
@click="handleOpenSite"
>
<header class="flex-y-center">
<Icon :icon="icon" :style="{ color: iconColor }" class="text-30px" />
<h3 class="pl-12px text-18px font-semibold">{{ name }}</h3>
</header>
<p class="py-8px h-56px text-[#999]">{{ description }}</p>
<div class="flex justify-end">
<span>{{ author }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue';
interface Props {
/** 技术名称 */
name: string;
/** 技术描述 */
description: string;
/** 技术作者 */
author: string;
/** 技术官网 */
site: string;
/** 技术图标 */
icon: string;
/** 图标颜色 */
iconColor?: string;
}
const props = defineProps<Props>();
function handleOpenSite() {
window.open(props.site, '_blank');
}
</script>
<style scoped></style>

View File

@ -1,4 +0,0 @@
import TechnologyCard from './TechnologyCard.vue';
import ShortcutsCard from './ShortcutsCard.vue';
export { TechnologyCard, ShortcutsCard };

View File

@ -1,152 +0,0 @@
<template>
<n-grid :item-responsive="true" responsive="screen" :x-gap="16" :y-gap="16">
<n-grid-item span="s:24 m:16">
<n-space :vertical="true" :size="16">
<n-card title="项目主要技术栈" :bordered="false" size="small" class="shadow-sm rounded-16px">
<template #header-extra>
<a class="text-primary" href="javascript:;">更多技术栈</a>
</template>
<n-grid :item-responsive="true" responsive="screen" cols="m:2 l:3" :x-gap="8" :y-gap="8">
<n-grid-item v-for="item in technology" :key="item.id">
<technology-card v-bind="item" />
</n-grid-item>
</n-grid>
</n-card>
<n-card title="动态" :bordered="false" size="small" class="shadow-sm rounded-16px">
<template #header-extra>
<a class="text-primary" href="javascript:;">更多动态</a>
</template>
<n-list>
<n-list-item v-for="item in activity" :key="item.id">
<template #prefix>
<div class="w-48px h-48px">
<img src="@/assets/svg/avatar/avatar01.svg" alt="" class="wh-full" />
</div>
</template>
<n-thing :title="item.content" :description="item.time" />
</n-list-item>
</n-list>
</n-card>
</n-space>
</n-grid-item>
<n-grid-item span="s:24 m:8">
<n-space :vertical="true" :size="16">
<n-card title="快捷操作" :bordered="false" size="small" class="shadow-sm rounded-16px">
<n-grid :item-responsive="true" responsive="screen" cols="m:2 l:3" :x-gap="8" :y-gap="8">
<n-grid-item v-for="item in shortcuts" :key="item.id">
<shortcuts-card v-bind="item" />
</n-grid-item>
</n-grid>
</n-card>
<n-card title="创意" :bordered="false" size="small" class="shadow-sm rounded-16px">
<n-carousel :autoplay="true" :show-arrow="true">
<banner-svg type="1" />
<banner-svg type="2" />
<banner-svg type="3" />
</n-carousel>
</n-card>
</n-space>
</n-grid-item>
</n-grid>
</template>
<script setup lang="ts">
import { NGrid, NGridItem, NSpace, NCard, NList, NListItem, NThing, NCarousel } from 'naive-ui';
import { BannerSvg } from '@/components';
import { TechnologyCard, ShortcutsCard } from './components';
interface Technology {
id: number;
name: string;
description: string;
author: string;
site: string;
icon: string;
iconColor?: string;
}
interface Activity {
id: number;
content: string;
time: string;
}
interface Shortcuts {
id: number;
label: string;
icon: string;
iconColor: string;
}
const technology: Technology[] = [
{
id: 0,
name: 'Vue',
description: '一套用于构建用户界面的渐进式框架',
author: '尤雨溪 - Evan You',
site: 'https://v3.cn.vuejs.org/',
icon: 'vscode-icons:file-type-vue'
},
{
id: 1,
name: 'TypeScript',
description: 'JavaScript类型的超集它可以编译成纯JavaScript',
author: '微软 - Microsoft',
site: 'https://www.typescriptlang.org/',
icon: 'vscode-icons:file-type-typescript-official'
},
{
id: 2,
name: 'Vite',
description: '下一代前端开发与构建工具',
author: '尤雨溪 - Evan You',
site: 'https://vitejs.cn/',
icon: 'vscode-icons:file-type-vite'
},
{
id: 3,
name: 'NaiveUI',
description: '一个 Vue 3 组件库',
author: '图森未来 - TuSimple',
site: 'https://www.naiveui.com/zh-CN/os-theme',
icon: 'mdi:alpha-n-box-outline',
iconColor: '#5fbc22'
},
{
id: 4,
name: 'WindiCSS',
description: '下一代实用优先的CSS框架',
author: 'Windicss',
site: 'https://windicss.org/',
icon: 'file-icons:windi',
iconColor: '#48b0f1'
},
{
id: 5,
name: 'Pinia',
description: 'vue状态管理框架支持vue2、vue3',
author: 'Posva',
site: 'https://pinia.esm.dev/',
icon: 'mdi:fruit-pineapple',
iconColor: '#fecf48'
}
];
const activity: Activity[] = [
{ id: 4, content: 'Soybean 刚才把工作台页面随便写了一些,凑合能看了!', time: '2021-11-07 22:45:32' },
{ id: 3, content: 'Soybean 正在忙于为soybean-admin写项目说明文档', time: '2021-11-03 20:33:31' },
{ id: 2, content: 'Soybean 准备为soybean-admin 1.0的发布做充分的准备工作!', time: '2021-10-31 22:43:12' },
{ id: 1, content: '@yanbowe 向soybean-admin提交了一个bug多标签栏不会自适应。', time: '2021-10-27 10:24:54' },
{ id: 0, content: 'Soybean 在2021年5月28日创建了开源项目soybean-admin', time: '2021-05-28 22:22:22' }
];
const shortcuts: Shortcuts[] = [
{ id: 0, label: '主控台', icon: 'mdi:desktop-mac-dashboard', iconColor: '#409eff' },
{ id: 1, label: '系统管理', icon: 'ic:outline-settings', iconColor: '#7238d1' },
{ id: 2, label: '权限管理', icon: 'mdi:family-tree', iconColor: '#f56c6c' },
{ id: 3, label: '组件', icon: 'fluent:app-store-24-filled', iconColor: '#19a2f1' },
{ id: 4, label: '表格', icon: 'mdi:table-large', iconColor: '#fab251' },
{ id: 5, label: '图表', icon: 'mdi:chart-areaspline', iconColor: '#8aca6b' }
];
</script>
<style scoped></style>

View File

@ -1,4 +0,0 @@
import WorkbenchHeader from './WorkbenchHeader/index.vue';
import WorkbenchMain from './WorkbenchMain/index.vue';
export { WorkbenchHeader, WorkbenchMain };

View File

@ -1,11 +1,6 @@
<template>
<n-space :vertical="true" :size="16">
<workbench-header />
<workbench-main />
</n-space>
<div>DashboardWorkbench</div>
</template>
<script lang="ts" setup>
import { NSpace } from 'naive-ui';
import { WorkbenchHeader, WorkbenchMain } from './components';
</script>
<script setup lang="ts"></script>
<style scoped></style>

View File

@ -1,5 +0,0 @@
import DocumentVue from './vue/index.vue';
import DocumentVite from './vite/index.vue';
import DocumentNaive from './naive/index.vue';
export { DocumentVue, DocumentVite, DocumentNaive };

View File

@ -1,10 +0,0 @@
<template>
<div>
<iframe class="wh-full" :src="src"></iframe>
</div>
</template>
<script setup lang="ts">
const src = 'https://www.naiveui.com/zh-CN/os-theme/docs/introduction';
</script>
<style scoped></style>

View File

@ -1,10 +0,0 @@
<template>
<div>
<iframe class="wh-full" :src="src"></iframe>
</div>
</template>
<script setup lang="ts">
const src = 'https://cn.vitejs.dev/';
</script>
<style scoped></style>

View File

@ -1,10 +0,0 @@
<template>
<div>
<iframe class="wh-full" :src="src"></iframe>
</div>
</template>
<script setup lang="ts">
const src = 'https://v3.cn.vuejs.org/';
</script>
<style scoped></style>

View File

@ -1,8 +1,4 @@
export * from './system';
export * from './dashboard';
export * from './document';
export * from './plugin';
export * from './component';
export * from './multi-menu';
export * from './about';
export * from './website';
export * from './multi-menu';

View File

@ -1,10 +1,6 @@
<template>
<div>
<n-card title="多级菜单-二级菜单" class="h-full shadow-sm rounded-16px"></n-card>
</div>
<div>多级菜单</div>
</template>
<script setup lang="ts">
import { NCard } from 'naive-ui';
</script>
<script setup lang="ts"></script>
<style scoped></style>

View File

@ -1,3 +1,3 @@
import MultiMenuFirstSecond from './first/second/index.vue';
const MultiMenuFirstSecond = () => import('./first/second/index.vue');
export { MultiMenuFirstSecond };

View File

@ -1,34 +0,0 @@
<template>
<div>
<n-card title="文本复制" class="h-full shadow-sm rounded-16px">
<n-input-group>
<n-input v-model:value="source" placeholder="请输入要复制的内容吧" />
<n-button type="primary" @click="handleCopy">复制</n-button>
</n-input-group>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { NCard, NInputGroup, NInput, NButton, useMessage } from 'naive-ui';
import { useClipboard } from '@vueuse/core';
const source = ref('');
const message = useMessage();
const { copy, isSupported } = useClipboard();
function handleCopy() {
if (!isSupported) {
message.error('您的浏览器不支持Clipboard API');
return;
}
if (!source.value) {
message.error('请输入要复制的内容');
return;
}
copy(source.value);
message.success(`复制成功:${source.value}`);
}
</script>
<style scoped></style>

View File

@ -1,46 +0,0 @@
<template>
<div>
<n-card title="markdown插件" class="shadow-sm rounded-16px">
<div ref="domRef"></div>
<template #footer>
<github-link link="https://github.com/Vanessa219/vditor" />
</template>
</n-card>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
import { NCard } from 'naive-ui';
import Vditor from 'vditor';
import 'vditor/src/assets/scss/index.scss';
import { useThemeStore } from '@/store';
import { GithubLink } from '@/components';
const theme = useThemeStore();
const vditor = ref<Vditor | null>(null);
const domRef = ref<HTMLElement | null>(null);
function renderVditor() {
vditor.value = new Vditor(domRef.value!, {
minHeight: 400,
theme: theme.darkMode ? 'dark' : 'classic',
icon: 'material',
cache: { enable: false }
});
}
watch(
() => theme.darkMode,
newValue => {
const themeMode = newValue ? 'dark' : 'classic';
vditor.value?.setTheme(themeMode);
}
);
onMounted(() => {
renderVditor();
});
</script>
<style scoped></style>

View File

@ -1,44 +0,0 @@
<template>
<div>
<n-card title="富文本插件" class="shadow-sm rounded-16px">
<div ref="domRef" class="bg-white dark:bg-dark"></div>
<template #footer>
<github-link link="https://github.com/wangeditor-team/wangEditor" />
</template>
</n-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { NCard } from 'naive-ui';
import WangEditor from 'wangeditor';
import { GithubLink } from '@/components';
const editor = ref<WangEditor | null>(null);
const domRef = ref<HTMLElement | null>(null);
function renderWangEditor() {
editor.value = new WangEditor(domRef.value);
setEditorConfig();
editor.value.create();
}
function setEditorConfig() {
editor.value!.config.zIndex = 10;
}
onMounted(() => {
renderWangEditor();
});
</script>
<style scoped>
:deep(.w-e-toolbar) {
background: inherit !important;
border-color: #999 !important;
}
:deep(.w-e-text-container) {
background: inherit;
border-color: #999 !important;
}
</style>

View File

@ -1,32 +0,0 @@
export const icons = [
'mdi:emoticon',
'mdi:ab-testing',
'ph:alarm',
'ph:android-logo',
'ph:align-bottom',
'ph:archive-box-light',
'uil:basketball',
'uil:brightness-plus',
'uil:capture',
'mdi:apps-box',
'mdi:alert',
'mdi:airballoon',
'mdi:airplane-edit',
'mdi:alpha-f-box-outline',
'mdi:arm-flex-outline',
'ic:baseline-10mp',
'ic:baseline-access-time',
'ic:baseline-brightness-4',
'ic:baseline-brightness-5',
'ic:baseline-credit-card',
'ic:baseline-filter-1',
'ic:baseline-filter-2',
'ic:baseline-filter-3',
'ic:baseline-filter-4',
'ic:baseline-filter-5',
'ic:baseline-filter-6',
'ic:baseline-filter-7',
'ic:baseline-filter-8',
'ic:baseline-filter-9',
'ic:baseline-filter-9-plus'
];

View File

@ -1,31 +0,0 @@
<template>
<div>
<n-card title="Icon组件示例" class="shadow-sm rounded-16px">
<div class="grid grid-cols-10">
<template v-for="item in icons" :key="item">
<div class="mt-5px flex-x-center">
<Icon :icon="item" class="text-30px" />
</div>
</template>
</div>
<div class="mt-50px">
<h1 class="mb-20px text-18px font-500">Icon图标选择器</h1>
<icon-select v-model:value="selectVal" :icons="icons" />
</div>
<template #footer>
<web-site-link label="iconify地址" link="https://icones.js.org/" class="mt-10px" />
</template>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { NCard } from 'naive-ui';
import { Icon } from '@iconify/vue';
import { IconSelect, WebSiteLink } from '@/components';
import { icons } from './icons';
const selectVal = ref('');
</script>
<style scoped></style>

View File

@ -1,19 +0,0 @@
import PluginMap from './map/index.vue';
import PluginVideo from './video/index.vue';
import PluginEditorQuill from './editor/quill/index.vue';
import PluginEditorMarkdown from './editor/markdown/index.vue';
import PluginSwiper from './swiper/index.vue';
import PluginCopy from './copy/index.vue';
import PluginIcon from './icon/index.vue';
import PluginPrint from './print/index.vue';
export {
PluginMap,
PluginVideo,
PluginEditorQuill,
PluginEditorMarkdown,
PluginSwiper,
PluginCopy,
PluginIcon,
PluginPrint
};

View File

@ -1,26 +0,0 @@
<template>
<div ref="domRef" class="w-full h-full"></div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useScriptTag } from '@vueuse/core';
import { BAIDU_MAP_SDK_URL } from '@/config';
const { load } = useScriptTag(BAIDU_MAP_SDK_URL);
const domRef = ref<HTMLDivElement | null>(null);
async function renderBaiduMap() {
await load(true);
const map = new BMap.Map(domRef.value!);
const point = new BMap.Point(114.05834626586915, 22.546789983033168);
map.centerAndZoom(point, 15);
map.enableScrollWheelZoom();
}
onMounted(() => {
renderBaiduMap();
});
</script>
<style scoped></style>

View File

@ -1,29 +0,0 @@
<template>
<div ref="domRef" class="w-full h-full"></div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useScriptTag } from '@vueuse/core';
import { GAODE_MAP_SDK_URL } from '@/config';
const { load } = useScriptTag(GAODE_MAP_SDK_URL);
const domRef = ref<HTMLDivElement | null>(null);
async function renderBaiduMap() {
await load(true);
const map = new AMap.Map(domRef.value!, {
zoom: 11,
center: [114.05834626586915, 22.546789983033168],
viewMode: '3D'
});
return map;
}
onMounted(() => {
renderBaiduMap();
});
</script>
<style scoped></style>

View File

@ -1,29 +0,0 @@
<template>
<div ref="domRef"></div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useScriptTag } from '@vueuse/core';
import { TENCENT_MAP_SDK_URL } from '@/config';
const { load } = useScriptTag(TENCENT_MAP_SDK_URL);
const domRef = ref<HTMLDivElement | null>(null);
async function renderBaiduMap() {
await load(true);
const map = new TMap.Map(domRef.value!, {
center: new TMap.LatLng(39.98412, 116.307484),
zoom: 11,
viewMode: '3D'
});
return map;
}
onMounted(() => {
renderBaiduMap();
});
</script>
<style scoped></style>

View File

@ -1,5 +0,0 @@
import BaiduMap from './BaiduMap.vue';
import GaodeMap from './GaodeMap.vue';
import TencentMap from './TencentMap.vue';
export { BaiduMap, GaodeMap, TencentMap };

View File

@ -1,29 +0,0 @@
<template>
<div>
<n-card title="地图插件" class="h-full shadow-sm rounded-16px" content-style="overflow:hidden">
<n-tabs type="line" class="flex-col-stretch h-full" pane-class="flex-1-hidden">
<n-tab-pane v-for="item in maps" :key="item.id" :name="item.id" :tab="item.label">
<component :is="item.component" />
</n-tab-pane>
</n-tabs>
</n-card>
</div>
</template>
<script setup lang="ts">
import type { Component } from 'vue';
import { NCard, NTabs, NTabPane } from 'naive-ui';
import { GaodeMap, TencentMap } from './components';
interface Map {
id: string;
label: string;
component: Component;
}
const maps: Map[] = [
{ id: 'gaode', label: '高德地图', component: GaodeMap },
{ id: 'tencent', label: '腾讯地图', component: TencentMap }
];
</script>
<style scoped></style>

View File

@ -1,40 +0,0 @@
<template>
<div>
<n-card title="打印" class="shadow-sm rounded-16px">
<n-button type="primary" class="mr-10px" @click="printTable">打印表格</n-button>
<n-button type="primary" @click="printImage">打印图片</n-button>
<template #footer>
<github-link label="printJS" link="https://github.com/crabbly/Print.js" class="mt-10px" />
</template>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { NCard, NButton } from 'naive-ui';
import printJS from 'print-js';
import { GithubLink } from '@/components';
function printTable() {
printJS({
printable: [
{ name: 'soybean', wechat: 'honghuangdc', remark: '欢迎来技术交流' },
{ name: 'soybean', wechat: 'honghuangdc', remark: '欢迎来技术交流' }
],
properties: ['name', 'wechat', 'remark'],
type: 'json'
});
}
function printImage() {
printJS({
printable: [
'https://raw.githubusercontent.com/honghuangdc/project-assets/main/img/qq_qrcode.JPG',
'https://raw.githubusercontent.com/honghuangdc/project-assets/main/img/qq_qrcode.JPG'
],
type: 'image',
header: 'Multiple Images',
imageStyle: 'width:100%;'
});
}
</script>
<style scoped></style>

View File

@ -1,118 +0,0 @@
<template>
<div>
<n-card title="Swiper插件" class="shadow-sm rounded-16px">
<n-space :vertical="true">
<github-link link="https://github.com/nolimits4web/swiper" />
<web-site-link label="vue3版文档地址" link="https://swiperjs.com/vue" />
<web-site-link label="插件demo地址" link="https://swiperjs.com/demos" />
</n-space>
<n-space :vertical="true">
<div v-for="item in swiperExample" :key="item.id">
<h3 class="py-24px text-24px font-bold">{{ item.label }}</h3>
<swiper v-bind="item.options">
<swiper-slide v-for="i in 5" :key="i">
<div class="flex-center h-240px border-1px border-[#999] text-18px font-bold">Slide{{ i }}</div>
</swiper-slide>
</swiper>
</div>
</n-space>
</n-card>
</div>
</template>
<script setup lang="ts">
import { NCard, NSpace } from 'naive-ui';
import SwiperCore, { Navigation, Pagination } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/vue';
import type { SwiperOptions } from 'swiper';
import { WebSiteLink, GithubLink } from '@/components';
type SwiperExampleOptions = Pick<
SwiperOptions,
| 'navigation'
| 'pagination'
| 'scrollbar'
| 'slidesPerView'
| 'slidesPerGroup'
| 'spaceBetween'
| 'direction'
| 'loop'
| 'loopFillGroupWithBlank'
>;
interface SwiperExample {
id: number;
label: string;
options: Partial<SwiperExampleOptions>;
}
SwiperCore.use([Navigation, Pagination]);
const swiperExample: SwiperExample[] = [
{ id: 0, label: 'Default', options: {} },
{
id: 1,
label: 'Navigation',
options: {
navigation: true
}
},
{
id: 2,
label: 'Pagination',
options: {
pagination: true
}
},
{
id: 3,
label: 'Pagination dynamic',
options: {
pagination: { dynamicBullets: true }
}
},
{
id: 4,
label: 'Pagination progress',
options: {
navigation: true,
pagination: {
type: 'progressbar'
}
}
},
{
id: 5,
label: 'Pagination fraction',
options: {
navigation: true,
pagination: {
type: 'fraction'
}
}
},
{
id: 6,
label: 'Slides per view',
options: {
pagination: {
clickable: true
},
slidesPerView: 3,
spaceBetween: 30
}
},
{
id: 7,
label: 'Infinite loop',
options: {
navigation: true,
pagination: {
clickable: true
},
loop: true
}
}
];
</script>
<style scoped></style>

View File

@ -1,36 +0,0 @@
<template>
<div>
<n-card title="视频播放器插件" class="h-full shadow-sm rounded-16px">
<div ref="domRef"></div>
</n-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { NCard } from 'naive-ui';
import Player from 'xgplayer';
const domRef = ref<HTMLElement | null>(null);
const player = ref<Player | null>(null);
function renderXgPlayer() {
const url = 'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/byted-player-videos/1.0.0/xgplayer-demo.mp4';
player.value = new Player({
el: domRef.value!,
url,
playbackRate: [0.5, 0.75, 1, 1.5, 2]
});
}
function destroyXgPlayer() {
player.value?.destroy();
}
onMounted(() => {
renderXgPlayer();
});
onUnmounted(() => {
destroyXgPlayer();
});
</script>
<style scoped></style>

View File

@ -1,8 +1,6 @@
<template>
<exception-base type="403" />
<div></div>
</template>
<script lang="ts" setup>
import { ExceptionBase } from './components';
</script>
<script setup lang="ts"></script>
<style scoped></style>

View File

@ -1,8 +1,6 @@
<template>
<exception-base type="404" />
<div>NotFound</div>
</template>
<script lang="ts" setup>
import { ExceptionBase } from './components';
</script>
<script setup lang="ts"></script>
<style scoped></style>

View File

@ -1,8 +1,6 @@
<template>
<exception-base type="500" />
<div>NotFound</div>
</template>
<script lang="ts" setup>
import { ExceptionBase } from './components';
</script>
<script setup lang="ts"></script>
<style scoped></style>

View File

@ -1,26 +0,0 @@
<template>
<div class="flex-col-center wh-full">
<div class="w-400px h-400px text-primary">
<svg-no-permission v-if="type === '403'" />
<svg-not-found v-if="type === '404'" />
<svg-service-error v-if="type === '500'" />
</div>
<router-link :to="ROUTE_HOME.path">
<n-button type="primary">回到首页</n-button>
</router-link>
</div>
</template>
<script lang="ts" setup>
import { NButton } from 'naive-ui';
import { SvgNoPermission, SvgNotFound, SvgServiceError } from '@/components';
import { ROUTE_HOME } from '@/router';
interface Props {
/** 异常类型 */
type: '403' | '404' | '500';
}
defineProps<Props>();
</script>
<style scoped></style>

View File

@ -1,3 +0,0 @@
import ExceptionBase from './ExceptionBase.vue';
export { ExceptionBase };

View File

@ -1,6 +1,6 @@
import Login from './login/index.vue';
import NoPermission from './exception/403.vue';
import NotFound from './exception/404.vue';
import ServiceError from './exception/500.vue';
const Login = () => import('./login/index.vue');
const NoPermission = () => import('./exception/403.vue');
const NotFound = () => import('./exception/404.vue');
const ServiceError = () => import('./exception/500.vue');
export { Login, NoPermission, NotFound, ServiceError };

View File

@ -1,6 +0,0 @@
<template>
<div></div>
</template>
<script lang="ts" setup></script>
<style scoped></style>

View File

@ -1,76 +0,0 @@
<template>
<div class="pt-24px">
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<n-form-item path="phone">
<n-input v-model:value="model.phone" placeholder="手机号码" />
</n-form-item>
<n-form-item path="code">
<div class="flex-y-center w-full">
<n-input v-model:value="model.code" placeholder="验证码" />
<div class="w-18px"></div>
<n-button size="large" :disabled="isCounting" @click="handleSmsCode">{{ label }}</n-button>
</div>
</n-form-item>
<n-form-item path="imgCode">
<n-input v-model:value="model.imgCode" placeholder="验证码,点击图片刷新" />
<div class="pl-8px">
<image-verify v-model:code="imgCode" />
</div>
</n-form-item>
<n-space :vertical="true" size="large">
<login-agreement v-model:value="agreement" class="pb-12px" />
<n-button type="primary" size="large" :block="true" :round="true" :loading="loading" @click="handleSubmit">
确定
</n-button>
<n-button size="large" :block="true" :round="true" @click="toCurrentLogin('pwd-login')">返回</n-button>
</n-space>
</n-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { NForm, NFormItem, NInput, NSpace, NButton } from 'naive-ui';
import type { FormInst } from 'naive-ui';
import { ImageVerify } from '@/components';
import { useRouterPush, useLogin } from '@/composables';
import { useSmsCode, useAgreement } from '@/hooks';
import { formRules, getImgCodeRule } from '@/utils';
import { LoginAgreement } from '../common';
const { toCurrentLogin } = useRouterPush();
const { loading, login } = useLogin();
const { label, isCounting, getSmsCode } = useSmsCode();
const { agreement, isAgree } = useAgreement();
const formRef = ref<(HTMLElement & FormInst) | null>(null);
const model = reactive({
phone: '',
code: '',
imgCode: ''
});
const imgCode = ref('');
const rules = {
phone: formRules.phone,
code: formRules.code,
imgCode: getImgCodeRule(imgCode)
};
function handleSmsCode() {
getSmsCode(model.phone);
}
function handleSubmit(e: MouseEvent) {
if (!formRef.value) return;
e.preventDefault();
formRef.value.validate(errors => {
if (!errors) {
if (!isAgree()) return;
const { phone, code } = model;
login({ phone, pwdOrCode: code, type: 'sms' });
}
});
}
</script>
<style scoped></style>

View File

@ -1,13 +0,0 @@
<template>
<n-space :vertical="true">
<n-divider class="!mb-0 text-14px text-[#666]">其他登录方式</n-divider>
<div class="flex-center">
<icon-mdi-wechat class="text-22px text-[#888] hover:text-[#52BF5E] cursor-pointer" />
</div>
</n-space>
</template>
<script lang="ts" setup>
import { NSpace, NDivider } from 'naive-ui';
</script>
<style scoped></style>

View File

@ -1,3 +0,0 @@
import OtherLogin from './OtherLogin.vue';
export { OtherLogin };

View File

@ -1,73 +0,0 @@
<template>
<div class="pt-24px">
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<n-form-item path="phone">
<n-input v-model:value="model.phone" placeholder="请输入手机号码" />
</n-form-item>
<n-form-item path="pwd">
<n-input v-model:value="model.pwd" type="password" show-password-on="click" placeholder="请输入密码" />
</n-form-item>
<n-space :vertical="true" :size="24">
<div class="flex-y-center justify-between">
<n-checkbox v-model:checked="rememberMe">记住我</n-checkbox>
<span class="text-primary cursor-pointer" @click="toCurrentLogin('reset-pwd')">忘记密码</span>
</div>
<login-agreement v-model:value="agreement" />
<n-button type="primary" size="large" :block="true" :round="true" :loading="loading" @click="handleSubmit">
确定
</n-button>
<div class="flex-y-center justify-between">
<n-button class="flex-1" :block="true" @click="toCurrentLogin('code-login')">
{{ EnumLoginModule['code-login'] }}
</n-button>
<div class="w-12px"></div>
<n-button class="flex-1" :block="true" @click="toCurrentLogin('register')">
{{ EnumLoginModule.register }}
</n-button>
</div>
</n-space>
</n-form>
<other-login />
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { NForm, NFormItem, NInput, NSpace, NCheckbox, NButton } from 'naive-ui';
import type { FormInst, FormRules } from 'naive-ui';
import { EnumLoginModule } from '@/enum';
import { useRouterPush, useLogin } from '@/composables';
import { useAgreement } from '@/hooks';
import { formRules } from '@/utils';
import { OtherLogin } from './components';
import { LoginAgreement } from '../common';
const { toCurrentLogin } = useRouterPush();
const { loading, login } = useLogin();
const { agreement, isAgree } = useAgreement();
const formRef = ref<(HTMLElement & FormInst) | null>(null);
const model = reactive({
phone: '15170283876',
pwd: 'a123456789'
});
const rules: FormRules = {
phone: formRules.phone,
pwd: formRules.pwd
};
const rememberMe = ref(false);
function handleSubmit(e: MouseEvent) {
if (!formRef.value) return;
e.preventDefault();
formRef.value.validate(errors => {
if (!errors) {
if (!isAgree()) return;
const { phone, pwd } = model;
login({ phone, pwdOrCode: pwd, type: 'pwd' });
}
});
}
</script>
<style scoped></style>

View File

@ -1,88 +0,0 @@
<template>
<div class="pt-24px">
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<n-form-item path="phone">
<n-input v-model:value="model.phone" placeholder="手机号码" />
</n-form-item>
<n-form-item path="code">
<div class="flex-y-center w-full">
<n-input v-model:value="model.code" placeholder="验证码" />
<div class="w-18px"></div>
<n-button size="large" :disabled="isCounting" @click="handleSmsCode">{{ label }}</n-button>
</div>
</n-form-item>
<n-form-item path="pwd">
<n-input v-model:value="model.pwd" placeholder="密码" />
</n-form-item>
<n-form-item path="confirmPwd">
<n-input v-model:value="model.confirmPwd" placeholder="确认密码" />
</n-form-item>
<n-space :vertical="true" size="large">
<n-checkbox v-model:checked="agreement">我已经仔细阅读并接受用户协议和隐私政策</n-checkbox>
<n-button type="primary" size="large" :block="true" :round="true" @click="handleSubmit">确定</n-button>
<n-button size="large" :block="true" :round="true" @click="toCurrentLogin('pwd-login')">返回</n-button>
</n-space>
</n-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { NForm, NFormItem, NInput, NSpace, NCheckbox, NButton, useMessage } from 'naive-ui';
import type { FormInst } from 'naive-ui';
import { useRouterPush } from '@/composables';
import { useSmsCode } from '@/hooks';
const message = useMessage();
const { toCurrentLogin } = useRouterPush();
const { label, isCounting, start } = useSmsCode();
const formRef = ref<(HTMLElement & FormInst) | null>(null);
const model = reactive({
phone: '',
code: '',
pwd: '',
confirmPwd: ''
});
const rules = {
phone: {
required: true,
trigger: ['blur', 'input'],
message: '请输入手机号'
},
code: {
required: true,
trigger: ['blur', 'input'],
message: '请输入验证码'
},
pwd: {
required: true,
trigger: ['blur', 'input'],
message: '请输入密码'
},
confirmPwd: {
required: true,
trigger: ['blur', 'input'],
message: '请输入确认密码'
}
};
const agreement = ref(false);
function handleSmsCode() {
start();
}
function handleSubmit(e: MouseEvent) {
if (!formRef.value) return;
e.preventDefault();
formRef.value.validate(errors => {
if (!errors) {
message.success('验证成功');
} else {
message.error('验证失败');
}
});
}
</script>
<style scoped></style>

View File

@ -1,86 +0,0 @@
<template>
<div class="pt-24px">
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<n-form-item path="phone">
<n-input v-model:value="model.phone" placeholder="手机号码" />
</n-form-item>
<n-form-item path="code">
<div class="flex-y-center w-full">
<n-input v-model:value="model.code" placeholder="验证码" />
<div class="w-18px"></div>
<n-button size="large" :disabled="isCounting" @click="handleSmsCode">{{ label }}</n-button>
</div>
</n-form-item>
<n-form-item path="pwd">
<n-input v-model:value="model.pwd" placeholder="密码" />
</n-form-item>
<n-form-item path="confirmPwd">
<n-input v-model:value="model.confirmPwd" placeholder="确认密码" />
</n-form-item>
<n-space :vertical="true" size="large">
<n-button type="primary" size="large" :block="true" :round="true" @click="handleSubmit">确定</n-button>
<n-button size="large" :block="true" :round="true" @click="toCurrentLogin('pwd-login')">返回</n-button>
</n-space>
</n-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { NForm, NFormItem, NInput, NSpace, NButton, useMessage } from 'naive-ui';
import type { FormInst } from 'naive-ui';
import { useRouterPush } from '@/composables';
import { useSmsCode } from '@/hooks';
const message = useMessage();
const { toCurrentLogin } = useRouterPush();
const { label, isCounting, start } = useSmsCode();
const formRef = ref<(HTMLElement & FormInst) | null>(null);
const model = reactive({
phone: '',
code: '',
pwd: '',
confirmPwd: ''
});
const rules = {
phone: {
required: true,
trigger: ['blur', 'input'],
message: '请输入手机号'
},
code: {
required: true,
trigger: ['blur', 'input'],
message: '请输入验证码'
},
pwd: {
required: true,
trigger: ['blur', 'input'],
message: '请输入密码'
},
confirmPwd: {
required: true,
trigger: ['blur', 'input'],
message: '请输入确认密码'
}
};
function handleSmsCode() {
start();
}
function handleSubmit(e: MouseEvent) {
if (!formRef.value) return;
e.preventDefault();
formRef.value.validate(errors => {
if (!errors) {
message.success('验证成功');
} else {
message.error('验证失败');
}
});
}
</script>
<style scoped></style>

View File

@ -1,41 +0,0 @@
<template>
<div class="w-full text-[14px]">
<n-checkbox v-model:checked="checked">我已经仔细阅读并接受</n-checkbox>
<n-button :text="true" type="primary">用户协议</n-button>
<n-button :text="true" type="primary">隐私权政策</n-button>
</div>
</template>
<script setup lang="ts">
import { watch } from 'vue';
import { NCheckbox, NButton } from 'naive-ui';
import { useBoolean } from '@/hooks';
interface Props {
/** 是否选中 */
value: boolean;
}
interface Emits {
(e: 'update:value', value: boolean): void;
}
const props = withDefaults(defineProps<Props>(), {
value: true
});
const emit = defineEmits<Emits>();
const { bool: checked, setBool } = useBoolean(props.value);
watch(
() => props.value,
newValue => {
setBool(newValue);
}
);
watch(checked, newValue => {
emit('update:value', newValue);
});
</script>
<style scoped></style>

View File

@ -1,3 +0,0 @@
import LoginAgreement from './LoginAgreement.vue';
export { LoginAgreement };

View File

@ -1,7 +0,0 @@
import PwdLogin from './PwdLogin/index.vue';
import CodeLogin from './CodeLogin/index.vue';
import Register from './Register/index.vue';
import ResetPwd from './ResetPwd/index.vue';
import BindWechat from './BindWechat/index.vue';
export { PwdLogin, CodeLogin, Register, ResetPwd, BindWechat };

View File

@ -1,76 +1,6 @@
<template>
<div class="login-bg relative flex-center wh-full">
<n-card :bordered="false" size="large" class="z-20 !w-auto rounded-20px shadow-sm">
<div class="w-360px">
<header class="flex-y-center justify-between">
<div class="w-70px h-70px rounded-35px overflow-hidden">
<system-logo class="wh-full" :fill="true" :color="theme.themeColor" />
</div>
<n-gradient-text type="primary" :size="28">{{ title }}</n-gradient-text>
</header>
<main class="pt-24px">
<h3 class="text-18px text-primary font-medium">{{ activeModule.label }}</h3>
<component :is="activeModule.component" />
</main>
</div>
</n-card>
<login-bg :theme-color="theme.themeColor" />
</div>
<div></div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import type { Component } from 'vue';
import { NCard, NGradientText } from 'naive-ui';
import { SystemLogo, LoginBg } from '@/components';
import { useAppInfo } from '@/composables';
import { EnumLoginModule } from '@/enum';
import { mixColor } from '@/utils';
import type { LoginModuleType } from '@/interface';
import { PwdLogin, CodeLogin, Register, ResetPwd, BindWechat } from './components';
import { useThemeStore } from '@/store';
interface Props {
/** 登录模块分类 */
module: LoginModuleType;
}
interface LoginModule {
key: LoginModuleType;
label: string;
component: Component;
}
const props = defineProps<Props>();
const theme = useThemeStore();
const { title } = useAppInfo();
const modules: LoginModule[] = [
{ key: 'pwd-login', label: EnumLoginModule['pwd-login'], component: PwdLogin },
{ key: 'code-login', label: EnumLoginModule['code-login'], component: CodeLogin },
{ key: 'register', label: EnumLoginModule.register, component: Register },
{ key: 'reset-pwd', label: EnumLoginModule['reset-pwd'], component: ResetPwd },
{ key: 'bind-wechat', label: EnumLoginModule['bind-wechat'], component: BindWechat }
];
const activeModule = computed(() => {
const active: LoginModule = { ...modules[0] };
const findItem = modules.find(item => item.key === props.module);
if (findItem) {
Object.assign(active, findItem);
}
return active;
});
const bgColor = computed(() => {
const COLOR_WHITE = '#ffffff';
const ratio = theme.darkMode ? 0.6 : 0.3;
return mixColor(COLOR_WHITE, theme.themeColor, ratio);
});
</script>
<style scoped>
.login-bg {
background-color: v-bind(bgColor);
}
</style>
<script setup lang="ts"></script>
<style scoped></style>

View File

@ -1,19 +0,0 @@
<template>
<header class="flex-y-center justify-between h-78px px-24px bg-light dark:bg-dark shadow-sm">
<div class="flex-y-center h-full">
<system-logo class="w-48px h-48px mr-12px" :color="theme.themeColor" />
<n-gradient-text size="32">Soybean 网址导航</n-gradient-text>
</div>
<theme-switch :dark="theme.darkMode" @update="handleDarkMode" />
</header>
</template>
<script setup lang="ts">
import { NGradientText } from 'naive-ui';
import { SystemLogo, ThemeSwitch } from '@/components';
import { useThemeStore } from '@/store';
const theme = useThemeStore();
const { handleDarkMode } = useThemeStore();
</script>
<style scoped></style>

View File

@ -1,3 +0,0 @@
import WebsiteHeader from './WebsiteHeader.vue';
export { WebsiteHeader };

View File

@ -1,3 +0,0 @@
import Website from './index.vue';
export { Website };

View File

@ -1,12 +0,0 @@
<template>
<div>
<website-header />
<n-space :vertical="true"></n-space>
</div>
</template>
<script setup lang="ts">
import { NSpace } from 'naive-ui';
import { WebsiteHeader } from './components';
</script>
<style scoped></style>