1
0
mirror of https://github.com/fumiama/paper-manager.git synced 2026-06-11 03:20:24 +08:00

add frontend/vben from vben-admin-thin

This commit is contained in:
源文雨
2023-03-10 17:18:32 +08:00
parent 30cd57ef76
commit 2a0fdeae31
469 changed files with 42028 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
<template>
<div class="md:flex">
<template>
<div>1</div>
</template>
</div>
</template>
<script lang="ts" setup>
defineProps({
loading: {
type: Boolean,
},
})
</script>

View File

@@ -0,0 +1,64 @@
<template>
<Card title="成交占比" :loading="loading">
<div ref="chartRef" :style="{ width, height }"></div>
</Card>
</template>
<script lang="ts" setup>
import { Ref, ref, watch } from 'vue'
import { Card } from 'ant-design-vue'
import { useECharts } from '/@/hooks/web/useECharts'
const props = defineProps({
loading: Boolean,
width: {
type: String as PropType<string>,
default: '100%',
},
height: {
type: String as PropType<string>,
default: '300px',
},
})
const chartRef = ref<HTMLDivElement | null>(null)
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
watch(
() => props.loading,
() => {
if (props.loading) {
return
}
setOptions({
tooltip: {
trigger: 'item',
},
series: [
{
name: '访问来源',
type: 'pie',
radius: '80%',
center: ['50%', '50%'],
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
data: [
{ value: 500, name: '电子产品' },
{ value: 310, name: '服装' },
{ value: 274, name: '化妆品' },
{ value: 400, name: '家居' },
].sort(function (a, b) {
return a.value - b.value
}),
roseType: 'radius',
animationType: 'scale',
animationEasing: 'exponentialInOut',
animationDelay: function () {
return Math.random() * 400
},
},
],
})
},
{ immediate: true },
)
</script>

View File

@@ -0,0 +1,38 @@
<template>
<Card
:tab-list="tabListTitle"
v-bind="$attrs"
:active-tab-key="activeKey"
@tab-change="onTabChange"
>
<p v-if="activeKey === 'tab1'">
<VisitAnalysis />
</p>
<p v-if="activeKey === 'tab2'">
<VisitAnalysisBar />
</p>
</Card>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { Card } from 'ant-design-vue'
import VisitAnalysis from './VisitAnalysis.vue'
import VisitAnalysisBar from './VisitAnalysisBar.vue'
const activeKey = ref('tab1')
const tabListTitle = [
{
key: 'tab1',
tab: '流量趋势',
},
{
key: 'tab2',
tab: '访问量',
},
]
function onTabChange(key) {
activeKey.value = key
}
</script>

View File

@@ -0,0 +1,89 @@
<template>
<div ref="chartRef" :style="{ height, width }"></div>
</template>
<script lang="ts">
import { basicProps } from './props'
</script>
<script lang="ts" setup>
import { onMounted, ref, Ref } from 'vue'
import { useECharts } from '/@/hooks/web/useECharts'
defineProps({
...basicProps,
})
const chartRef = ref<HTMLDivElement | null>(null)
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
onMounted(() => {
setOptions({
tooltip: {
trigger: 'axis',
axisPointer: {
lineStyle: {
width: 1,
color: '#019680',
},
},
},
xAxis: {
type: 'category',
boundaryGap: false,
data: [...new Array(18)].map((_item, index) => `${index + 6}:00`),
splitLine: {
show: true,
lineStyle: {
width: 1,
type: 'solid',
color: 'rgba(226,226,226,0.5)',
},
},
axisTick: {
show: false,
},
},
yAxis: [
{
type: 'value',
max: 80000,
splitNumber: 4,
axisTick: {
show: false,
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(255,255,255,0.2)', 'rgba(226,226,226,0.2)'],
},
},
},
],
grid: { left: '1%', right: '1%', top: '2 %', bottom: 0, containLabel: true },
series: [
{
smooth: true,
data: [
111, 222, 4000, 18000, 33333, 55555, 66666, 33333, 14000, 36000, 66666, 44444, 22222,
11111, 4000, 2000, 500, 333, 222, 111,
],
type: 'line',
areaStyle: {},
itemStyle: {
color: '#5ab1ef',
},
},
{
smooth: true,
data: [
33, 66, 88, 333, 3333, 5000, 18000, 3000, 1200, 13000, 22000, 11000, 2221, 1201, 390,
198, 60, 30, 22, 11,
],
type: 'line',
areaStyle: {},
itemStyle: {
color: '#019680',
},
},
],
})
})
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div ref="chartRef" :style="{ height, width }"></div>
</template>
<script lang="ts">
import { basicProps } from './props'
</script>
<script lang="ts" setup>
import { onMounted, ref, Ref } from 'vue'
import { useECharts } from '/@/hooks/web/useECharts'
defineProps({
...basicProps,
})
const chartRef = ref<HTMLDivElement | null>(null)
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
onMounted(() => {
setOptions({
tooltip: {
trigger: 'axis',
axisPointer: {
lineStyle: {
width: 1,
color: '#019680',
},
},
},
grid: { left: '1%', right: '1%', top: '2 %', bottom: 0, containLabel: true },
xAxis: {
type: 'category',
data: [...new Array(12)].map((_item, index) => `${index + 1}`),
},
yAxis: {
type: 'value',
max: 8000,
splitNumber: 4,
},
series: [
{
data: [3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000, 3200, 4800],
type: 'bar',
barMaxWidth: 80,
},
],
})
})
</script>

View File

@@ -0,0 +1,94 @@
<template>
<Card title="转化率" :loading="loading">
<div ref="chartRef" :style="{ width, height }"></div>
</Card>
</template>
<script lang="ts" setup>
import { Ref, ref, watch } from 'vue'
import { Card } from 'ant-design-vue'
import { useECharts } from '/@/hooks/web/useECharts'
const props = defineProps({
loading: Boolean,
width: {
type: String as PropType<string>,
default: '100%',
},
height: {
type: String as PropType<string>,
default: '300px',
},
})
const chartRef = ref<HTMLDivElement | null>(null)
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
watch(
() => props.loading,
() => {
if (props.loading) {
return
}
setOptions({
legend: {
bottom: 0,
data: ['访问', '购买'],
},
tooltip: {},
radar: {
radius: '60%',
splitNumber: 8,
indicator: [
{
name: '电脑',
},
{
name: '充电器',
},
{
name: '耳机',
},
{
name: '手机',
},
{
name: 'Ipad',
},
{
name: '耳机',
},
],
},
series: [
{
type: 'radar',
symbolSize: 0,
areaStyle: {
shadowBlur: 0,
shadowColor: 'rgba(0,0,0,.2)',
shadowOffsetX: 0,
shadowOffsetY: 10,
opacity: 1,
},
data: [
{
value: [90, 50, 86, 40, 50, 20],
name: '访问',
itemStyle: {
color: '#b6a2de',
},
},
{
value: [70, 75, 70, 76, 20, 85],
name: '购买',
itemStyle: {
color: '#5ab1ef',
},
},
],
},
],
})
},
{ immediate: true },
)
</script>

View File

@@ -0,0 +1,81 @@
<template>
<Card title="访问来源" :loading="loading">
<div ref="chartRef" :style="{ width, height }"></div>
</Card>
</template>
<script lang="ts" setup>
import { Ref, ref, watch } from 'vue'
import { Card } from 'ant-design-vue'
import { useECharts } from '/@/hooks/web/useECharts'
const props = defineProps({
loading: Boolean,
width: {
type: String as PropType<string>,
default: '100%',
},
height: {
type: String as PropType<string>,
default: '300px',
},
})
const chartRef = ref<HTMLDivElement | null>(null)
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
watch(
() => props.loading,
() => {
if (props.loading) {
return
}
setOptions({
tooltip: {
trigger: 'item',
},
legend: {
bottom: '1%',
left: 'center',
},
series: [
{
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
name: '访问来源',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: '12',
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [
{ value: 1048, name: '搜索引擎' },
{ value: 735, name: '直接访问' },
{ value: 580, name: '邮件营销' },
{ value: 484, name: '联盟广告' },
],
animationType: 'scale',
animationEasing: 'exponentialInOut',
animationDelay: function () {
return Math.random() * 100
},
},
],
})
},
{ immediate: true },
)
</script>

View File

@@ -0,0 +1,16 @@
import { PropType } from 'vue'
export interface BasicProps {
width: string
height: string
}
export const basicProps = {
width: {
type: String as PropType<string>,
default: '100%',
},
height: {
type: String as PropType<string>,
default: '280px',
},
}

View File

@@ -0,0 +1,8 @@
export interface GrowCardItem {
icon: string
title: string
value: number
total: number
color: string
action: string
}

View File

@@ -0,0 +1,25 @@
<template>
<div class="p-4">
<GrowCard :loading="loading" class="enter-y" />
<SiteAnalysis class="!my-4 enter-y" :loading="loading" />
<div class="md:flex enter-y">
<VisitRadar class="md:w-1/3 w-full" :loading="loading" />
<VisitSource class="md:w-1/3 !md:mx-4 !md:my-0 !my-4 w-full" :loading="loading" />
<SalesProductPie class="md:w-1/3 w-full" :loading="loading" />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import GrowCard from './components/GrowCard.vue'
import SiteAnalysis from './components/SiteAnalysis.vue'
import VisitSource from './components/VisitSource.vue'
import VisitRadar from './components/VisitRadar.vue'
import SalesProductPie from './components/SalesProductPie.vue'
const loading = ref(true)
setTimeout(() => {
loading.value = false
}, 1500)
</script>

View File

@@ -0,0 +1,31 @@
<template>
<Card title="最新动态" v-bind="$attrs">
<template #extra>
<a-button type="link" size="small">更多</a-button>
</template>
<List item-layout="horizontal" :data-source="dynamicInfoItems">
<template #renderItem="{ item }">
<ListItem>
<ListItemMeta>
<template #description>
{{ item.date }}
</template>
<!-- eslint-disable-next-line -->
<template #title> {{ item.name }} <span v-html="item.desc"> </span> </template>
<template #avatar>
<Icon :icon="item.avatar" :size="30" />
</template>
</ListItemMeta>
</ListItem>
</template>
</List>
</Card>
</template>
<script lang="ts" setup>
import { Card, List } from 'ant-design-vue'
import { dynamicInfoItems } from './data'
import { Icon } from '/@/components/Icon'
const ListItem = List.Item
const ListItemMeta = List.Item.Meta
</script>

View File

@@ -0,0 +1,32 @@
<template>
<Card title="项目" v-bind="$attrs">
<template #extra>
<a-button type="link" size="small">更多</a-button>
</template>
<CardGrid v-for="item in items" :key="item" class="!md:w-1/3 !w-full">
<span class="flex">
<Icon :icon="item.icon" :color="item.color" size="30" />
<span class="text-lg ml-4">{{ item.title }}</span>
</span>
<div class="flex mt-2 h-10 text-secondary">{{ item.desc }}</div>
<div class="flex justify-between text-secondary">
<span>{{ item.group }}</span>
<span>{{ item.date }}</span>
</div>
</CardGrid>
</Card>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { Card } from 'ant-design-vue'
import { Icon } from '/@/components/Icon'
import { groupItems } from './data'
export default defineComponent({
components: { Card, CardGrid: Card.Grid, Icon },
setup() {
return { items: groupItems }
},
})
</script>

View File

@@ -0,0 +1,17 @@
<template>
<Card title="快捷导航" v-bind="$attrs">
<CardGrid v-for="item in navItems" :key="item">
<span class="flex flex-col items-center">
<Icon :icon="item.icon" :color="item.color" size="20" />
<span class="text-md mt-2">{{ item.title }}</span>
</span>
</CardGrid>
</Card>
</template>
<script lang="ts" setup>
import { Card } from 'ant-design-vue'
import { navItems } from './data'
import { Icon } from '/@/components/Icon'
const CardGrid = Card.Grid
</script>

View File

@@ -0,0 +1,94 @@
<template>
<Card title="销售统计" :loading="loading">
<div ref="chartRef" :style="{ width, height }"></div>
</Card>
</template>
<script lang="ts" setup>
import { Ref, ref, watch } from 'vue'
import { Card } from 'ant-design-vue'
import { useECharts } from '/@/hooks/web/useECharts'
const props = defineProps({
loading: Boolean,
width: {
type: String as PropType<string>,
default: '100%',
},
height: {
type: String as PropType<string>,
default: '400px',
},
})
const chartRef = ref<HTMLDivElement | null>(null)
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
watch(
() => props.loading,
() => {
if (props.loading) {
return
}
setOptions({
legend: {
bottom: 0,
data: ['Visits', 'Sales'],
},
tooltip: {},
radar: {
radius: '60%',
splitNumber: 8,
indicator: [
{
name: '2017',
},
{
name: '2017',
},
{
name: '2018',
},
{
name: '2019',
},
{
name: '2020',
},
{
name: '2021',
},
],
},
series: [
{
type: 'radar',
symbolSize: 0,
areaStyle: {
shadowBlur: 0,
shadowColor: 'rgba(0,0,0,.2)',
shadowOffsetX: 0,
shadowOffsetY: 10,
opacity: 1,
},
data: [
{
value: [90, 50, 86, 40, 50, 20],
name: 'Visits',
itemStyle: {
color: '#b6a2de',
},
},
{
value: [70, 75, 70, 76, 20, 85],
name: 'Sales',
itemStyle: {
color: '#67e0e3',
},
},
],
},
],
})
},
{ immediate: true },
)
</script>

View File

@@ -0,0 +1,33 @@
<template>
<div class="lg:flex">
<Avatar :src="userinfo.avatar || headerImg" :size="72" class="!mx-auto !block" />
<div class="md:ml-6 flex flex-col justify-center md:mt-0 mt-2">
<h1 class="md:text-lg text-md">早安, {{ userinfo.realName }}, 开始您一天的工作吧</h1>
<span class="text-secondary"> 今日晴20 - 32 </span>
</div>
<div class="flex flex-1 justify-end md:mt-0 mt-4">
<div class="flex flex-col justify-center text-right">
<span class="text-secondary"> 待办 </span>
<span class="text-2xl">2/10</span>
</div>
<div class="flex flex-col justify-center text-right md:mx-16 mx-12">
<span class="text-secondary"> 项目 </span>
<span class="text-2xl">8</span>
</div>
<div class="flex flex-col justify-center text-right md:mr-10 mr-4">
<span class="text-secondary"> 团队 </span>
<span class="text-2xl">300</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { Avatar } from 'ant-design-vue'
import { useUserStore } from '/@/store/modules/user'
import headerImg from '/@/assets/images/header.jpg'
const userStore = useUserStore()
const userinfo = computed(() => userStore.getUserInfo)
</script>

View File

@@ -0,0 +1,156 @@
interface GroupItem {
title: string
icon: string
color: string
desc: string
date: string
group: string
}
interface NavItem {
title: string
icon: string
color: string
}
interface DynamicInfoItem {
avatar: string
name: string
date: string
desc: string
}
export const navItems: NavItem[] = [
{
title: '首页',
icon: 'ion:home-outline',
color: '#1fdaca',
},
{
title: '仪表盘',
icon: 'ion:grid-outline',
color: '#bf0c2c',
},
{
title: '组件',
icon: 'ion:layers-outline',
color: '#e18525',
},
{
title: '系统管理',
icon: 'ion:settings-outline',
color: '#3fb27f',
},
{
title: '权限管理',
icon: 'ion:key-outline',
color: '#4daf1bc9',
},
{
title: '图表',
icon: 'ion:bar-chart-outline',
color: '#00d8ff',
},
]
export const dynamicInfoItems: DynamicInfoItem[] = [
{
avatar: 'dynamic-avatar-1|svg',
name: '威廉',
date: '刚刚',
desc: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
},
{
avatar: 'dynamic-avatar-2|svg',
name: '艾文',
date: '1个小时前',
desc: `关注了 <a>威廉</a> `,
},
{
avatar: 'dynamic-avatar-3|svg',
name: '克里斯',
date: '1天前',
desc: `发布了 <a>个人动态</a> `,
},
{
avatar: 'dynamic-avatar-4|svg',
name: 'Vben',
date: '2天前',
desc: `发表文章 <a>如何编写一个Vite插件</a> `,
},
{
avatar: 'dynamic-avatar-5|svg',
name: '皮特',
date: '3天前',
desc: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
},
{
avatar: 'dynamic-avatar-6|svg',
name: '杰克',
date: '1周前',
desc: `关闭了问题 <a>如何运行项目</a> `,
},
{
avatar: 'dynamic-avatar-1|svg',
name: '威廉',
date: '1周前',
desc: `发布了 <a>个人动态</a> `,
},
{
avatar: 'dynamic-avatar-1|svg',
name: '威廉',
date: '2021-04-01 20:00',
desc: `推送了代码到 <a>Github</a>`,
},
]
export const groupItems: GroupItem[] = [
{
title: 'Github',
icon: 'carbon:logo-github',
color: '',
desc: '不要等待机会,而要创造机会。',
group: '开源组',
date: '2021-04-01',
},
{
title: 'Vue',
icon: 'ion:logo-vue',
color: '#3fb27f',
desc: '现在的你决定将来的你。',
group: '算法组',
date: '2021-04-01',
},
{
title: 'Html5',
icon: 'ion:logo-html5',
color: '#e18525',
desc: '没有什么才能比努力更重要。',
group: '上班摸鱼',
date: '2021-04-01',
},
{
title: 'Angular',
icon: 'ion:logo-angular',
color: '#bf0c2c',
desc: '热情和欲望可以突破一切难关。',
group: 'UI',
date: '2021-04-01',
},
{
title: 'React',
icon: 'bx:bxl-react',
color: '#00d8ff',
desc: '健康的身体是实目标的基石。',
group: '技术牛',
date: '2021-04-01',
},
{
title: 'Js',
icon: 'ion:logo-javascript',
color: '#4daf1bc9',
desc: '路是走出来的,而不是空想出来的。',
group: '架构组',
date: '2021-04-01',
},
]

View File

@@ -0,0 +1,36 @@
<template>
<PageWrapper>
<template #headerContent> <WorkbenchHeader /> </template>
<div class="lg:flex">
<div class="lg:w-7/10 w-full !mr-4 enter-y">
<ProjectCard :loading="loading" class="enter-y" />
<DynamicInfo :loading="loading" class="!my-4 enter-y" />
</div>
<div class="lg:w-3/10 w-full enter-y">
<QuickNav :loading="loading" class="enter-y" />
<Card class="!my-4 enter-y" :loading="loading">
<img class="xl:h-50 h-30 mx-auto" src="../../../assets/svg/illustration.svg" />
</Card>
<SaleRadar :loading="loading" class="enter-y" />
</div>
</div>
</PageWrapper>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { Card } from 'ant-design-vue'
import { PageWrapper } from '/@/components/Page'
import WorkbenchHeader from './components/WorkbenchHeader.vue'
import ProjectCard from './components/ProjectCard.vue'
import QuickNav from './components/QuickNav.vue'
import DynamicInfo from './components/DynamicInfo.vue'
import SaleRadar from './components/SaleRadar.vue'
const loading = ref(true)
setTimeout(() => {
loading.value = false
}, 1500)
</script>

View File

@@ -0,0 +1,43 @@
<template>
<PageWrapper title="引导页" content="用于给用户的指引操作">
<a-button type="primary" @click="handleStart">开始</a-button>
</PageWrapper>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { PageWrapper } from '/@/components/Page'
import { useDesign } from '/@/hooks/web/useDesign'
import intro from 'intro.js'
import 'intro.js/minified/introjs.min.css'
export default defineComponent({
components: { PageWrapper },
setup() {
const { prefixVar } = useDesign('')
function handleStart() {
intro()
.setOptions({
steps: [
{
title: 'Welcome',
intro: 'Hello World! 👋',
},
{
title: 'Collapse Button',
element: document.querySelector(`.${prefixVar}-layout-header-trigger`)!,
intro: 'This is the menu collapse button.',
},
{
title: 'User Action',
element: document.querySelector(`.${prefixVar}-layout-header-action`)!,
intro: 'This is the user function area.',
},
],
})
.start()
}
return { handleStart }
},
})
</script>

View File

@@ -0,0 +1,98 @@
<template>
<PageWrapper title="关于">
<template #headerContent>
<div class="flex justify-between items-center">
<span class="flex-1">
<a :href="GITHUB_URL" target="_blank">{{ name }}</a>
是一个基于Vue3.0Vite Ant-Design-Vue TypeScript
的后台解决方案目标是为中大型项目开发,提供现成的开箱解决方案及丰富的示例,原则上不会限制任何代码用于商用
</span>
</div>
</template>
<Description @register="infoRegister" class="enter-y" />
<Description @register="register" class="my-4 enter-y" />
<Description @register="registerDev" class="enter-y" />
</PageWrapper>
</template>
<script lang="ts" setup>
import { h } from 'vue'
import { Tag } from 'ant-design-vue'
import { PageWrapper } from '/@/components/Page'
import { Description, DescItem, useDescription } from '/@/components/Description/index'
import { GITHUB_URL, SITE_URL, DOC_URL } from '/@/settings/siteSetting'
const { pkg, lastBuildTime } = __APP_INFO__
const { dependencies, devDependencies, name, version } = pkg
const schema: DescItem[] = []
const devSchema: DescItem[] = []
const commonTagRender = (color: string) => (curVal) => h(Tag, { color }, () => curVal)
const commonLinkRender = (text: string) => (href) => h('a', { href, target: '_blank' }, text)
const infoSchema: DescItem[] = [
{
label: '版本',
field: 'version',
render: commonTagRender('blue'),
},
{
label: '最后编译时间',
field: 'lastBuildTime',
render: commonTagRender('blue'),
},
{
label: '文档地址',
field: 'doc',
render: commonLinkRender('文档地址'),
},
{
label: '预览地址',
field: 'preview',
render: commonLinkRender('预览地址'),
},
{
label: 'Github',
field: 'github',
render: commonLinkRender('Github'),
},
]
const infoData = {
version,
lastBuildTime,
doc: DOC_URL,
preview: SITE_URL,
github: GITHUB_URL,
}
Object.keys(dependencies).forEach((key) => {
schema.push({ field: key, label: key })
})
Object.keys(devDependencies).forEach((key) => {
devSchema.push({ field: key, label: key })
})
const [register] = useDescription({
title: '生产环境依赖',
data: dependencies,
schema: schema,
column: 3,
})
const [registerDev] = useDescription({
title: '开发环境依赖',
data: devDependencies,
schema: devSchema,
column: 3,
})
const [infoRegister] = useDescription({
title: '项目信息',
data: infoData,
schema: infoSchema,
column: 2,
})
</script>

View File

@@ -0,0 +1,148 @@
<script lang="tsx">
import type { PropType } from 'vue'
import { Result, Button } from 'ant-design-vue'
import { defineComponent, ref, computed, unref } from 'vue'
import { ExceptionEnum } from '/@/enums/exceptionEnum'
import notDataSvg from '/@/assets/svg/no-data.svg'
import netWorkSvg from '/@/assets/svg/net-error.svg'
import { useRoute } from 'vue-router'
import { useDesign } from '/@/hooks/web/useDesign'
import { useI18n } from '/@/hooks/web/useI18n'
import { useGo, useRedo } from '/@/hooks/web/usePage'
import { PageEnum } from '/@/enums/pageEnum'
interface MapValue {
title: string
subTitle: string
btnText?: string
icon?: string
handler?: Fn
status?: string
}
export default defineComponent({
name: 'ErrorPage',
props: {
// 状态码
status: {
type: Number as PropType<number>,
default: ExceptionEnum.PAGE_NOT_FOUND,
},
title: {
type: String as PropType<string>,
default: '',
},
subTitle: {
type: String as PropType<string>,
default: '',
},
full: {
type: Boolean as PropType<boolean>,
default: false,
},
},
setup(props) {
const statusMapRef = ref(new Map<string | number, MapValue>())
const { query } = useRoute()
const go = useGo()
const redo = useRedo()
const { t } = useI18n()
const { prefixCls } = useDesign('app-exception-page')
const getStatus = computed(() => {
const { status: routeStatus } = query
const { status } = props
return Number(routeStatus) || status
})
const getMapValue = computed((): MapValue => {
return unref(statusMapRef).get(unref(getStatus)) as MapValue
})
const backLoginI18n = t('sys.exception.backLogin')
const backHomeI18n = t('sys.exception.backHome')
unref(statusMapRef).set(ExceptionEnum.PAGE_NOT_ACCESS, {
title: '403',
status: `${ExceptionEnum.PAGE_NOT_ACCESS}`,
subTitle: t('sys.exception.subTitle403'),
btnText: props.full ? backLoginI18n : backHomeI18n,
handler: () => (props.full ? go(PageEnum.BASE_LOGIN) : go()),
})
unref(statusMapRef).set(ExceptionEnum.PAGE_NOT_FOUND, {
title: '404',
status: `${ExceptionEnum.PAGE_NOT_FOUND}`,
subTitle: t('sys.exception.subTitle404'),
btnText: props.full ? backLoginI18n : backHomeI18n,
handler: () => (props.full ? go(PageEnum.BASE_LOGIN) : go()),
})
unref(statusMapRef).set(ExceptionEnum.ERROR, {
title: '500',
status: `${ExceptionEnum.ERROR}`,
subTitle: t('sys.exception.subTitle500'),
btnText: backHomeI18n,
handler: () => go(),
})
unref(statusMapRef).set(ExceptionEnum.PAGE_NOT_DATA, {
title: t('sys.exception.noDataTitle'),
subTitle: '',
btnText: t('common.redo'),
handler: () => redo(),
icon: notDataSvg,
})
unref(statusMapRef).set(ExceptionEnum.NET_WORK_ERROR, {
title: t('sys.exception.networkErrorTitle'),
subTitle: t('sys.exception.networkErrorSubTitle'),
btnText: t('common.redo'),
handler: () => redo(),
icon: netWorkSvg,
})
return () => {
const { title, subTitle, btnText, icon, handler, status } = unref(getMapValue) || {}
return (
<Result
class={prefixCls}
status={status as any}
title={props.title || title}
sub-title={props.subTitle || subTitle}
>
{{
extra: () =>
btnText && (
<Button type="primary" onClick={handler}>
{() => btnText}
</Button>
),
icon: () => (icon ? <img src={icon} /> : null),
}}
</Result>
)
}
},
})
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-app-exception-page';
.@{prefix-cls} {
display: flex;
align-items: center;
flex-direction: column;
.ant-result-icon {
img {
max-width: 400px;
max-height: 300px;
}
}
}
</style>

View File

@@ -0,0 +1 @@
export { default as Exception } from './Exception.vue'

View File

@@ -0,0 +1,64 @@
<template>
<template v-if="getShow">
<LoginFormTitle class="enter-x" />
<Form class="p-4 enter-x" :model="formData" :rules="getFormRules" ref="formRef">
<FormItem name="account" class="enter-x">
<Input
size="large"
v-model:value="formData.account"
:placeholder="t('sys.login.userName')"
/>
</FormItem>
<FormItem name="mobile" class="enter-x">
<Input size="large" v-model:value="formData.mobile" :placeholder="t('sys.login.mobile')" />
</FormItem>
<FormItem name="sms" class="enter-x">
<CountdownInput
size="large"
v-model:value="formData.sms"
:placeholder="t('sys.login.smsCode')"
/>
</FormItem>
<FormItem class="enter-x">
<Button type="primary" size="large" block @click="handleReset" :loading="loading">
{{ t('common.resetText') }}
</Button>
<Button size="large" block class="mt-4" @click="handleBackLogin">
{{ t('sys.login.backSignIn') }}
</Button>
</FormItem>
</Form>
</template>
</template>
<script lang="ts" setup>
import { reactive, ref, computed, unref } from 'vue'
import LoginFormTitle from './LoginFormTitle.vue'
import { Form, Input, Button } from 'ant-design-vue'
import { CountdownInput } from '/@/components/CountDown'
import { useI18n } from '/@/hooks/web/useI18n'
import { useLoginState, useFormRules, LoginStateEnum } from './useLogin'
const FormItem = Form.Item
const { t } = useI18n()
const { handleBackLogin, getLoginState } = useLoginState()
const { getFormRules } = useFormRules()
const formRef = ref()
const loading = ref(false)
const formData = reactive({
account: '',
mobile: '',
sms: '',
})
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD)
async function handleReset() {
const form = unref(formRef)
if (!form) return
await form.resetFields()
}
</script>

View File

@@ -0,0 +1,215 @@
<template>
<div :class="prefixCls" class="relative w-full h-full px-4">
<div class="flex items-center absolute right-4 top-4">
<AppDarkModeToggle class="enter-x mr-2" v-if="!sessionTimeout" />
<AppLocalePicker
class="text-white enter-x xl:text-gray-600"
:show-text="false"
v-if="!sessionTimeout && showLocale"
/>
</div>
<span class="-enter-x xl:hidden">
<AppLogo :alwaysShowTitle="true" />
</span>
<div class="container relative h-full py-2 mx-auto sm:px-10">
<div class="flex h-full">
<div class="hidden min-h-full pl-4 mr-4 xl:flex xl:flex-col xl:w-6/12">
<AppLogo class="-enter-x" />
<div class="my-auto">
<img
:alt="title"
src="../../../assets/svg/login-box-bg.svg"
class="w-1/2 -mt-16 -enter-x"
/>
<div class="mt-10 font-medium text-white -enter-x">
<span class="inline-block mt-4 text-3xl"> {{ t('sys.login.signInTitle') }}</span>
</div>
<div class="mt-5 font-normal text-white dark:text-gray-500 -enter-x">
{{ t('sys.login.signInDesc') }}
</div>
</div>
</div>
<div class="flex w-full h-full py-5 xl:h-auto xl:py-0 xl:my-0 xl:w-6/12">
<div
:class="`${prefixCls}-form`"
class="relative w-full px-5 py-8 mx-auto my-auto rounded-md shadow-md xl:ml-16 xl:bg-transparent sm:px-8 xl:p-4 xl:shadow-none sm:w-3/4 lg:w-2/4 xl:w-auto enter-x"
>
<LoginForm />
<ForgetPasswordForm />
<RegisterForm />
<MobileForm />
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { AppLogo } from '/@/components/Application'
import { AppLocalePicker, AppDarkModeToggle } from '/@/components/Application'
import LoginForm from './LoginForm.vue'
import ForgetPasswordForm from './ForgetPasswordForm.vue'
import RegisterForm from './RegisterForm.vue'
import MobileForm from './MobileForm.vue'
import { useGlobSetting } from '/@/hooks/setting'
import { useI18n } from '/@/hooks/web/useI18n'
import { useDesign } from '/@/hooks/web/useDesign'
import { useLocaleStore } from '/@/store/modules/locale'
defineProps({
sessionTimeout: {
type: Boolean,
},
})
const globSetting = useGlobSetting()
const { prefixCls } = useDesign('login')
const { t } = useI18n()
const localeStore = useLocaleStore()
const showLocale = localeStore.getShowPicker
const title = computed(() => globSetting?.title ?? '')
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-login';
@logo-prefix-cls: ~'@{namespace}-app-logo';
@countdown-prefix-cls: ~'@{namespace}-countdown-input';
@dark-bg: #293146;
html[data-theme='dark'] {
.@{prefix-cls} {
background-color: @dark-bg;
&::before {
background-image: url(/@/assets/svg/login-bg-dark.svg);
}
.ant-input,
.ant-input-password {
background-color: #232a3b;
}
.ant-btn:not(.ant-btn-link):not(.ant-btn-primary) {
border: 1px solid #4a5569;
}
&-form {
background: transparent !important;
}
.app-iconify {
color: #fff;
}
}
input.fix-auto-fill,
.fix-auto-fill input {
-webkit-text-fill-color: #c9d1d9 !important;
box-shadow: inherit !important;
}
}
.@{prefix-cls} {
min-height: 100%;
overflow: hidden;
@media (max-width: @screen-xl) {
background-color: #293146;
.@{prefix-cls}-form {
background-color: #fff;
}
}
&::before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
margin-left: -48%;
background-image: url(/@/assets/svg/login-bg.svg);
background-position: 100%;
background-repeat: no-repeat;
background-size: auto 100%;
content: '';
@media (max-width: @screen-xl) {
display: none;
}
}
.@{logo-prefix-cls} {
position: absolute;
top: 12px;
height: 30px;
&__title {
font-size: 16px;
color: #fff;
}
img {
width: 32px;
}
}
.container {
.@{logo-prefix-cls} {
display: flex;
width: 60%;
height: 80px;
&__title {
font-size: 24px;
color: #fff;
}
img {
width: 48px;
}
}
}
&-sign-in-way {
.anticon {
font-size: 22px;
color: #888;
cursor: pointer;
&:hover {
color: @primary-color;
}
}
}
input:not([type='checkbox']) {
min-width: 360px;
@media (max-width: @screen-xl) {
min-width: 320px;
}
@media (max-width: @screen-lg) {
min-width: 260px;
}
@media (max-width: @screen-md) {
min-width: 240px;
}
@media (max-width: @screen-sm) {
min-width: 160px;
}
}
.@{countdown-prefix-cls} input {
min-width: unset;
}
.ant-divider-inner-text {
font-size: 12px;
color: @text-color-secondary;
}
}
</style>

View File

@@ -0,0 +1,137 @@
<template>
<LoginFormTitle v-show="getShow" class="enter-x" />
<Form
class="p-4 enter-x"
:model="formData"
:rules="getFormRules"
ref="formRef"
v-show="getShow"
@keypress.enter="handleLogin"
>
<FormItem name="account" class="enter-x">
<Input
size="large"
v-model:value="formData.account"
:placeholder="t('sys.login.userName')"
class="fix-auto-fill"
/>
</FormItem>
<FormItem name="password" class="enter-x">
<InputPassword
size="large"
visibilityToggle
v-model:value="formData.password"
:placeholder="t('sys.login.password')"
/>
</FormItem>
<ARow class="enter-x">
<ACol :span="12">
<FormItem>
<!-- No logic, you need to deal with it yourself -->
<Checkbox v-model:checked="rememberMe" size="small">
{{ t('sys.login.rememberMe') }}
</Checkbox>
</FormItem>
</ACol>
<ACol :span="12">
<FormItem :style="{ 'text-align': 'right' }">
<!-- No logic, you need to deal with it yourself -->
<Button type="link" size="small" @click="setLoginState(LoginStateEnum.RESET_PASSWORD)">
{{ t('sys.login.forgetPassword') }}
</Button>
</FormItem>
</ACol>
</ARow>
<FormItem class="enter-x">
<Button type="primary" size="large" block @click="handleLogin" :loading="loading">
{{ t('sys.login.loginButton') }}
</Button>
<!-- <Button size="large" class="mt-4 enter-x" block @click="handleRegister">
{{ t('sys.login.registerButton') }}
</Button> -->
</FormItem>
<ARow class="enter-x">
<ACol :md="8" :xs="24">
<Button block @click="setLoginState(LoginStateEnum.MOBILE)">
{{ t('sys.login.mobileSignInFormTitle') }}
</Button>
</ACol>
<ACol :md="6" :xs="24">
<Button block @click="setLoginState(LoginStateEnum.REGISTER)">
{{ t('sys.login.registerButton') }}
</Button>
</ACol>
</ARow>
</Form>
</template>
<script lang="ts" setup>
import { reactive, ref, unref, computed } from 'vue'
import { Checkbox, Form, Input, Row, Col, Button } from 'ant-design-vue'
import LoginFormTitle from './LoginFormTitle.vue'
import { useI18n } from '/@/hooks/web/useI18n'
import { useMessage } from '/@/hooks/web/useMessage'
import { useUserStore } from '/@/store/modules/user'
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin'
import { useDesign } from '/@/hooks/web/useDesign'
//import { onKeyStroke } from '@vueuse/core';
const ACol = Col
const ARow = Row
const FormItem = Form.Item
const InputPassword = Input.Password
const { t } = useI18n()
const { notification, createErrorModal } = useMessage()
const { prefixCls } = useDesign('login')
const userStore = useUserStore()
const { setLoginState, getLoginState } = useLoginState()
const { getFormRules } = useFormRules()
const formRef = ref()
const loading = ref(false)
const rememberMe = ref(false)
const formData = reactive({
account: 'vben',
password: '123456',
})
const { validForm } = useFormValid(formRef)
//onKeyStroke('Enter', handleLogin);
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
async function handleLogin() {
const data = await validForm()
if (!data) return
try {
loading.value = true
const userInfo = await userStore.login({
password: data.password,
username: data.account,
mode: 'none', //不要默认的错误提示
})
if (userInfo) {
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realName}`,
duration: 3,
})
}
} catch (error) {
createErrorModal({
title: t('sys.api.errorTip'),
content: (error as unknown as Error).message || t('sys.api.networkExceptionMsg'),
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body,
})
} finally {
loading.value = false
}
}
</script>

View File

@@ -0,0 +1,24 @@
<template>
<h2 class="mb-3 text-2xl font-bold text-center xl:text-3xl enter-x xl:text-left">
{{ getFormTitle }}
</h2>
</template>
<script lang="ts" setup>
import { computed, unref } from 'vue'
import { useI18n } from '/@/hooks/web/useI18n'
import { LoginStateEnum, useLoginState } from './useLogin'
const { t } = useI18n()
const { getLoginState } = useLoginState()
const getFormTitle = computed(() => {
const titleObj = {
[LoginStateEnum.RESET_PASSWORD]: t('sys.login.forgetFormTitle'),
[LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'),
[LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'),
[LoginStateEnum.MOBILE]: t('sys.login.mobileSignInFormTitle'),
}
return titleObj[unref(getLoginState)]
})
</script>

View File

@@ -0,0 +1,63 @@
<template>
<template v-if="getShow">
<LoginFormTitle class="enter-x" />
<Form class="p-4 enter-x" :model="formData" :rules="getFormRules" ref="formRef">
<FormItem name="mobile" class="enter-x">
<Input
size="large"
v-model:value="formData.mobile"
:placeholder="t('sys.login.mobile')"
class="fix-auto-fill"
/>
</FormItem>
<FormItem name="sms" class="enter-x">
<CountdownInput
size="large"
class="fix-auto-fill"
v-model:value="formData.sms"
:placeholder="t('sys.login.smsCode')"
/>
</FormItem>
<FormItem class="enter-x">
<Button type="primary" size="large" block @click="handleLogin" :loading="loading">
{{ t('sys.login.loginButton') }}
</Button>
<Button size="large" block class="mt-4" @click="handleBackLogin">
{{ t('sys.login.backSignIn') }}
</Button>
</FormItem>
</Form>
</template>
</template>
<script lang="ts" setup>
import { reactive, ref, computed, unref } from 'vue'
import { Form, Input, Button } from 'ant-design-vue'
import { CountdownInput } from '/@/components/CountDown'
import LoginFormTitle from './LoginFormTitle.vue'
import { useI18n } from '/@/hooks/web/useI18n'
import { useLoginState, useFormRules, useFormValid, LoginStateEnum } from './useLogin'
const FormItem = Form.Item
const { t } = useI18n()
const { handleBackLogin, getLoginState } = useLoginState()
const { getFormRules } = useFormRules()
const formRef = ref()
const loading = ref(false)
const formData = reactive({
mobile: '',
sms: '',
})
const { validForm } = useFormValid(formRef)
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.MOBILE)
async function handleLogin() {
const data = await validForm()
if (!data) return
console.log(data)
}
</script>

View File

@@ -0,0 +1,104 @@
<template>
<template v-if="getShow">
<LoginFormTitle class="enter-x" />
<Form class="p-4 enter-x" :model="formData" :rules="getFormRules" ref="formRef">
<FormItem name="account" class="enter-x">
<Input
class="fix-auto-fill"
size="large"
v-model:value="formData.account"
:placeholder="t('sys.login.userName')"
/>
</FormItem>
<FormItem name="mobile" class="enter-x">
<Input
size="large"
v-model:value="formData.mobile"
:placeholder="t('sys.login.mobile')"
class="fix-auto-fill"
/>
</FormItem>
<FormItem name="sms" class="enter-x">
<CountdownInput
size="large"
class="fix-auto-fill"
v-model:value="formData.sms"
:placeholder="t('sys.login.smsCode')"
/>
</FormItem>
<FormItem name="password" class="enter-x">
<StrengthMeter
size="large"
v-model:value="formData.password"
:placeholder="t('sys.login.password')"
/>
</FormItem>
<FormItem name="confirmPassword" class="enter-x">
<InputPassword
size="large"
visibilityToggle
v-model:value="formData.confirmPassword"
:placeholder="t('sys.login.confirmPassword')"
/>
</FormItem>
<FormItem class="enter-x" name="policy">
<!-- No logic, you need to deal with it yourself -->
<Checkbox v-model:checked="formData.policy" size="small">
{{ t('sys.login.policy') }}
</Checkbox>
</FormItem>
<Button
type="primary"
class="enter-x"
size="large"
block
@click="handleRegister"
:loading="loading"
>
{{ t('sys.login.registerButton') }}
</Button>
<Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
{{ t('sys.login.backSignIn') }}
</Button>
</Form>
</template>
</template>
<script lang="ts" setup>
import { reactive, ref, unref, computed } from 'vue'
import LoginFormTitle from './LoginFormTitle.vue'
import { Form, Input, Button, Checkbox } from 'ant-design-vue'
import { StrengthMeter } from '/@/components/StrengthMeter'
import { CountdownInput } from '/@/components/CountDown'
import { useI18n } from '/@/hooks/web/useI18n'
import { useLoginState, useFormRules, useFormValid, LoginStateEnum } from './useLogin'
const FormItem = Form.Item
const InputPassword = Input.Password
const { t } = useI18n()
const { handleBackLogin, getLoginState } = useLoginState()
const formRef = ref()
const loading = ref(false)
const formData = reactive({
account: '',
password: '',
confirmPassword: '',
mobile: '',
sms: '',
policy: false,
})
const { getFormRules } = useFormRules(formData)
const { validForm } = useFormValid(formRef)
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER)
async function handleRegister() {
const data = await validForm()
if (!data) return
console.log(data)
}
</script>

View File

@@ -0,0 +1,53 @@
<template>
<transition>
<div :class="prefixCls">
<Login sessionTimeout />
</div>
</transition>
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref } from 'vue'
import Login from './Login.vue'
import { useDesign } from '/@/hooks/web/useDesign'
import { useUserStore } from '/@/store/modules/user'
import { usePermissionStore } from '/@/store/modules/permission'
import { useAppStore } from '/@/store/modules/app'
import { PermissionModeEnum } from '/@/enums/appEnum'
const { prefixCls } = useDesign('st-login')
const userStore = useUserStore()
const permissionStore = usePermissionStore()
const appStore = useAppStore()
const userId = ref<Nullable<number | string>>(0)
const isBackMode = () => {
return appStore.getProjectConfig.permissionMode === PermissionModeEnum.BACK
}
onMounted(() => {
// 记录当前的UserId
userId.value = userStore.getUserInfo?.userId
console.log('Mounted', userStore.getUserInfo)
})
onBeforeUnmount(() => {
if (userId.value && userId.value !== userStore.getUserInfo.userId) {
// 登录的不是同一个用户,刷新整个页面以便丢弃之前用户的页面状态
document.location.reload()
} else if (isBackMode() && permissionStore.getLastBuildMenuTime === 0) {
// 后台权限模式下没有成功加载过菜单就重新加载整个页面。这通常发生在会话过期后按F5刷新整个页面后载入了本模块这种场景
document.location.reload()
}
})
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-st-login';
.@{prefix-cls} {
position: fixed;
z-index: 9999999;
width: 100%;
height: 100%;
background: @component-background;
}
</style>

View File

@@ -0,0 +1,118 @@
import type { ValidationRule } from 'ant-design-vue/lib/form/Form'
import type { RuleObject } from 'ant-design-vue/lib/form/interface'
import { ref, computed, unref, Ref } from 'vue'
import { useI18n } from '/@/hooks/web/useI18n'
export enum LoginStateEnum {
LOGIN,
REGISTER,
RESET_PASSWORD,
MOBILE,
QR_CODE,
}
const currentState = ref(LoginStateEnum.LOGIN)
export function useLoginState() {
function setLoginState(state: LoginStateEnum) {
currentState.value = state
}
const getLoginState = computed(() => currentState.value)
function handleBackLogin() {
setLoginState(LoginStateEnum.LOGIN)
}
return { setLoginState, getLoginState, handleBackLogin }
}
export function useFormValid<T extends Object = any>(formRef: Ref<any>) {
async function validForm() {
const form = unref(formRef)
if (!form) return
const data = await form.validate()
return data as T
}
return { validForm }
}
export function useFormRules(formData?: Recordable) {
const { t } = useI18n()
const getAccountFormRule = computed(() => createRule(t('sys.login.accountPlaceholder')))
const getPasswordFormRule = computed(() => createRule(t('sys.login.passwordPlaceholder')))
const getSmsFormRule = computed(() => createRule(t('sys.login.smsPlaceholder')))
const getMobileFormRule = computed(() => createRule(t('sys.login.mobilePlaceholder')))
const validatePolicy = async (_: RuleObject, value: boolean) => {
return !value ? Promise.reject(t('sys.login.policyPlaceholder')) : Promise.resolve()
}
const validateConfirmPassword = (password: string) => {
return async (_: RuleObject, value: string) => {
if (!value) {
return Promise.reject(t('sys.login.passwordPlaceholder'))
}
if (value !== password) {
return Promise.reject(t('sys.login.diffPwd'))
}
return Promise.resolve()
}
}
const getFormRules = computed((): { [k: string]: ValidationRule | ValidationRule[] } => {
const accountFormRule = unref(getAccountFormRule)
const passwordFormRule = unref(getPasswordFormRule)
const smsFormRule = unref(getSmsFormRule)
const mobileFormRule = unref(getMobileFormRule)
const mobileRule = {
sms: smsFormRule,
mobile: mobileFormRule,
}
switch (unref(currentState)) {
// register form rules
case LoginStateEnum.REGISTER:
return {
account: accountFormRule,
password: passwordFormRule,
confirmPassword: [
{ validator: validateConfirmPassword(formData?.password), trigger: 'change' },
],
policy: [{ validator: validatePolicy, trigger: 'change' }],
...mobileRule,
}
// reset password form rules
case LoginStateEnum.RESET_PASSWORD:
return {
account: accountFormRule,
...mobileRule,
}
// mobile form rules
case LoginStateEnum.MOBILE:
return mobileRule
// login form rules
default:
return {
account: accountFormRule,
password: passwordFormRule,
}
}
})
return { getFormRules }
}
function createRule(message: string) {
return [
{
required: true,
message,
trigger: 'change',
},
]
}

View File

@@ -0,0 +1,30 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import { unref } from 'vue'
import { useRouter } from 'vue-router'
const { currentRoute, replace } = useRouter()
const { params, query } = unref(currentRoute)
const { path, _redirect_type = 'path' } = params
Reflect.deleteProperty(params, '_redirect_type')
Reflect.deleteProperty(params, 'path')
const _path = Array.isArray(path) ? path.join('/') : path
if (_redirect_type === 'name') {
replace({
name: _path,
query,
params,
})
} else {
replace({
path: _path.startsWith('/') ? _path : '/' + _path,
query,
})
}
</script>