项目创建
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1 class="title">留言墙</h1>
|
||||
<p class="desc">每一份留言都记录了您的想法,也为我们提供了珍贵的回忆</p>
|
||||
|
||||
<div class="list">
|
||||
<ul class="offset">
|
||||
<li
|
||||
class="comment-box"
|
||||
v-for="item in commentList"
|
||||
:key="item.id"
|
||||
:style="{ background: randomColor() }"
|
||||
@click="openDrawer(item)"
|
||||
>
|
||||
<p class="date">{{ item.date }}</p>
|
||||
<p class="content">{{ item.content }}</p>
|
||||
<div class="bottom">
|
||||
<div class="left">
|
||||
<span><i class="iconfont-sys"></i>{{ item.collection }}</span>
|
||||
<span><i class="iconfont-sys"></i>{{ item.comment }}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span>{{ item.userName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ElDrawer
|
||||
lDrawer
|
||||
v-model="showDrawer"
|
||||
:lock-scroll="false"
|
||||
:size="360"
|
||||
modal-class="comment-modal"
|
||||
>
|
||||
<template #header>
|
||||
<h4>详情</h4>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="drawer-default">
|
||||
<div class="comment-box" :style="{ background: randomColor() }">
|
||||
<p class="date">{{ clickItem.date }}</p>
|
||||
<p class="content">{{ clickItem.content }}</p>
|
||||
<div class="bottom">
|
||||
<div class="left">
|
||||
<span><i class="iconfont-sys"></i>{{ clickItem.collection }}</span>
|
||||
<span><i class="iconfont-sys"></i>{{ clickItem.comment }}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span>{{ clickItem.userName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评论组件 -->
|
||||
<CommentWidget />
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div>
|
||||
<!-- <ElButton @click="cancelClick">cancel</ElButton> -->
|
||||
<!-- <ElButton type="primary" @click="confirmClick">confirm</ElButton> -->
|
||||
</div>
|
||||
</template>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { commentList } from '@/mock/temp/commentList'
|
||||
const showDrawer = ref(false)
|
||||
|
||||
defineOptions({ name: 'ArticleComment' })
|
||||
|
||||
// const colorList = reactive([
|
||||
// 'rgba(216, 248, 255, 0.8)',
|
||||
// 'rgba(253, 223, 217, 0.8)',
|
||||
// 'rgba(252, 230, 240, 0.8)',
|
||||
// 'rgba(211, 248, 240, 0.8)',
|
||||
// 'rgba(255, 234, 188, 0.8)',
|
||||
// 'rgba(245, 225, 255, 0.8)',
|
||||
// 'rgba(225, 230, 254, 0.8)'
|
||||
// ])
|
||||
|
||||
const colorList = reactive([
|
||||
'#D8F8FF',
|
||||
'#FDDFD9',
|
||||
'#FCE6F0',
|
||||
'#D3F8F0',
|
||||
'#FFEABC',
|
||||
'#F5E1FF',
|
||||
'#E1E6FE'
|
||||
])
|
||||
|
||||
let lastColor: string | null = null
|
||||
|
||||
const randomColor = () => {
|
||||
let newColor: string
|
||||
|
||||
do {
|
||||
const index = Math.floor(Math.random() * colorList.length)
|
||||
newColor = colorList[index]
|
||||
} while (newColor === lastColor)
|
||||
|
||||
lastColor = newColor
|
||||
return newColor
|
||||
}
|
||||
|
||||
const clickItem = ref({
|
||||
id: 1,
|
||||
date: '2024-9-3',
|
||||
content: '加油!学好Node 自己写个小Demo',
|
||||
collection: 5,
|
||||
comment: 8,
|
||||
userName: '匿名'
|
||||
})
|
||||
|
||||
const openDrawer = (item: any) => {
|
||||
showDrawer.value = true
|
||||
clickItem.value = item
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
background-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
:deep(.comment-modal) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-top: 20px;
|
||||
font-size: 36px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
color: var(--art-text-gray-600);
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 40px;
|
||||
|
||||
.offset {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: calc(100% + 16px);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-box {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: calc(20% - 16px);
|
||||
aspect-ratio: 16 / 12;
|
||||
padding: 16px;
|
||||
margin: 0 16px 16px 0;
|
||||
cursor: pointer;
|
||||
background-color: #eae2cb;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 12px;
|
||||
color: #949494;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
font-size: 12px;
|
||||
color: #949494;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-default {
|
||||
.comment-box {
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-notebook) {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
width: calc(25% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-ipad-pro) {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
width: calc(33.333% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-ipad) {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
width: calc(50% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-phone) {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.page-content {
|
||||
.comment-box {
|
||||
color: #333 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="article-detail page-content">
|
||||
<div class="content">
|
||||
<h1>{{ articleTitle }}</h1>
|
||||
<div class="markdown-body" v-highlight v-html="articleHtml"></div>
|
||||
</div>
|
||||
<ArtBackToTop />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import '@/assets/styles/markdown.scss'
|
||||
import '@/assets/styles/one-dark-pro.scss'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
import axios from 'axios'
|
||||
// import 'highlight.js/styles/atom-one-dark.css';
|
||||
// import 'highlight.js/styles/vs2015.css';
|
||||
|
||||
defineOptions({ name: 'ArticleDetail' })
|
||||
|
||||
const articleId = ref(0)
|
||||
const router = useRoute()
|
||||
const articleTitle = ref('')
|
||||
const articleHtml = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
useCommon().scrollToTop()
|
||||
articleId.value = Number(router.query.id)
|
||||
getArticleDetail()
|
||||
})
|
||||
|
||||
const getArticleDetail = async () => {
|
||||
if (articleId.value) {
|
||||
const res = await axios.get('https://www.qiniu.lingchen.kim/blog_detail.json')
|
||||
if (res.data.code === 200) {
|
||||
articleTitle.value = res.data.data.title
|
||||
articleHtml.value = res.data.data.html_content
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.article-detail {
|
||||
.content {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
margin-top: 60px;
|
||||
|
||||
.markdown-body {
|
||||
margin-top: 60px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
border: 1px solid var(--art-gray-200);
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.copy-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50px;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
content: '';
|
||||
background: #0a0a0e;
|
||||
}
|
||||
}
|
||||
|
||||
.code-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.line-number {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
color: #9e9e9e;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
z-index: 1;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 20px;
|
||||
line-height: 40px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background-color: #000;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
opacity: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,380 @@
|
||||
<template>
|
||||
<div class="page-content article-list">
|
||||
<ElRow justify="space-between" :gutter="10">
|
||||
<ElCol :lg="6" :md="6" :sm="14" :xs="16">
|
||||
<ElInput
|
||||
v-model="searchVal"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
placeholder="输入文章标题查询"
|
||||
@keyup.enter="searchArticle"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :lg="12" :md="12" :sm="0" :xs="0">
|
||||
<div class="custom-segmented">
|
||||
<ElSegmented v-model="yearVal" :options="options" @change="searchArticleByYear" />
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :lg="6" :md="6" :sm="10" :xs="6" style="display: flex; justify-content: end">
|
||||
<ElButton @click="toAddArticle" v-auth="'add'">新增文章</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<div class="list">
|
||||
<div class="offset">
|
||||
<div class="item" v-for="item in articleList" :key="item.id" @click="toDetail(item)">
|
||||
<!-- 骨架屏 -->
|
||||
<ElSkeleton animated :loading="isLoading" style="width: 100%; height: 100%">
|
||||
<template #template>
|
||||
<div class="top">
|
||||
<ElSkeletonItem
|
||||
variant="image"
|
||||
style="width: 100%; height: 100%; border-radius: 10px"
|
||||
/>
|
||||
<div style="padding: 16px 0">
|
||||
<ElSkeletonItem variant="p" style="width: 80%" />
|
||||
<ElSkeletonItem variant="p" style="width: 40%; margin-top: 10px" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<div class="top">
|
||||
<ElImage class="cover" :src="item.home_img" lazy fit="cover">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<ElIcon><icon-picture /></ElIcon>
|
||||
</div>
|
||||
</template>
|
||||
</ElImage>
|
||||
|
||||
<span class="type">{{ item.type_name }}</span>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<h2>{{ item.title }}</h2>
|
||||
<div class="info">
|
||||
<div class="text">
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>{{ useDateFormat(item.create_time, 'YYYY-MM-DD') }}</span>
|
||||
<div class="line"></div>
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>{{ item.count }}</span>
|
||||
</div>
|
||||
<ElButton v-auth="'edit'" size="small" @click.stop="toEdit(item)">编辑</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElSkeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 16vh" v-if="showEmpty">
|
||||
<ElEmpty :description="`未找到相关数据 ${EmojiText[0]}`" />
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center; margin-top: 20px">
|
||||
<ElPagination
|
||||
size="default"
|
||||
background
|
||||
v-model:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:pager-count="9"
|
||||
layout="prev, pager, next, total,jumper"
|
||||
:total="total"
|
||||
:hide-on-single-page="true"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Picture as IconPicture } from '@element-plus/icons-vue'
|
||||
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { router } from '@/router'
|
||||
import { useDateFormat } from '@vueuse/core'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import EmojiText from '@/utils/ui/emojo'
|
||||
import { ArticleList } from '@/mock/temp/articleList'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
defineOptions({ name: 'ArticleList' })
|
||||
|
||||
const yearVal = ref('All')
|
||||
|
||||
const options = ['All', '2024', '2023', '2022', '2021', '2020', '2019']
|
||||
|
||||
const searchVal = ref('')
|
||||
const articleList = ref<any[]>([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(40)
|
||||
// const lastPage = ref(0)
|
||||
const total = ref(0)
|
||||
const isLoading = ref(true)
|
||||
|
||||
const showEmpty = computed(() => {
|
||||
return articleList.value.length === 0 && !isLoading.value
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getArticleList({ backTop: false })
|
||||
})
|
||||
|
||||
// 搜索文章
|
||||
const searchArticle = () => {
|
||||
getArticleList({ backTop: true })
|
||||
}
|
||||
|
||||
// 根据年份查询文章
|
||||
const searchArticleByYear = () => {
|
||||
getArticleList({ backTop: true })
|
||||
}
|
||||
|
||||
const getArticleList = async ({ backTop = false }) => {
|
||||
isLoading.value = true
|
||||
// let year = yearVal.value
|
||||
|
||||
if (searchVal.value) {
|
||||
yearVal.value = 'All'
|
||||
}
|
||||
|
||||
if (yearVal.value === 'All') {
|
||||
// year = ''
|
||||
}
|
||||
|
||||
// const params = {
|
||||
// page: currentPage.value,
|
||||
// size: pageSize.value,
|
||||
// searchVal: searchVal.value,
|
||||
// year
|
||||
// }
|
||||
|
||||
articleList.value = ArticleList
|
||||
isLoading.value = false
|
||||
|
||||
if (backTop) {
|
||||
useCommon().scrollToTop()
|
||||
}
|
||||
|
||||
// const res = await ArticleService.getArticleList(params)
|
||||
// if (res.code === ApiStatus.success) {
|
||||
// currentPage.value = res.currentPage
|
||||
// pageSize.value = res.pageSize
|
||||
// lastPage.value = res.lastPage
|
||||
// total.value = res.total
|
||||
// articleList.value = res.data
|
||||
|
||||
// // setTimeout(() => {
|
||||
// isLoading.value = false
|
||||
// // }, 3000)
|
||||
|
||||
// if (searchVal.value) {
|
||||
// searchVal.value = ''
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val: number) => {
|
||||
currentPage.value = val
|
||||
getArticleList({ backTop: true })
|
||||
}
|
||||
|
||||
const toDetail = (item: any) => {
|
||||
router.push({
|
||||
path: RoutesAlias.ArticleDetail,
|
||||
query: {
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toEdit = (item: any) => {
|
||||
router.push({
|
||||
path: RoutesAlias.ArticlePublish,
|
||||
query: {
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toAddArticle = () => {
|
||||
router.push({
|
||||
path: RoutesAlias.ArticlePublish
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.article-list {
|
||||
.custom-segmented .el-segmented {
|
||||
height: 40px;
|
||||
padding: 6px;
|
||||
|
||||
--el-border-radius-base: 8px;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 20px;
|
||||
|
||||
.offset {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: calc(100% + 20px);
|
||||
|
||||
.item {
|
||||
box-sizing: border-box;
|
||||
width: calc(20% - 20px);
|
||||
margin: 0 20px 20px 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--art-border-color);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
|
||||
|
||||
&:hover {
|
||||
.el-button {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.top {
|
||||
position: relative;
|
||||
aspect-ratio: 16/9.5;
|
||||
|
||||
.cover {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background: var(--art-gray-200);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px)
|
||||
calc(var(--custom-radius) / 2 + 2px) 0 0;
|
||||
|
||||
.image-slot {
|
||||
font-size: 26px;
|
||||
color: var(--art-gray-400);
|
||||
}
|
||||
}
|
||||
|
||||
.type {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
padding: 5px 4px;
|
||||
font-size: 12px;
|
||||
color: rgba(#fff, 0.8);
|
||||
background: rgba($color: #000, $alpha: 60%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
padding: 5px 10px;
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
|
||||
@include ellipsis();
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 25px;
|
||||
margin-top: 6px;
|
||||
line-height: 25px;
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--art-text-gray-600);
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
color: var(--art-gray-600);
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
margin: 0 15px;
|
||||
background-color: var(--art-border-dashed-color);
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
opacity: 0;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-notebook) {
|
||||
.article-list {
|
||||
.list {
|
||||
.offset {
|
||||
.item {
|
||||
width: calc(25% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-ipad-pro) {
|
||||
.article-list {
|
||||
.list {
|
||||
.offset {
|
||||
.item {
|
||||
width: calc(33.333% - 20px);
|
||||
|
||||
.bottom {
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-ipad) {
|
||||
.article-list {
|
||||
.list {
|
||||
.offset {
|
||||
.item {
|
||||
width: calc(50% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-phone) {
|
||||
.article-list {
|
||||
.list {
|
||||
.offset {
|
||||
.item {
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,359 @@
|
||||
<template>
|
||||
<div class="article-edit">
|
||||
<div>
|
||||
<div class="editor-wrap">
|
||||
<!-- 文章标题、类型 -->
|
||||
<ElRow :gutter="10">
|
||||
<ElCol :span="18">
|
||||
<ElInput
|
||||
v-model.trim="articleName"
|
||||
placeholder="请输入文章标题(最多100个字符)"
|
||||
maxlength="100"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElSelect v-model="articleType" placeholder="请选择文章类型" filterable>
|
||||
<ElOption
|
||||
v-for="item in articleTypes"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 富文本编辑器 -->
|
||||
<ArtWangEditor class="el-top" v-model="editorHtml" />
|
||||
|
||||
<div class="form-wrap">
|
||||
<h2>发布设置</h2>
|
||||
<!-- 图片上传 -->
|
||||
<ElForm>
|
||||
<ElFormItem label="封面">
|
||||
<div class="el-top upload-container">
|
||||
<ElUpload
|
||||
class="cover-uploader"
|
||||
:action="uploadImageUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="onSuccess"
|
||||
:on-error="onError"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<div v-if="!cover" class="upload-placeholder">
|
||||
<ElIcon class="upload-icon"><Plus /></ElIcon>
|
||||
<div class="upload-text">点击上传封面</div>
|
||||
</div>
|
||||
<img v-else :src="cover" class="cover-image" />
|
||||
</ElUpload>
|
||||
<div class="el-upload__tip">建议尺寸 16:9,jpg/png 格式</div>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="可见">
|
||||
<ElSwitch v-model="visible" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<div style="display: flex; justify-content: flex-end">
|
||||
<ElButton type="primary" @click="submit" style="width: 100px">
|
||||
{{ pageMode === PageModeEnum.Edit ? '保存' : '发布' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="outline-wrap">
|
||||
<div class="item" v-for="(item, index) in outlineList" :key="index">
|
||||
<p :class="`level${item.level}`">{{ item.text }}</p>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { ApiStatus } from '@/utils/http/status'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import EmojiText from '@/utils/ui/emojo'
|
||||
import { PageModeEnum } from '@/enums/formEnum'
|
||||
import axios from 'axios'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
|
||||
defineOptions({ name: 'ArticlePublish' })
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const userStore = useUserStore()
|
||||
let { accessToken } = userStore
|
||||
|
||||
// 上传路径
|
||||
const uploadImageUrl = `${import.meta.env.VITE_API_URL}/api/common/upload`
|
||||
// 传递 token
|
||||
const uploadHeaders = { Authorization: accessToken }
|
||||
|
||||
let pageMode: PageModeEnum = PageModeEnum.Add // 页面类型 新增 | 编辑
|
||||
const articleName = ref('') // 文章标题
|
||||
const articleType = ref() // 文章类型
|
||||
const articleTypes = ref() // 类型列表
|
||||
const editorHtml = ref('') // 编辑器内容
|
||||
const createDate = ref('') // 创建时间
|
||||
const cover = ref('') // 图片
|
||||
const visible = ref(true) // 可见
|
||||
// const outlineList = ref()
|
||||
|
||||
onMounted(() => {
|
||||
useCommon().scrollToTop()
|
||||
getArticleTypes()
|
||||
initPageMode()
|
||||
})
|
||||
|
||||
// 初始化页面类型 新增 | 编辑
|
||||
const initPageMode = () => {
|
||||
const { id } = route.query
|
||||
pageMode = id ? PageModeEnum.Edit : PageModeEnum.Add
|
||||
if (pageMode === PageModeEnum.Edit && id) {
|
||||
initEditArticle()
|
||||
} else {
|
||||
initAddArticle()
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化编辑文章的逻辑
|
||||
const initEditArticle = () => {
|
||||
getArticleDetail()
|
||||
}
|
||||
|
||||
// 初始化新增文章逻辑
|
||||
const initAddArticle = () => {
|
||||
createDate.value = formDate(useNow().value)
|
||||
}
|
||||
|
||||
// 获取文章类型
|
||||
const getArticleTypes = async () => {
|
||||
try {
|
||||
const response = await axios.get('https://www.qiniu.lingchen.kim/classify.json')
|
||||
if (response.data.code === 200) {
|
||||
articleTypes.value = response.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching JSON data:', error)
|
||||
}
|
||||
// try {
|
||||
// const res = await ArticleService.getArticleTypes({})
|
||||
// if (res.code === ApiStatus.success) {
|
||||
// articleTypes.value = res.data
|
||||
// }
|
||||
// } catch (err) { }
|
||||
}
|
||||
|
||||
const getArticleDetail = async () => {
|
||||
const res = await axios.get('https://www.qiniu.lingchen.kim/blog_list.json')
|
||||
|
||||
if (res.data.code === ApiStatus.success) {
|
||||
let { title, blog_class, html_content } = res.data.data
|
||||
articleName.value = title
|
||||
articleType.value = Number(blog_class)
|
||||
editorHtml.value = html_content
|
||||
}
|
||||
}
|
||||
|
||||
// const getOutline = (content: string) => {
|
||||
// const regex = /<h([1-3])>(.*?)<\/h\1>/g
|
||||
// const headings = []
|
||||
// let match
|
||||
|
||||
// while ((match = regex.exec(content)) !== null) {
|
||||
// headings.push({ level: match[1], text: match[2] })
|
||||
// }
|
||||
// outlineList.value = headings
|
||||
// }
|
||||
|
||||
// 提交
|
||||
const submit = () => {
|
||||
if (pageMode === PageModeEnum.Edit) {
|
||||
editArticle()
|
||||
} else {
|
||||
addArticle()
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formDate = (date: string | Date): string => {
|
||||
return useDateFormat(date, 'YYYY-MM-DD').value
|
||||
}
|
||||
|
||||
// 验证输入
|
||||
const validateArticle = () => {
|
||||
if (!articleName.value) {
|
||||
ElMessage.error(`请输入文章标题`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!articleType.value) {
|
||||
ElMessage.error(`请选择文章类型`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (editorHtml.value === '<p><br></p>') {
|
||||
ElMessage.error(`请输入文章内容`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!cover.value) {
|
||||
ElMessage.error(`请上传图片`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 添加文章
|
||||
const addArticle = async () => {
|
||||
try {
|
||||
if (!validateArticle()) return
|
||||
editorHtml.value = delCodeTrim(editorHtml.value)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑文章
|
||||
const editArticle = async () => {
|
||||
try {
|
||||
if (!validateArticle()) return
|
||||
|
||||
editorHtml.value = delCodeTrim(editorHtml.value)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
const delCodeTrim = (content: string): string => {
|
||||
return content.replace(/(\s*)<\/code>/g, '</code>')
|
||||
}
|
||||
|
||||
const onSuccess = (response: any) => {
|
||||
cover.value = response.data.url
|
||||
ElMessage.success(`图片上传成功 ${EmojiText[200]}`)
|
||||
}
|
||||
|
||||
const onError = () => {
|
||||
ElMessage.error(`图片上传失败 ${EmojiText[500]}`)
|
||||
}
|
||||
|
||||
// 添加上传前的校验
|
||||
const beforeUpload = (file: File) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('图片大小不能超过 2MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.article-edit {
|
||||
.editor-wrap {
|
||||
max-width: 1000px;
|
||||
margin: 20px auto;
|
||||
|
||||
.el-top {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.form-wrap {
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
background-color: var(--art-main-bg-color);
|
||||
border: 1px solid var(--art-border-color);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.outline-wrap {
|
||||
box-sizing: border-box;
|
||||
width: 280px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e3e3e3;
|
||||
border-radius: 8px;
|
||||
|
||||
.item {
|
||||
p {
|
||||
height: 30px;
|
||||
font-size: 13px;
|
||||
line-height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.level3 {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-container {
|
||||
.cover-uploader {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: var(--el-transition-duration);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 260px;
|
||||
height: 160px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
|
||||
.upload-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: #8c939d;
|
||||
}
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
display: block;
|
||||
width: 260px;
|
||||
height: 160px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="login register">
|
||||
<LoginLeftView></LoginLeftView>
|
||||
<div class="right-wrap">
|
||||
<div class="header">
|
||||
<ArtLogo class="icon" />
|
||||
<h1>{{ systemName }}</h1>
|
||||
</div>
|
||||
<div class="login-wrap">
|
||||
<div class="form">
|
||||
<h3 class="title">{{ $t('forgetPassword.title') }}</h3>
|
||||
<p class="sub-title">{{ $t('forgetPassword.subTitle') }}</p>
|
||||
<div class="input-wrap">
|
||||
<span class="input-label" v-if="showInputLabel">账号</span>
|
||||
<ElInput :placeholder="$t('forgetPassword.placeholder')" v-model.trim="username" />
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px">
|
||||
<ElButton
|
||||
class="login-btn"
|
||||
type="primary"
|
||||
@click="register"
|
||||
:loading="loading"
|
||||
v-ripple
|
||||
>
|
||||
{{ $t('forgetPassword.submitBtnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px">
|
||||
<ElButton class="back-btn" plain @click="toLogin">
|
||||
{{ $t('forgetPassword.backBtnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
defineOptions({ name: 'ForgetPassword' })
|
||||
|
||||
const router = useRouter()
|
||||
const showInputLabel = ref(false)
|
||||
|
||||
const systemName = AppConfig.systemInfo.name
|
||||
const username = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const register = async () => {}
|
||||
|
||||
const toLogin = () => {
|
||||
router.push(RoutesAlias.Login)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../login/index';
|
||||
</style>
|
||||
@@ -0,0 +1,233 @@
|
||||
@use '@styles/variables.scss' as *;
|
||||
|
||||
.login {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
|
||||
.el-input__inner {
|
||||
&:focus {
|
||||
border: 1px solid #4e83fd;
|
||||
}
|
||||
}
|
||||
|
||||
.el-input--medium .el-input__inner {
|
||||
height: var(--el-component-custom-height);
|
||||
line-height: var(--el-component-custom-height);
|
||||
}
|
||||
|
||||
.right-wrap {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
|
||||
.top-right-wrap {
|
||||
position: fixed;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
margin-left: 15px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.3s;
|
||||
|
||||
i {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--main-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-wrap {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 440px;
|
||||
height: 610px;
|
||||
padding: 0 5px;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
background-size: cover;
|
||||
border-radius: 5px;
|
||||
|
||||
.form {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
padding: 40px 0;
|
||||
widows: 100%;
|
||||
|
||||
.title {
|
||||
margin-left: -2px;
|
||||
font-size: 34px;
|
||||
font-weight: 600;
|
||||
color: var(--art-text-gray-900) !important;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: var(--art-text-gray-500) !important;
|
||||
}
|
||||
|
||||
.input-wrap {
|
||||
margin-top: 25px;
|
||||
|
||||
.input-label {
|
||||
display: block;
|
||||
padding-bottom: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--art-text-gray-800);
|
||||
}
|
||||
}
|
||||
|
||||
.account-select :deep(.el-select__wrapper),
|
||||
.el-input,
|
||||
.login-btn {
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
.drag-verify {
|
||||
position: relative;
|
||||
padding-bottom: 20px;
|
||||
margin-top: 25px;
|
||||
|
||||
.drag-verify-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.error {
|
||||
border-color: #f56c6c;
|
||||
}
|
||||
}
|
||||
|
||||
.error-text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: #f56c6c;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.show-error-text {
|
||||
transform: translateY(40px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.forget-password {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: var(--art-text-gray-500);
|
||||
|
||||
a {
|
||||
color: var(--main-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 40px !important;
|
||||
color: #fff;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
width: 100%;
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
color: var(--art-text-gray-800);
|
||||
|
||||
a {
|
||||
color: var(--main-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-ipad-pro) {
|
||||
.login {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
|
||||
.right-wrap {
|
||||
margin: auto;
|
||||
|
||||
.login-wrap {
|
||||
position: relative;
|
||||
width: 440px;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
|
||||
.form {
|
||||
margin-top: 10vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-phone) {
|
||||
.login {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
||||
.right-wrap {
|
||||
box-sizing: border-box;
|
||||
width: 100% !important;
|
||||
padding: 0 30px;
|
||||
margin: auto;
|
||||
|
||||
.login-wrap {
|
||||
width: 100%;
|
||||
|
||||
.form {
|
||||
margin-top: 12vh;
|
||||
|
||||
.input-wrap {
|
||||
.input-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.input-wrap,
|
||||
.drag-verify {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<LoginLeftView></LoginLeftView>
|
||||
|
||||
<div class="right-wrap">
|
||||
<div class="top-right-wrap">
|
||||
<div class="btn theme-btn" @click="toggleTheme">
|
||||
<i class="iconfont-sys">
|
||||
{{ isDark ? '' : '' }}
|
||||
</i>
|
||||
</div>
|
||||
<ElDropdown @command="changeLanguage" popper-class="langDropDownStyle">
|
||||
<div class="btn language-btn">
|
||||
<i class="iconfont-sys icon-language"></i>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<div v-for="lang in languageOptions" :key="lang.value" class="lang-btn-item">
|
||||
<ElDropdownItem
|
||||
:command="lang.value"
|
||||
:class="{ 'is-selected': locale === lang.value }"
|
||||
>
|
||||
<span class="menu-txt">{{ lang.label }}</span>
|
||||
<i v-if="locale === lang.value" class="iconfont-sys icon-check"></i>
|
||||
</ElDropdownItem>
|
||||
</div>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
<div class="header">
|
||||
<ArtLogo class="icon" />
|
||||
<h1>{{ systemName }}</h1>
|
||||
</div>
|
||||
<div class="login-wrap">
|
||||
<div class="form">
|
||||
<h3 class="title">{{ $t('login.title') }}</h3>
|
||||
<p class="sub-title">{{ $t('login.subTitle') }}</p>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
@keyup.enter="handleSubmit"
|
||||
style="margin-top: 25px"
|
||||
>
|
||||
<ElFormItem prop="account">
|
||||
<ElSelect v-model="formData.account" @change="setupAccount" class="account-select">
|
||||
<ElOption
|
||||
v-for="account in accounts"
|
||||
:key="account.key"
|
||||
:label="account.label"
|
||||
:value="account.key"
|
||||
>
|
||||
<span>{{ account.label }}</span>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="username">
|
||||
<ElInput :placeholder="$t('login.placeholder[0]')" v-model.trim="formData.username" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="password">
|
||||
<ElInput
|
||||
:placeholder="$t('login.placeholder[1]')"
|
||||
v-model.trim="formData.password"
|
||||
type="password"
|
||||
radius="8px"
|
||||
autocomplete="off"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
<div class="drag-verify">
|
||||
<div class="drag-verify-content" :class="{ error: !isPassing && isClickPass }">
|
||||
<ArtDragVerify
|
||||
ref="dragVerify"
|
||||
v-model:value="isPassing"
|
||||
:width="width < 500 ? 328 : 438"
|
||||
:text="$t('login.sliderText')"
|
||||
textColor="var(--art-gray-800)"
|
||||
:successText="$t('login.sliderSuccessText')"
|
||||
:progressBarBg="getCssVar('--el-color-primary')"
|
||||
background="var(--art-gray-200)"
|
||||
handlerBg="var(--art-main-bg-color)"
|
||||
/>
|
||||
</div>
|
||||
<p class="error-text" :class="{ 'show-error-text': !isPassing && isClickPass }">{{
|
||||
$t('login.placeholder[2]')
|
||||
}}</p>
|
||||
</div>
|
||||
|
||||
<div class="forget-password">
|
||||
<ElCheckbox v-model="formData.rememberPassword">{{
|
||||
$t('login.rememberPwd')
|
||||
}}</ElCheckbox>
|
||||
<RouterLink :to="RoutesAlias.ForgetPassword">{{ $t('login.forgetPwd') }}</RouterLink>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px">
|
||||
<ElButton
|
||||
class="login-btn"
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="loading"
|
||||
v-ripple
|
||||
>
|
||||
{{ $t('login.btnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>
|
||||
{{ $t('login.noAccount') }}
|
||||
<RouterLink :to="RoutesAlias.Register">{{ $t('login.register') }}</RouterLink>
|
||||
</p>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { ElNotification, ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { HOME_PAGE } from '@/router/routesAlias'
|
||||
import { getCssVar } from '@/utils/ui'
|
||||
import { languageOptions } from '@/locales'
|
||||
import { LanguageEnum, SystemThemeEnum } from '@/enums/appEnum'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { HttpError } from '@/utils/http/error'
|
||||
|
||||
defineOptions({ name: 'Login' })
|
||||
|
||||
const { t } = useI18n()
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
type AccountKey = 'super' | 'admin' | 'user'
|
||||
|
||||
export interface Account {
|
||||
key: AccountKey
|
||||
label: string
|
||||
userName: string
|
||||
password: string
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
const accounts = computed<Account[]>(() => [
|
||||
{
|
||||
key: 'super',
|
||||
label: t('login.roles.super'),
|
||||
userName: 'Super',
|
||||
password: '123456',
|
||||
roles: ['R_SUPER']
|
||||
},
|
||||
{
|
||||
key: 'admin',
|
||||
label: t('login.roles.admin'),
|
||||
userName: 'Admin',
|
||||
password: '123456',
|
||||
roles: ['R_ADMIN']
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
label: t('login.roles.user'),
|
||||
userName: 'User',
|
||||
password: '123456',
|
||||
roles: ['R_USER']
|
||||
}
|
||||
])
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const { isDark, systemThemeType } = storeToRefs(settingStore)
|
||||
|
||||
const dragVerify = ref()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const isPassing = ref(false)
|
||||
const isClickPass = ref(false)
|
||||
|
||||
const systemName = AppConfig.systemInfo.name
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const formData = reactive({
|
||||
account: '',
|
||||
username: '',
|
||||
password: '',
|
||||
rememberPassword: true
|
||||
})
|
||||
|
||||
const rules = computed<FormRules>(() => ({
|
||||
username: [{ required: true, message: t('login.placeholder[0]'), trigger: 'blur' }],
|
||||
password: [{ required: true, message: t('login.placeholder[1]'), trigger: 'blur' }]
|
||||
}))
|
||||
|
||||
const loading = ref(false)
|
||||
const { width } = useWindowSize()
|
||||
|
||||
onMounted(() => {
|
||||
setupAccount('super')
|
||||
})
|
||||
|
||||
// 设置账号
|
||||
const setupAccount = (key: AccountKey) => {
|
||||
const selectedAccount = accounts.value.find((account: Account) => account.key === key)
|
||||
formData.account = key
|
||||
formData.username = selectedAccount?.userName ?? ''
|
||||
formData.password = selectedAccount?.password ?? ''
|
||||
}
|
||||
|
||||
// 登录
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
// 表单验证
|
||||
const valid = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
// 拖拽验证
|
||||
if (!isPassing.value) {
|
||||
isClickPass.value = true
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
// 登录请求
|
||||
const { username, password } = formData
|
||||
|
||||
const { token, refreshToken } = await UserService.login({
|
||||
userName: username,
|
||||
password
|
||||
})
|
||||
|
||||
// 验证token
|
||||
if (!token) {
|
||||
throw new Error('Login failed - no token received')
|
||||
}
|
||||
|
||||
// 存储token和用户信息
|
||||
userStore.setToken(token, refreshToken)
|
||||
const userInfo = await UserService.getUserInfo()
|
||||
userStore.setUserInfo(userInfo)
|
||||
userStore.setLoginStatus(true)
|
||||
|
||||
// 登录成功处理
|
||||
showLoginSuccessNotice()
|
||||
router.push(HOME_PAGE)
|
||||
} catch (error) {
|
||||
// 处理 HttpError
|
||||
if (error instanceof HttpError) {
|
||||
// console.log(error.code)
|
||||
} else {
|
||||
// 处理非 HttpError
|
||||
ElMessage.error('登录失败,请稍后重试')
|
||||
console.error('[Login] Unexpected error:', error)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
resetDragVerify()
|
||||
}
|
||||
}
|
||||
|
||||
// 重置拖拽验证
|
||||
const resetDragVerify = () => {
|
||||
dragVerify.value.reset()
|
||||
}
|
||||
|
||||
// 登录成功提示
|
||||
const showLoginSuccessNotice = () => {
|
||||
setTimeout(() => {
|
||||
ElNotification({
|
||||
title: t('login.success.title'),
|
||||
type: 'success',
|
||||
duration: 2500,
|
||||
zIndex: 10000,
|
||||
message: `${t('login.success.message')}, ${systemName}!`
|
||||
})
|
||||
}, 150)
|
||||
}
|
||||
|
||||
// 切换语言
|
||||
const { locale } = useI18n()
|
||||
|
||||
const changeLanguage = (lang: LanguageEnum) => {
|
||||
if (locale.value === lang) return
|
||||
locale.value = lang
|
||||
userStore.setLanguage(lang)
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { UserService } from '@/api/usersApi'
|
||||
|
||||
const toggleTheme = () => {
|
||||
let { LIGHT, DARK } = SystemThemeEnum
|
||||
useTheme().switchThemeStyles(systemThemeType.value === LIGHT ? DARK : LIGHT)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './index';
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
.register {
|
||||
.right-wrap {
|
||||
.login-wrap {
|
||||
.form {
|
||||
.el-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.privacy-policy {
|
||||
margin-top: 15px;
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--main-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.register-btn {
|
||||
width: 100%;
|
||||
height: 40px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div class="login register">
|
||||
<LoginLeftView></LoginLeftView>
|
||||
<div class="right-wrap">
|
||||
<div class="header">
|
||||
<ArtLogo class="icon" />
|
||||
<h1>{{ systemName }}</h1>
|
||||
</div>
|
||||
<div class="login-wrap">
|
||||
<div class="form">
|
||||
<h3 class="title">{{ $t('register.title') }}</h3>
|
||||
<p class="sub-title">{{ $t('register.subTitle') }}</p>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-position="top">
|
||||
<ElFormItem prop="username">
|
||||
<ElInput
|
||||
v-model.trim="formData.username"
|
||||
:placeholder="$t('register.placeholder[0]')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="password">
|
||||
<ElInput
|
||||
v-model.trim="formData.password"
|
||||
:placeholder="$t('register.placeholder[1]')"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="confirmPassword">
|
||||
<ElInput
|
||||
v-model.trim="formData.confirmPassword"
|
||||
:placeholder="$t('register.placeholder[2]')"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
@keyup.enter="register"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem prop="agreement">
|
||||
<ElCheckbox v-model="formData.agreement">
|
||||
{{ $t('register.agreeText') }}
|
||||
<router-link
|
||||
style="color: var(--main-color); text-decoration: none"
|
||||
to="/privacy-policy"
|
||||
>{{ $t('register.privacyPolicy') }}</router-link
|
||||
>
|
||||
</ElCheckbox>
|
||||
</ElFormItem>
|
||||
|
||||
<div style="margin-top: 15px">
|
||||
<ElButton
|
||||
class="register-btn"
|
||||
type="primary"
|
||||
@click="register"
|
||||
:loading="loading"
|
||||
v-ripple
|
||||
>
|
||||
{{ $t('register.submitBtnText') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>
|
||||
{{ $t('register.hasAccount') }}
|
||||
<router-link :to="RoutesAlias.Login">{{ $t('register.toLogin') }}</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineOptions({ name: 'Register' })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const router = useRouter()
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const systemName = AppConfig.systemInfo.name
|
||||
const loading = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
agreement: false
|
||||
})
|
||||
|
||||
const validatePass = (rule: any, value: string, callback: any) => {
|
||||
if (value === '') {
|
||||
callback(new Error(t('register.placeholder[1]')))
|
||||
} else {
|
||||
if (formData.confirmPassword !== '') {
|
||||
formRef.value?.validateField('confirmPassword')
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const validatePass2 = (rule: any, value: string, callback: any) => {
|
||||
if (value === '') {
|
||||
callback(new Error(t('register.rule[0]')))
|
||||
} else if (value !== formData.password) {
|
||||
callback(new Error(t('register.rule[1]')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
username: [
|
||||
{ required: true, message: t('register.placeholder[0]'), trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: t('register.rule[2]'), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, validator: validatePass, trigger: 'blur' },
|
||||
{ min: 6, message: t('register.rule[3]'), trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [{ required: true, validator: validatePass2, trigger: 'blur' }],
|
||||
agreement: [
|
||||
{
|
||||
validator: (rule: any, value: boolean, callback: any) => {
|
||||
if (!value) {
|
||||
callback(new Error(t('register.rule[4]')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const register = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
loading.value = true
|
||||
|
||||
// 模拟注册请求
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
ElMessage.success('注册成功')
|
||||
toLogin()
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
console.log('验证失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const toLogin = () => {
|
||||
setTimeout(() => {
|
||||
router.push(RoutesAlias.Login)
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '../login/index' as login;
|
||||
@use './index' as register;
|
||||
</style>
|
||||
@@ -0,0 +1,344 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput placeholder="部门名称"></ElInput>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple>搜索</ElButton>
|
||||
<ElButton @click="showDialog('add')" v-ripple>新增角色</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable :data="roleList" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="角色名称" prop="roleName" />
|
||||
<ElTableColumn label="角色编码" prop="roleCode" />
|
||||
<ElTableColumn label="描述" prop="des" />
|
||||
<ElTableColumn label="启用" prop="enable">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.enable ? 'primary' : 'info'">
|
||||
{{ scope.row.enable ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="date">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.date) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn fixed="right" label="操作" width="200px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button link @click="showPermissionDialog()"> 菜单权限 </el-button>
|
||||
<el-button link @click="showDialog('edit', scope.row)"> 编辑 </el-button>
|
||||
<el-button link @click="deleteRole()"> 删除 </el-button>
|
||||
<!-- <ArtButtonMore
|
||||
:list="[
|
||||
{ key: 'permission', label: '菜单权限' },
|
||||
{ key: 'edit', label: '编辑角色' },
|
||||
{ key: 'delete', label: '删除角色' }
|
||||
]"
|
||||
@click="buttonMoreClick($event, scope.row)"
|
||||
/> -->
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增角色' : '编辑角色'"
|
||||
width="30%"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="角色名称" prop="roleName">
|
||||
<ElInput v-model="form.roleName" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="角色编码" prop="roleCode">
|
||||
<ElInput v-model="form.roleCode" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="描述" prop="roleStatus">
|
||||
<ElInput v-model="form.des" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="启用">
|
||||
<ElSwitch v-model="form.enable" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<ElDialog
|
||||
v-model="permissionDialog"
|
||||
title="菜单权限"
|
||||
width="520px"
|
||||
align-center
|
||||
class="el-dialog-border"
|
||||
>
|
||||
<ElScrollbar height="70vh">
|
||||
<ElTree
|
||||
ref="treeRef"
|
||||
:data="processedMenuList"
|
||||
show-checkbox
|
||||
node-key="name"
|
||||
:default-expand-all="isExpandAll"
|
||||
:default-checked-keys="[1, 2, 3]"
|
||||
:props="defaultProps"
|
||||
@check="handleTreeCheck"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div style="display: flex; align-items: center">
|
||||
<span v-if="data.isAuth">
|
||||
{{ data.label }}
|
||||
</span>
|
||||
<span v-else>{{ defaultProps.label(data) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElTree>
|
||||
</ElScrollbar>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton>
|
||||
<ElButton @click="toggleSelectAll" style="margin-left: 8px">{{
|
||||
isSelectAll ? '取消全选' : '全部选择'
|
||||
}}</ElButton>
|
||||
<ElButton type="primary" @click="savePermission">保存</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { formatMenuTitle } from '@/router/utils/utils'
|
||||
// import { ButtonMoreItem } from '@/components/core/forms/ArtButtonMore.vue'
|
||||
import { Role, ROLE_LIST_DATA } from '@/mock/temp/formData'
|
||||
|
||||
defineOptions({ name: 'Role' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const permissionDialog = ref(false)
|
||||
const { menuList } = storeToRefs(useMenuStore())
|
||||
const treeRef = ref()
|
||||
const isExpandAll = ref(true)
|
||||
const isSelectAll = ref(false)
|
||||
|
||||
// 处理菜单数据,将 authList 转换为子节点
|
||||
const processedMenuList = computed(() => {
|
||||
const processNode = (node: any) => {
|
||||
const processed = { ...node }
|
||||
|
||||
// 如果有 authList,将其转换为子节点
|
||||
if (node.meta && node.meta.authList && node.meta.authList.length) {
|
||||
const authNodes = node.meta.authList.map((auth: any) => ({
|
||||
id: `${node.id}_${auth.auth_mark}`,
|
||||
name: `${node.name}_${auth.auth_mark}`,
|
||||
label: auth.title,
|
||||
auth_mark: auth.auth_mark,
|
||||
isAuth: true,
|
||||
checked: auth.checked || false
|
||||
}))
|
||||
|
||||
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
|
||||
}
|
||||
|
||||
// 递归处理子节点
|
||||
if (processed.children) {
|
||||
processed.children = processed.children.map(processNode)
|
||||
}
|
||||
|
||||
return processed
|
||||
}
|
||||
|
||||
return menuList.value.map(processNode)
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: '请输入角色名称', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
des: [{ required: true, message: '请输入角色描述', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const form = reactive<Role>({
|
||||
roleName: '',
|
||||
roleCode: '',
|
||||
des: '',
|
||||
date: '',
|
||||
enable: true
|
||||
})
|
||||
|
||||
const roleList = ref<Role[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
|
||||
const getTableData = () => {
|
||||
roleList.value = ROLE_LIST_DATA
|
||||
}
|
||||
|
||||
const dialogType = ref('add')
|
||||
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
form.roleName = row.roleName
|
||||
form.roleCode = row.roleCode
|
||||
form.des = row.des
|
||||
form.date = row.date
|
||||
form.enable = row.enable
|
||||
} else {
|
||||
form.roleName = ''
|
||||
form.roleCode = ''
|
||||
form.des = ''
|
||||
form.date = ''
|
||||
form.enable = true
|
||||
}
|
||||
}
|
||||
|
||||
// const buttonMoreClick = (item: ButtonMoreItem, row: any) => {
|
||||
// if (item.key === 'permission') {
|
||||
// showPermissionDialog()
|
||||
// } else if (item.key === 'edit') {
|
||||
// showDialog('edit', row)
|
||||
// } else if (item.key === 'delete') {
|
||||
// deleteRole()
|
||||
// }
|
||||
// }
|
||||
|
||||
const showPermissionDialog = () => {
|
||||
permissionDialog.value = true
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
children: 'children',
|
||||
label: (data: any) => formatMenuTitle(data.meta?.title) || ''
|
||||
}
|
||||
|
||||
const deleteRole = () => {
|
||||
ElMessageBox.confirm('确定删除该角色吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
const message = dialogType.value === 'add' ? '新增成功' : '修改成功'
|
||||
ElMessage.success(message)
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const savePermission = () => {
|
||||
ElMessage.success('权限保存成功')
|
||||
permissionDialog.value = false
|
||||
}
|
||||
|
||||
const toggleExpandAll = () => {
|
||||
const tree = treeRef.value
|
||||
if (!tree) return
|
||||
|
||||
// 使用store.nodesMap直接控制所有节点的展开状态
|
||||
const nodes = tree.store.nodesMap
|
||||
for (const node in nodes) {
|
||||
nodes[node].expanded = !isExpandAll.value
|
||||
}
|
||||
|
||||
isExpandAll.value = !isExpandAll.value
|
||||
}
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
const tree = treeRef.value
|
||||
if (!tree) return
|
||||
|
||||
if (!isSelectAll.value) {
|
||||
// 全选:获取所有节点的key并设置为选中
|
||||
const allKeys = getAllNodeKeys(processedMenuList.value)
|
||||
tree.setCheckedKeys(allKeys)
|
||||
} else {
|
||||
// 取消全选:清空所有选中
|
||||
tree.setCheckedKeys([])
|
||||
}
|
||||
|
||||
isSelectAll.value = !isSelectAll.value
|
||||
}
|
||||
|
||||
const getAllNodeKeys = (nodes: any[]): string[] => {
|
||||
const keys: string[] = []
|
||||
const traverse = (nodeList: any[]) => {
|
||||
nodeList.forEach((node) => {
|
||||
if (node.name) {
|
||||
keys.push(node.name)
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
traverse(node.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
traverse(nodes)
|
||||
return keys
|
||||
}
|
||||
|
||||
const handleTreeCheck = () => {
|
||||
const tree = treeRef.value
|
||||
if (!tree) return
|
||||
|
||||
// 使用树组件的getCheckedKeys方法获取选中的节点
|
||||
const checkedKeys = tree.getCheckedKeys()
|
||||
const allKeys = getAllNodeKeys(processedMenuList.value)
|
||||
|
||||
// 判断是否全选:选中的节点数量等于总节点数量
|
||||
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
|
||||
}
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date)
|
||||
.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
.replace(/\//g, '-')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.svg-icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
overflow: hidden;
|
||||
vertical-align: -8px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,685 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="account-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" @refresh="handleRefresh">
|
||||
<template #left>
|
||||
<ElButton @click="showDialog('add')">新增</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '添加' : '编辑'"
|
||||
width="60%"
|
||||
align-center
|
||||
class="custom-dialog-scroll"
|
||||
style="max-height:80vh;overflow:auto;"
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
|
||||
<el-tab-pane label="基本信息" name="first">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="12">
|
||||
<ElFormItem label="产品名称" prop="name">
|
||||
<ElInput v-model="formData.name" />
|
||||
</ElFormItem>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12">
|
||||
<ElFormItem label="封面图" prop="phone">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<el-upload
|
||||
ref="upload"
|
||||
v-model:file-list="fileList"
|
||||
class="upload-demo"
|
||||
action="https://usedcar.server.hnxinyouhe.com/console/upload/image"
|
||||
name="image"
|
||||
:on-success="handleCoverSuccess"
|
||||
:show-file-list="false"
|
||||
>
|
||||
<el-button type="primary">点击上传</el-button>
|
||||
</el-upload>
|
||||
<img
|
||||
v-if="coverImageUrl"
|
||||
:src="coverImageUrl"
|
||||
alt="封面图"
|
||||
style="width: 40px; height: 40px; margin-left: 16px; border-radius: 4px; object-fit: cover;"
|
||||
/>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12">
|
||||
<ElFormItem label="车辆识别码" prop="vin">
|
||||
<ElInput v-model="formData.vin" />
|
||||
</ElFormItem>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12">
|
||||
<ElFormItem label="售价" prop="price">
|
||||
<ElInput v-model="formData.price" />
|
||||
</ElFormItem>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12">
|
||||
<ElFormItem label="品牌" prop="brand">
|
||||
<ElSelect v-model="formData.brand" placeholder="请选择品牌" clearable>
|
||||
<ElOption
|
||||
v-for="brand in brandList"
|
||||
:key="brand.id"
|
||||
:value="brand.name"
|
||||
:label="brand.name"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12">
|
||||
<ElFormItem label="型号" prop="model">
|
||||
<ElInput v-model="formData.model" />
|
||||
</ElFormItem>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12">
|
||||
<ElFormItem label="类型" prop="type">
|
||||
<ElInput v-model="formData.type" />
|
||||
</ElFormItem>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12">
|
||||
<ElFormItem label="能源" prop="energy">
|
||||
<ElInput v-model="formData.energy" />
|
||||
</ElFormItem>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12">
|
||||
<ElFormItem label="库存状态" prop="store_status">
|
||||
<ElSelect v-model="formData.store_status">
|
||||
<ElOption label="男" value="男" />
|
||||
<ElOption label="女" value="女" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12">
|
||||
<ElFormItem label="里程" prop="mileage">
|
||||
<ElInput v-model="formData.mileage" />
|
||||
</ElFormItem>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12">
|
||||
<ElFormItem label="车龄(年)" prop="vehicle_age">
|
||||
<ElInput v-model="formData.vehicle_age" />
|
||||
</ElFormItem>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12">
|
||||
<ElFormItem label="位置" prop="location">
|
||||
<ElSelect v-model="formData.location">
|
||||
<ElOption
|
||||
v-for="role in roleList"
|
||||
:key="role.roleCode"
|
||||
:value="role.roleCode"
|
||||
:label="role.roleName"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="24">
|
||||
<ElFormItem label="车况描述" prop="condition_desc">
|
||||
<ElInput v-model="formData.condition_desc" :rows="4" type="textarea" />
|
||||
</ElFormItem>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="车辆图册" name="second">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8" v-for="(url, part) in carAlbum" :key="part">
|
||||
<div style="margin-bottom: 16px; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||
<div style="margin-bottom: 8px; text-align: center;">{{ part }}</div>
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:action="'https://usedcar.server.hnxinyouhe.com/console/upload/image'"
|
||||
name="image"
|
||||
:on-success="(res, file) => handleAlbumSuccess(res, file, part)"
|
||||
>
|
||||
<el-button type="primary" size="small">上传</el-button>
|
||||
</el-upload>
|
||||
<img
|
||||
v-if="carAlbum[part]"
|
||||
:src="carAlbum[part]"
|
||||
style="display:block;width:60px;height:60px;margin-top:8px;object-fit:cover;border-radius:4px;"
|
||||
/>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ROLE_LIST_DATA, ACCOUNT_TABLE_DATA } from '@/mock/temp/formData'
|
||||
|
||||
import { ElDialog, FormInstance, ElTag } from 'element-plus'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { UserService } from '@/api/usersApi'
|
||||
import { BrandService } from '@/api/carApi'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
import type { TabsPaneContext } from 'element-plus'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const activeName = ref('first')
|
||||
|
||||
const handleClick = (tab: TabsPaneContext, event: Event) => {
|
||||
console.log(tab, event)
|
||||
}
|
||||
const { width } = useWindowSize()
|
||||
|
||||
defineOptions({ name: 'User' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
name: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
level: '',
|
||||
email: '',
|
||||
date: '',
|
||||
daterange: '',
|
||||
status: '1'
|
||||
}
|
||||
|
||||
const roleList = ref<any[]>([])
|
||||
const brandList = ref<any[]>([])
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1 // 重置到第一页
|
||||
getUserList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1 // 搜索时重置到第一页
|
||||
getUserList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '用户名',
|
||||
prop: 'name',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
|
||||
{
|
||||
label: '电话',
|
||||
prop: 'phone',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '用户等级',
|
||||
prop: 'level',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true
|
||||
},
|
||||
options: () => [
|
||||
{ label: '普通用户', value: 'normal' },
|
||||
{ label: 'VIP用户', value: 'vip' },
|
||||
{ label: '高级VIP', value: 'svip' },
|
||||
{ label: '企业用户', value: 'enterprise', disabled: true }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '地址',
|
||||
prop: 'address',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '邮箱',
|
||||
prop: 'email',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
// 支持 9 种日期类型定义
|
||||
// 具体可参考 src/components/core/forms/art-search-bar/widget/art-search-date/README.md
|
||||
{
|
||||
prop: 'date',
|
||||
label: '日期',
|
||||
type: 'date',
|
||||
config: {
|
||||
type: 'date',
|
||||
placeholder: '请选择日期'
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'daterange',
|
||||
label: '日期范围',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'radio',
|
||||
options: [
|
||||
{ label: '在线', value: '1' },
|
||||
{ label: '离线', value: '2' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 获取标签类型
|
||||
// 1: 在线 2: 离线 3: 异常 4: 注销
|
||||
const getTagType = (status: string) => {
|
||||
switch (status) {
|
||||
case '1':
|
||||
return 'success'
|
||||
case '2':
|
||||
return 'info'
|
||||
case '3':
|
||||
return 'warning'
|
||||
case '4':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 构建标签文本
|
||||
const buildTagText = (status: string) => {
|
||||
let text = ''
|
||||
if (status === '1') {
|
||||
text = '在线'
|
||||
} else if (status === '2') {
|
||||
text = '离线'
|
||||
} else if (status === '3') {
|
||||
text = '异常'
|
||||
} else if (status === '4') {
|
||||
text = '注销'
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
// 重置表单验证状态
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
formData.name = row.username
|
||||
} else {
|
||||
formData.name=''
|
||||
formData.cover=''
|
||||
formData.vin=''
|
||||
formData.price=''
|
||||
formData.brand=''
|
||||
formData.model=''
|
||||
formData.type=''
|
||||
formData.energy=''
|
||||
formData.store_status=''
|
||||
formData.mileage=''
|
||||
formData.vehicle_age=''
|
||||
formData.location=''
|
||||
formData.condition_desc=''
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const deleteUser = () => {
|
||||
ElMessageBox.confirm('确定要注销该用户吗?', '注销用户', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
ElMessage.success('注销成功')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' }, // 勾选列
|
||||
// { type: 'expand', label: '展开', width: 80 }, // 展开列
|
||||
// { type: 'index', label: '序号', width: 80 }, // 序号列
|
||||
{
|
||||
prop: 'avatar',
|
||||
label: '用户名',
|
||||
minWidth: width.value < 500 ? 220 : '',
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'user', style: 'display: flex; align-items: center' }, [
|
||||
h('img', { class: 'avatar', src: row.avatar }),
|
||||
h('div', {}, [
|
||||
h('p', { class: 'user-name' }, row.userName),
|
||||
h('p', { class: 'email' }, row.userEmail)
|
||||
])
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'userGender',
|
||||
label: '性别',
|
||||
sortable: true,
|
||||
formatter: (row) => (row.userGender === 1 ? '男' : '女')
|
||||
},
|
||||
{ prop: 'userPhone', label: '手机号' },
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getTagType(row.status) }, () => buildTagText(row.status))
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'createTime',
|
||||
label: '创建日期',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 150,
|
||||
// fixed: 'right', // 固定在右侧
|
||||
// disabled: true,
|
||||
formatter: (row: any) => {
|
||||
return h('div', [
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteUser()
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
cover: '',
|
||||
vin: '',
|
||||
price: '',
|
||||
brand: '',
|
||||
model: '',
|
||||
type: '',
|
||||
energy: '',
|
||||
store_status: '',
|
||||
mileage: '',
|
||||
vehicle_age: '',
|
||||
location: '',
|
||||
condition_desc: ''
|
||||
})
|
||||
|
||||
const fileList = ref<any[]>([])
|
||||
const coverImageUrl = ref('')
|
||||
|
||||
const handleCoverSuccess = (response: any, file: any) => {
|
||||
coverImageUrl.value = response.data.image_url || ''
|
||||
// 上传成功后清空 fileList,允许再次上传
|
||||
fileList.value = []
|
||||
}
|
||||
|
||||
defineExpose({ handleCoverSuccess })
|
||||
|
||||
onMounted(() => {
|
||||
getUserList()
|
||||
getRoleList()
|
||||
getBrandList()
|
||||
})
|
||||
|
||||
// 获取用户列表数据
|
||||
const getUserList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
|
||||
const { records, current, size, total } = await UserService.getUserList({
|
||||
current: currentPage,
|
||||
size: pageSize
|
||||
})
|
||||
|
||||
// 使用本地头像替换接口返回的头像
|
||||
tableData.value = records.map((item: any, index: number) => ({
|
||||
...item,
|
||||
avatar: ACCOUNT_TABLE_DATA[index % ACCOUNT_TABLE_DATA.length].avatar
|
||||
}))
|
||||
|
||||
// 更新分页信息
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getRoleList = () => {
|
||||
roleList.value = ROLE_LIST_DATA
|
||||
}
|
||||
|
||||
// 获取品牌列表
|
||||
const getBrandList = async () => {
|
||||
try {
|
||||
// 这里调用获取品牌列表的接口
|
||||
const response = await BrandService.getBrandInfo()
|
||||
brandList.value = response.data || []
|
||||
|
||||
// 临时使用模拟数据,实际使用时请替换为真实接口
|
||||
// brandList.value = [
|
||||
// { id: 1, name: '奔驰' },
|
||||
// { id: 2, name: '宝马' },
|
||||
// { id: 3, name: '奥迪' },
|
||||
// { id: 4, name: '大众' },
|
||||
// { id: 5, name: '丰田' },
|
||||
// { id: 6, name: '本田' },
|
||||
// { id: 7, name: '日产' },
|
||||
// { id: 8, name: '现代' },
|
||||
// { id: 9, name: '起亚' },
|
||||
// { id: 10, name: '福特' }
|
||||
// ]
|
||||
} catch (error) {
|
||||
console.error('获取品牌列表失败:', error)
|
||||
ElMessage.error('获取品牌列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getUserList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
||||
],
|
||||
gender: [{ required: true, message: '请选择性别', trigger: 'change' }],
|
||||
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '添加成功' : '更新成功')
|
||||
dialogVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getUserList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getUserList()
|
||||
}
|
||||
|
||||
const carAlbum = reactive<Record<string, string>>({
|
||||
'45度正脸': '',
|
||||
'左前45度': '',
|
||||
'左前大灯': '',
|
||||
'正前': '',
|
||||
'车头下部': ''
|
||||
// 可继续添加更多部位
|
||||
})
|
||||
|
||||
const handleAlbumSuccess = (response: any, file: any, part: string) => {
|
||||
carAlbum[part] = response.data?.image_url || ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.account-page {
|
||||
:deep(.user) {
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
> div {
|
||||
margin-left: 10px;
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
color: var(--art-text-gray-800);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-dialog-scroll .el-dialog__body {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.avatar-uploader .el-upload {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
}
|
||||
|
||||
.avatar-uploader .el-upload:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.el-icon.avatar-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h3 class="table-title"><i class="iconfont-sys"></i>更新日志</h3>
|
||||
|
||||
<ArtTable :data="upgradeLogList" :pagination="false">
|
||||
<ElTableColumn label="版本号" prop="version" width="200" />
|
||||
<ElTableColumn label="内容">
|
||||
<template #default="scope">
|
||||
<div class="title">{{ scope.row.title }}</div>
|
||||
<div v-if="scope.row.detail" style="margin-top: 10px">
|
||||
<div class="detail-item" v-for="(item, index) in scope.row.detail" :key="index">
|
||||
{{ index + 1 }}. {{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="时间" prop="date" />
|
||||
</ArtTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ArtTable from '@/components/core/tables/ArtTable.vue'
|
||||
import { upgradeLogList } from '@/mock/upgrade/changeLog'
|
||||
|
||||
defineOptions({ name: 'ChangeLog' })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.table-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
|
||||
i {
|
||||
margin-right: 10px;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--art-gray-800);
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
color: var(--art-gray-600);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="analysis-dashboard">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xl="14" :lg="15" :xs="24">
|
||||
<TodaySales />
|
||||
</el-col>
|
||||
<el-col :xl="10" :lg="9" :xs="24">
|
||||
<VisitorInsights />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="mt-20">
|
||||
<el-col :xl="10" :lg="10" :xs="24">
|
||||
<TotalRevenue />
|
||||
</el-col>
|
||||
<el-col :xl="7" :lg="7" :xs="24">
|
||||
<CustomerSatisfaction />
|
||||
</el-col>
|
||||
<el-col :xl="7" :lg="7" :xs="24">
|
||||
<TargetVsReality />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="mt-20">
|
||||
<el-col :xl="10" :lg="10" :xs="24">
|
||||
<TopProducts />
|
||||
</el-col>
|
||||
<el-col :xl="7" :lg="7" :xs="24">
|
||||
<SalesMappingByCountry />
|
||||
</el-col>
|
||||
<el-col :xl="7" :lg="7" :xs="24">
|
||||
<VolumeServiceLevel />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TodaySales from './widget/TodaySales.vue'
|
||||
import VisitorInsights from './widget/VisitorInsights.vue'
|
||||
import TotalRevenue from './widget/TotalRevenue.vue'
|
||||
import CustomerSatisfaction from './widget/CustomerSatisfaction.vue'
|
||||
import TargetVsReality from './widget/TargetVsReality.vue'
|
||||
import TopProducts from './widget/TopProducts.vue'
|
||||
import SalesMappingByCountry from './widget/SalesMappingByCountry.vue'
|
||||
import VolumeServiceLevel from './widget/VolumeServiceLevel.vue'
|
||||
|
||||
defineOptions({ name: 'Analysis' })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './style';
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
.analysis-dashboard {
|
||||
padding-bottom: 20px;
|
||||
|
||||
:deep(.custom-card) {
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: calc(var(--custom-radius) + 4px) !important;
|
||||
}
|
||||
|
||||
// 卡片头部
|
||||
:deep(.custom-card-header) {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 20px;
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
color: var(--art-text-gray-900);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
left: 21px;
|
||||
font-size: 13px;
|
||||
color: var(--art-gray-600);
|
||||
}
|
||||
}
|
||||
|
||||
.el-card {
|
||||
border: 1px solid #e8ebf1;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.analysis-dashboard {
|
||||
:deep(.custom-card) {
|
||||
box-shadow: 0 4px 20px rgb(0 0 0 / 50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 1200px) {
|
||||
.analysis-dashboard {
|
||||
.mt-20 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
:deep(.custom-card) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="custom-card art-custom-card customer-satisfaction">
|
||||
<div class="custom-card-header">
|
||||
<span class="title">{{ t('analysis.customerSatisfaction.title') }}</span>
|
||||
</div>
|
||||
<div class="custom-card-body">
|
||||
<div ref="chartRef" style="height: 300px; margin-top: 10px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
const { t } = useI18n()
|
||||
|
||||
const { chartRef, isDark, initChart } = useChart()
|
||||
|
||||
const options: () => echarts.EChartsOption = () => ({
|
||||
grid: {
|
||||
top: 30,
|
||||
right: 20,
|
||||
bottom: 50,
|
||||
left: 20,
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
confine: true
|
||||
},
|
||||
legend: {
|
||||
data: [
|
||||
t('analysis.customerSatisfaction.legend.lastMonth'),
|
||||
t('analysis.customerSatisfaction.legend.thisMonth')
|
||||
],
|
||||
bottom: 0,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: isDark.value ? '#808290' : '#222B45'
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['Week 0', 'Week 1', 'Week 2', 'Week 3', 'Week 4', 'Week 5', 'Week 6'],
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { show: false } // 隐藏 x 轴标签
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { show: false },
|
||||
splitLine: {
|
||||
show: false // 将 show 设置为 false 以去除水平线条
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: t('analysis.customerSatisfaction.legend.lastMonth'),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [1800, 2800, 1800, 2300, 2600, 2500, 3000],
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(0,157,255,0.33)' },
|
||||
{ offset: 1, color: 'rgba(255,255,255,0)' }
|
||||
])
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#0086E1'
|
||||
},
|
||||
symbol: 'none',
|
||||
itemStyle: {
|
||||
color: '#0095FF'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t('analysis.customerSatisfaction.legend.thisMonth'),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [4000, 3500, 4300, 3700, 4500, 3500, 4000],
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(147,241,180,0.33)' },
|
||||
{ offset: 1, color: 'rgba(255,255,255,0)' }
|
||||
])
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#14DEB9'
|
||||
},
|
||||
symbol: 'none',
|
||||
itemStyle: {
|
||||
color: '#14DEB9'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChart(options())
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.custom-card {
|
||||
height: 400px;
|
||||
|
||||
&-body {
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-notebook) {
|
||||
.custom-card {
|
||||
height: 350px;
|
||||
|
||||
&-body {
|
||||
> div {
|
||||
height: 260px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="custom-card art-custom-card sales-mapping-country">
|
||||
<div class="custom-card-header">
|
||||
<span class="title">{{ t('analysis.salesMappingCountry.title') }}</span>
|
||||
</div>
|
||||
<div class="custom-card-body">
|
||||
<div ref="chartRef" class="sales-mapping-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { chartRef, initChart, isDark } = useChart()
|
||||
|
||||
const chartData = [
|
||||
{ value: 1048, name: 'Beijing', itemStyle: { color: 'rgba(99, 102, 241, 0.9)' } },
|
||||
{ value: 735, name: 'Shanghai', itemStyle: { color: 'rgba(134, 239, 172, 0.9)' } },
|
||||
{ value: 580, name: 'Guangzhou', itemStyle: { color: 'rgba(253, 224, 71, 0.9)' } },
|
||||
{ value: 484, name: 'Shenzhen', itemStyle: { color: 'rgba(248, 113, 113, 0.9)' } },
|
||||
{ value: 300, name: 'Chengdu', itemStyle: { color: 'rgba(125, 211, 252, 0.9)' } }
|
||||
]
|
||||
|
||||
const options: () => EChartsOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Sales Mapping',
|
||||
type: 'pie',
|
||||
radius: ['40%', '60%'],
|
||||
avoidLabelOverlap: false,
|
||||
padAngle: 5,
|
||||
itemStyle: {
|
||||
borderRadius: 10
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: chartData
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChart(options())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sales-mapping-country {
|
||||
height: 330px;
|
||||
|
||||
.sales-mapping-chart {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<div class="custom-card art-custom-card target-vs-reality">
|
||||
<div class="custom-card-header">
|
||||
<span class="title">{{ t('analysis.targetVsReality.title') }}</span>
|
||||
</div>
|
||||
<div class="custom-card-body">
|
||||
<div ref="chartRef" style="height: 160px"></div>
|
||||
</div>
|
||||
<div class="custom-card-footer">
|
||||
<div class="total-item">
|
||||
<div class="label">
|
||||
<i class="iconfont-sys"></i>
|
||||
<div class="label-text">
|
||||
<span>{{ t('analysis.targetVsReality.realitySales.label') }}</span>
|
||||
<span>{{ t('analysis.targetVsReality.realitySales.sublabel') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="value text-color-green">8,823</div>
|
||||
</div>
|
||||
<div class="total-item">
|
||||
<div class="label">
|
||||
<i class="iconfont-sys"></i>
|
||||
<div class="label-text">
|
||||
<span>{{ t('analysis.targetVsReality.targetSales.label') }}</span>
|
||||
<span>{{ t('analysis.targetVsReality.targetSales.sublabel') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="value text-color-orange">12,122</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { EChartsOption } from 'echarts'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
|
||||
const { chartRef, isDark, initChart } = useChart()
|
||||
|
||||
const options: () => EChartsOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 10,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'June', 'July'],
|
||||
axisLabel: {
|
||||
color: '#7B91B0'
|
||||
},
|
||||
axisLine: {
|
||||
show: false // 隐藏 x 轴线
|
||||
},
|
||||
axisTick: {
|
||||
show: false // 隐藏刻度线
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
show: false // 隐藏 y 轴文字
|
||||
},
|
||||
splitLine: {
|
||||
show: false // 隐藏 y 轴分割线
|
||||
},
|
||||
axisLine: {
|
||||
show: false // 隐藏 y 轴线
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Reality Sales',
|
||||
type: 'bar',
|
||||
data: [8000, 7000, 6000, 8500, 9000, 10000, 9500],
|
||||
barWidth: '15',
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
color: '#2B8DFA'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Target Sales',
|
||||
type: 'bar',
|
||||
data: [10000, 9000, 11000, 10000, 12000, 12500, 11500],
|
||||
barWidth: '15',
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 4, 4],
|
||||
color: '#95E0FB'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChart(options())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-card {
|
||||
height: 400px;
|
||||
|
||||
&-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
box-sizing: border-box;
|
||||
padding: 0 20px;
|
||||
margin-top: 15px;
|
||||
|
||||
.total-item {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
|
||||
&:first-of-type .label .iconfont-sys {
|
||||
color: #2b8dfa !important;
|
||||
background-color: #e6f7ff !important;
|
||||
}
|
||||
|
||||
&:last-of-type .label .iconfont-sys {
|
||||
color: #1cb8fc !important;
|
||||
background-color: #e6f7ff !important;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 60%;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
|
||||
.iconfont-sys {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 12px;
|
||||
font-size: 18px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
background-color: #f2f2f2;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
span {
|
||||
&:first-of-type {
|
||||
font-size: 16px;
|
||||
color: var(--art-text-gray-800);
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #737791;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
|
||||
&.text-color-green {
|
||||
color: #2b8dfa !important;
|
||||
}
|
||||
|
||||
&.text-color-orange {
|
||||
color: #1cb8fc !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-notebook) {
|
||||
.custom-card {
|
||||
height: 350px;
|
||||
|
||||
&-body {
|
||||
padding-top: 10px;
|
||||
|
||||
> div {
|
||||
height: 140px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.custom-card {
|
||||
&-footer {
|
||||
.total-item {
|
||||
&:first-of-type .label .iconfont-sys {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
|
||||
&:last-of-type .label .iconfont-sys {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div class="custom-card art-custom-card today-sales">
|
||||
<div class="custom-card-header">
|
||||
<span class="title">{{ t('analysis.todaySales.title') }}</span>
|
||||
<span class="subtitle">{{ t('analysis.todaySales.subtitle') }}</span>
|
||||
<div class="export-btn">
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>{{ t('analysis.todaySales.export') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sales-summary">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6" :xs="24" v-for="(item, index) in salesData" :key="index">
|
||||
<div :class="['sales-card art-custom-card']">
|
||||
<i class="iconfont-sys" :class="item.class" v-html="item.iconfont"></i>
|
||||
<h2>
|
||||
<CountTo
|
||||
class="number box-title"
|
||||
:endVal="item.value"
|
||||
:duration="1000"
|
||||
separator=""
|
||||
></CountTo>
|
||||
</h2>
|
||||
<p>{{ item.label }}</p>
|
||||
<small>{{ item.change }} {{ t('analysis.todaySales.fromYesterday') }}</small>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { CountTo } from 'vue3-count-to'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const salesData = ref([
|
||||
{
|
||||
label: t('analysis.todaySales.cards.totalSales.label'),
|
||||
value: 999,
|
||||
change: t('analysis.todaySales.cards.totalSales.change'),
|
||||
iconfont: '',
|
||||
class: 'bg-primary'
|
||||
},
|
||||
{
|
||||
label: t('analysis.todaySales.cards.totalOrder.label'),
|
||||
value: 300,
|
||||
change: t('analysis.todaySales.cards.totalOrder.change'),
|
||||
iconfont: '',
|
||||
class: 'bg-warning'
|
||||
},
|
||||
{
|
||||
label: t('analysis.todaySales.cards.productSold.label'),
|
||||
value: 56,
|
||||
change: t('analysis.todaySales.cards.productSold.change'),
|
||||
iconfont: '',
|
||||
class: 'bg-error'
|
||||
},
|
||||
{
|
||||
label: t('analysis.todaySales.cards.newCustomers.label'),
|
||||
value: 68,
|
||||
change: t('analysis.todaySales.cards.newCustomers.change'),
|
||||
iconfont: '',
|
||||
class: 'bg-success'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.today-sales {
|
||||
height: 330px;
|
||||
|
||||
.export-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 66px;
|
||||
padding: 6px 0;
|
||||
color: var(--art-gray-600);
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--art-border-dashed-color);
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: var(--main-color);
|
||||
border-color: var(--main-color);
|
||||
}
|
||||
|
||||
.iconfont-sys {
|
||||
margin-right: 5px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.sales-summary {
|
||||
padding: 20px;
|
||||
|
||||
.sales-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 220px;
|
||||
padding: 0 20px;
|
||||
overflow: hidden;
|
||||
border-radius: calc(var(--custom-radius) / 2 + 4px) !important;
|
||||
|
||||
.iconfont-sys {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 20px;
|
||||
line-height: 48px;
|
||||
color: #fff;
|
||||
color: var(--el-color-primary);
|
||||
text-align: center;
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 10px;
|
||||
font-size: 26px;
|
||||
font-weight: 400;
|
||||
color: var(--art-text-gray-900) !important;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 10px;
|
||||
font-size: 16px;
|
||||
color: var(--art-text-gray-700) !important;
|
||||
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
small {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
color: var(--art-text-gray-500) !important;
|
||||
|
||||
@include ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 暗黑模式降低颜色强度
|
||||
.dark {
|
||||
.today-sales {
|
||||
.sales-summary {
|
||||
.sales-card {
|
||||
.iconfont-sys {
|
||||
&.red,
|
||||
&.yellow,
|
||||
&.green,
|
||||
&.purple {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-notebook) {
|
||||
.today-sales {
|
||||
height: 280px;
|
||||
|
||||
.sales-summary {
|
||||
.sales-card {
|
||||
height: 170px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.today-sales {
|
||||
height: auto;
|
||||
|
||||
.sales-summary {
|
||||
padding-bottom: 0;
|
||||
|
||||
.sales-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="custom-card art-custom-card top-products">
|
||||
<div class="custom-card-header">
|
||||
<span class="title">{{ t('analysis.topProducts.title') }}</span>
|
||||
</div>
|
||||
<div class="custom-card-body">
|
||||
<art-table
|
||||
:data="products"
|
||||
style="width: 100%"
|
||||
:pagination="false"
|
||||
size="large"
|
||||
:border="false"
|
||||
:stripe="false"
|
||||
:show-header-background="false"
|
||||
>
|
||||
<el-table-column prop="name" :label="t('analysis.topProducts.columns.name')" width="200" />
|
||||
<el-table-column prop="popularity" :label="t('analysis.topProducts.columns.popularity')">
|
||||
<template #default="scope">
|
||||
<el-progress
|
||||
:percentage="scope.row.popularity"
|
||||
:color="getColor(scope.row.popularity)"
|
||||
:stroke-width="5"
|
||||
:show-text="false"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sales" :label="t('analysis.topProducts.columns.sales')" width="80">
|
||||
<template #default="scope">
|
||||
<span
|
||||
:style="{
|
||||
color: getColor(scope.row.popularity),
|
||||
backgroundColor: `rgba(${hexToRgb(getColor(scope.row.popularity))}, 0.08)`,
|
||||
border: '1px solid',
|
||||
padding: '3px 6px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px'
|
||||
}"
|
||||
>{{ scope.row.sales }}</span
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</art-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { hexToRgb } from '@/utils/ui'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
|
||||
// 使用 computed 来创建响应式的产品数据
|
||||
const products = computed(() => [
|
||||
{
|
||||
name: t('analysis.topProducts.products.homeDecor.name'),
|
||||
popularity: 10,
|
||||
sales: t('analysis.topProducts.products.homeDecor.sales')
|
||||
},
|
||||
{
|
||||
name: t('analysis.topProducts.products.disneyBag.name'),
|
||||
popularity: 29,
|
||||
sales: t('analysis.topProducts.products.disneyBag.sales')
|
||||
},
|
||||
{
|
||||
name: t('analysis.topProducts.products.bathroom.name'),
|
||||
popularity: 65,
|
||||
sales: t('analysis.topProducts.products.bathroom.sales')
|
||||
},
|
||||
{
|
||||
name: t('analysis.topProducts.products.smartwatch.name'),
|
||||
popularity: 32,
|
||||
sales: t('analysis.topProducts.products.smartwatch.sales')
|
||||
},
|
||||
{
|
||||
name: t('analysis.topProducts.products.fitness.name'),
|
||||
popularity: 78,
|
||||
sales: t('analysis.topProducts.products.fitness.sales')
|
||||
},
|
||||
{
|
||||
name: t('analysis.topProducts.products.earbuds.name'),
|
||||
popularity: 41,
|
||||
sales: t('analysis.topProducts.products.earbuds.sales')
|
||||
}
|
||||
])
|
||||
|
||||
const getColor = (percentage: number) => {
|
||||
if (percentage < 25) return '#00E096'
|
||||
if (percentage < 50) return '#0095FF'
|
||||
if (percentage < 75) return '#884CFF'
|
||||
return '#FE8F0E'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-card {
|
||||
height: 330px;
|
||||
overflow-y: scroll;
|
||||
|
||||
// 隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-body {
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 1200px) {
|
||||
.custom-card {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="custom-card art-custom-card total-revenue">
|
||||
<div class="custom-card-header">
|
||||
<span class="title">{{ t('analysis.totalRevenue.title') }}</span>
|
||||
</div>
|
||||
<div class="custom-card-body">
|
||||
<div ref="chartRef" style="height: 300px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { EChartsOption } from 'echarts'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { chartRef, isDark, initChart } = useChart()
|
||||
|
||||
// 创建图表选项
|
||||
const options: () => EChartsOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 20,
|
||||
right: 3,
|
||||
bottom: 40,
|
||||
left: 3,
|
||||
containLabel: true
|
||||
},
|
||||
legend: {
|
||||
data: [
|
||||
t('analysis.totalRevenue.legend.onlineSales'),
|
||||
t('analysis.totalRevenue.legend.offlineSales')
|
||||
],
|
||||
bottom: 0,
|
||||
icon: 'circle',
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
itemGap: 15,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: isDark.value ? '#808290' : '#222B45'
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: isDark.value ? '#808290' : '#7B91B0'
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: isDark.value ? 'rgba(255, 255, 255, 0.1)' : '#EFF1F3',
|
||||
width: 0.8
|
||||
}
|
||||
},
|
||||
axisLabel: { color: isDark.value ? '#808290' : '#7B91B0' }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: t('analysis.totalRevenue.legend.onlineSales'),
|
||||
type: 'bar',
|
||||
data: [12, 13, 5, 15, 10, 15, 18],
|
||||
barWidth: '15',
|
||||
itemStyle: {
|
||||
color: '#0095FF',
|
||||
borderRadius: [4, 4, 4, 4]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t('analysis.totalRevenue.legend.offlineSales'),
|
||||
type: 'bar',
|
||||
data: [10, 11, 20, 5, 11, 13, 10],
|
||||
barWidth: '15',
|
||||
itemStyle: {
|
||||
color: '#95E0FB',
|
||||
borderRadius: [4, 4, 4, 4]
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChart(options())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-card {
|
||||
height: 400px;
|
||||
|
||||
&-body {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-notebook) {
|
||||
.custom-card {
|
||||
height: 350px;
|
||||
|
||||
&-body {
|
||||
> div {
|
||||
height: 260px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="custom-card art-custom-card visitor-insights">
|
||||
<div class="custom-card-header">
|
||||
<span class="title">{{ t('analysis.visitorInsights.title') }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div ref="chartRef" style="height: 250px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { EChartsOption } from 'echarts'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { chartRef, isDark, initChart } = useChart()
|
||||
|
||||
const { width } = useWindowSize()
|
||||
|
||||
const options: () => EChartsOption = () => {
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
grid: {
|
||||
top: 20,
|
||||
right: 20,
|
||||
bottom: width.value < 600 ? 80 : 40,
|
||||
left: 20,
|
||||
containLabel: true
|
||||
},
|
||||
legend: {
|
||||
data: [
|
||||
t('analysis.visitorInsights.legend.loyalCustomers'),
|
||||
t('analysis.visitorInsights.legend.newCustomers')
|
||||
],
|
||||
bottom: 0,
|
||||
left: 'center',
|
||||
itemWidth: 14,
|
||||
itemHeight: 14,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: isDark.value ? '#808290' : '#222B45'
|
||||
},
|
||||
icon: 'roundRect',
|
||||
itemStyle: {
|
||||
borderRadius: 4
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: isDark.value ? '#808290' : '#7B91B0' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: isDark.value ? 'rgba(255, 255, 255, 0.1)' : '#EFF1F3',
|
||||
width: 0.8
|
||||
}
|
||||
},
|
||||
axisLabel: { color: isDark.value ? '#808290' : '#7B91B0' }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: t('analysis.visitorInsights.legend.loyalCustomers'),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
data: [260, 200, 150, 130, 180, 270, 340, 380, 300, 220, 170, 130],
|
||||
lineStyle: {
|
||||
color: '#2B8DFA',
|
||||
width: 3
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#2B8DFA'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t('analysis.visitorInsights.legend.newCustomers'),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
data: [280, 350, 300, 250, 230, 210, 240, 280, 320, 350, 300, 200],
|
||||
lineStyle: {
|
||||
color: '#49BEFF',
|
||||
width: 3
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#49BEFF'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChart(options())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.visitor-insights {
|
||||
height: 330px;
|
||||
}
|
||||
|
||||
@media (max-width: $device-notebook) {
|
||||
.visitor-insights {
|
||||
height: 280px;
|
||||
|
||||
.card-body {
|
||||
> div {
|
||||
height: 210px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-phone) {
|
||||
.visitor-insights {
|
||||
height: 315px;
|
||||
|
||||
.card-body {
|
||||
> div {
|
||||
height: 240px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="custom-card art-custom-card volume-service-level">
|
||||
<div class="custom-card-header">
|
||||
<span class="title">{{ t('analysis.volumeServiceLevel.title') }}</span>
|
||||
</div>
|
||||
<div class="custom-card-body">
|
||||
<div ref="chartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { EChartsOption } from 'echarts'
|
||||
const { t } = useI18n()
|
||||
|
||||
const { chartRef, isDark, initChart } = useChart()
|
||||
|
||||
// 模拟数据
|
||||
const chartData = [
|
||||
{ volume: 800, services: 400 },
|
||||
{ volume: 1000, services: 600 },
|
||||
{ volume: 750, services: 300 },
|
||||
{ volume: 600, services: 250 },
|
||||
{ volume: 450, services: 200 },
|
||||
{ volume: 500, services: 300 }
|
||||
]
|
||||
|
||||
const options: () => EChartsOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: [
|
||||
t('analysis.volumeServiceLevel.legend.volume'),
|
||||
t('analysis.volumeServiceLevel.legend.services')
|
||||
],
|
||||
bottom: 20,
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
icon: 'circle',
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: isDark.value ? '#808290' : '#222B45'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '20',
|
||||
right: '20',
|
||||
bottom: '60',
|
||||
top: '30',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: chartData.map((_, index) => `${index + 1}`),
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: isDark.value ? 'rgba(255, 255, 255, 0.1)' : '#EFF1F3',
|
||||
width: 0.8
|
||||
}
|
||||
},
|
||||
axisTick: { show: false },
|
||||
axisLabel: { show: false }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { show: false }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: t('analysis.volumeServiceLevel.legend.volume'),
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
data: chartData.map((item) => item.volume),
|
||||
itemStyle: {
|
||||
color: '#0095FF',
|
||||
borderRadius: [0, 0, 4, 4]
|
||||
},
|
||||
barWidth: '15'
|
||||
},
|
||||
{
|
||||
name: t('analysis.volumeServiceLevel.legend.services'),
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
data: chartData.map((item) => item.services),
|
||||
itemStyle: {
|
||||
color: '#95E0FB',
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
},
|
||||
barWidth: '50%'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChart(options())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.volume-service-level {
|
||||
height: 330px;
|
||||
|
||||
.custom-card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="console">
|
||||
<CardList></CardList>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="12" :lg="10">
|
||||
<ActiveUser />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" :lg="14">
|
||||
<SalesOverview />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="24" :lg="12">
|
||||
<NewUser />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" :lg="6">
|
||||
<Dynamic />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" :lg="6">
|
||||
<TodoList />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<AboutProject />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CardList from './widget/CardList.vue'
|
||||
import ActiveUser from './widget/ActiveUser.vue'
|
||||
import SalesOverview from './widget/SalesOverview.vue'
|
||||
import NewUser from './widget/NewUser.vue'
|
||||
import Dynamic from './widget/Dynamic.vue'
|
||||
import TodoList from './widget/TodoList.vue'
|
||||
import AboutProject from './widget/AboutProject.vue'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
|
||||
defineOptions({ name: 'Console' })
|
||||
|
||||
useCommon().scrollToTop()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './style';
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
@use '@styles/variables.scss' as *;
|
||||
|
||||
.console {
|
||||
--card-spacing: 20px;
|
||||
|
||||
// 卡片头部
|
||||
:deep(.card-header) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px 25px 5px 0;
|
||||
|
||||
.title {
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--art-gray-900) !important;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 3px;
|
||||
font-size: 13px;
|
||||
color: var(--art-gray-600) !important;
|
||||
|
||||
span {
|
||||
margin-left: 10px;
|
||||
color: #52c41a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置卡片背景色、圆角、间隙
|
||||
:deep(.card-list .card),
|
||||
.card {
|
||||
margin-bottom: var(--card-spacing);
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: calc(var(--custom-radius) + 4px) !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-phone) {
|
||||
--card-spacing: 15px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="card about-project art-custom-card">
|
||||
<div>
|
||||
<h2 class="box-title">关于项目</h2>
|
||||
<p>{{ systemName }} 是一款专注于用户体验和视觉设计的后台管理系统模版</p>
|
||||
<p>使用了 Vue3、TypeScript、Vite、Element Plus 等前沿技术</p>
|
||||
|
||||
<div class="button-wrap">
|
||||
<div class="btn art-custom-card" @click="goPage(WEB_LINKS.DOCS)">
|
||||
<span>项目官网</span>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="btn art-custom-card" @click="goPage(WEB_LINKS.INTRODUCE)">
|
||||
<span>文档</span>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="btn art-custom-card" @click="goPage(WEB_LINKS.GITHUB_HOME)">
|
||||
<span>Github</span>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="btn art-custom-card" @click="goPage(WEB_LINKS.BLOG)">
|
||||
<span>博客</span>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<img class="right-img" src="@imgs/draw/draw1.png" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from '@/config'
|
||||
import { WEB_LINKS } from '@/utils/constants'
|
||||
|
||||
const systemName = AppConfig.systemInfo.name
|
||||
|
||||
const goPage = (url: string) => {
|
||||
window.open(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.about-project {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 300px;
|
||||
padding: 20px;
|
||||
|
||||
h2 {
|
||||
margin-top: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: var(--art-gray-900) !important;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-600);
|
||||
}
|
||||
}
|
||||
|
||||
.button-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 600px;
|
||||
margin-top: 35px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 240px;
|
||||
height: 50px;
|
||||
padding: 0 15px;
|
||||
margin: 0 15px 15px 0;
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-800);
|
||||
cursor: pointer;
|
||||
background: var(--art-bg-color);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 5px 10px rgb(0 0 0 / 5%);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计 - iPad Pro及以下
|
||||
@media screen and (max-width: $device-ipad-pro) {
|
||||
.about-project {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.button-wrap {
|
||||
width: 470px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.right-img {
|
||||
width: 300px;
|
||||
height: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计 - iPad垂直及以下
|
||||
@media screen and (max-width: $device-ipad-vertical) {
|
||||
.button-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 190px;
|
||||
}
|
||||
|
||||
.right-img {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计 - 手机端
|
||||
@media screen and (max-width: $device-phone) {
|
||||
.about-project {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div class="card art-custom-card">
|
||||
<div class="chart" ref="chartRef"></div>
|
||||
<div class="text">
|
||||
<h3 class="box-title">用户概述</h3>
|
||||
<p class="subtitle">比上周 <span class="text-success">+23%</span></p>
|
||||
<p class="subtitle">我们为您创建了多个选项,可将它们组合在一起并定制为像素完美的页面</p>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div v-for="(item, index) in list" :key="index">
|
||||
<p>{{ item.num }}</p>
|
||||
<p class="subtitle">{{ item.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts'
|
||||
import { getCssVar } from '@/utils/ui'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { EChartsOption } from 'echarts'
|
||||
|
||||
const {
|
||||
chartRef,
|
||||
isDark,
|
||||
initChart,
|
||||
getAxisLineStyle,
|
||||
getAxisLabelStyle,
|
||||
getAxisTickStyle,
|
||||
getSplitLineStyle
|
||||
} = useChart()
|
||||
|
||||
const list = [
|
||||
{ name: '总用户量', num: '32k' },
|
||||
{ name: '总访问量', num: '128k' },
|
||||
{ name: '日访问量', num: '1.2k' },
|
||||
{ name: '周同比', num: '+5%' }
|
||||
]
|
||||
|
||||
const options: () => EChartsOption = () => {
|
||||
return {
|
||||
grid: {
|
||||
top: 15,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
axisTick: getAxisTickStyle(),
|
||||
axisLine: getAxisLineStyle(true),
|
||||
axisLabel: getAxisLabelStyle(true)
|
||||
},
|
||||
yAxis: {
|
||||
axisLabel: getAxisLabelStyle(true),
|
||||
axisLine: getAxisLineStyle(!isDark.value),
|
||||
splitLine: getSplitLineStyle(true)
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [160, 100, 150, 80, 190, 100, 175, 120, 160],
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
borderRadius: 4,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: getCssVar('--el-color-primary-light-4')
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: getCssVar('--el-color-primary')
|
||||
}
|
||||
])
|
||||
},
|
||||
barWidth: '50%',
|
||||
animationDelay: (idx) => idx * 50 + 300,
|
||||
animationDuration: (idx) => 1500 - idx * 50,
|
||||
animationEasing: 'quarticOut' // 推荐动画: quarticOut exponentialOut quinticOut backOut
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChart(options())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
padding: 16px;
|
||||
|
||||
.chart {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
padding: 20px 0 20px 20px;
|
||||
border-radius: calc(var(--custom-radius) / 2 + 4px) !important;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 3px;
|
||||
|
||||
h3 {
|
||||
margin-top: 20px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
|
||||
&:last-of-type {
|
||||
height: 42px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-left: 3px;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
|
||||
p {
|
||||
font-weight: 400;
|
||||
|
||||
&:first-of-type {
|
||||
font-size: 24px;
|
||||
color: var(--art-gray-900);
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.card {
|
||||
.chart {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-phone) {
|
||||
.dark {
|
||||
.card {
|
||||
.chart {
|
||||
padding: 15px 0 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<el-row :gutter="20" :style="{ marginTop: showWorkTab ? '0' : '10px' }" class="card-list">
|
||||
<el-col v-for="(item, index) in dataList" :key="index" :sm="12" :md="6" :lg="6">
|
||||
<div class="card art-custom-card">
|
||||
<span class="des subtitle">{{ item.des }}</span>
|
||||
<CountTo
|
||||
class="number box-title"
|
||||
:endVal="item.num"
|
||||
:duration="1000"
|
||||
separator=""
|
||||
></CountTo>
|
||||
<div class="change-box">
|
||||
<span class="change-text">较上周</span>
|
||||
<span
|
||||
class="change"
|
||||
:class="[item.change.indexOf('+') === -1 ? 'text-danger' : 'text-success']"
|
||||
>
|
||||
{{ item.change }}
|
||||
</span>
|
||||
</div>
|
||||
<i class="iconfont-sys" v-html="item.icon"></i>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import { CountTo } from 'vue3-count-to'
|
||||
|
||||
const { showWorkTab } = storeToRefs(useSettingStore())
|
||||
|
||||
const dataList = reactive([
|
||||
{
|
||||
des: '总访问次数',
|
||||
icon: '',
|
||||
startVal: 0,
|
||||
duration: 1000,
|
||||
num: 9120,
|
||||
change: '+20%'
|
||||
},
|
||||
{
|
||||
des: '在线访客数',
|
||||
icon: '',
|
||||
startVal: 0,
|
||||
duration: 1000,
|
||||
num: 182,
|
||||
change: '+10%'
|
||||
},
|
||||
{
|
||||
des: '点击量',
|
||||
icon: '',
|
||||
startVal: 0,
|
||||
duration: 1000,
|
||||
num: 9520,
|
||||
change: '-12%'
|
||||
},
|
||||
{
|
||||
des: '新用户',
|
||||
icon: '',
|
||||
startVal: 0,
|
||||
duration: 1000,
|
||||
num: 156,
|
||||
change: '+30%'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-list {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background-color: transparent !important;
|
||||
|
||||
.art-custom-card {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
padding: 0 18px;
|
||||
list-style: none;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
$icon-size: 52px;
|
||||
|
||||
.iconfont-sys {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 20px;
|
||||
bottom: 0;
|
||||
width: $icon-size;
|
||||
height: $icon-size;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
font-size: 22px;
|
||||
line-height: $icon-size;
|
||||
color: var(--el-color-primary) !important;
|
||||
text-align: center;
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.des {
|
||||
display: block;
|
||||
height: 14px;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.number {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.change-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
|
||||
.change-text {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: var(--art-text-gray-600);
|
||||
}
|
||||
|
||||
.change {
|
||||
display: block;
|
||||
margin-left: 5px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
|
||||
&.text-success {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
&.text-danger {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.card-list {
|
||||
.art-custom-card {
|
||||
.iconfont-sys {
|
||||
background-color: #232323 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<div class="title">
|
||||
<h4 class="box-title">动态</h4>
|
||||
<p class="subtitle">新增<span class="text-success">+6</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list">
|
||||
<div v-for="(item, index) in list" :key="index">
|
||||
<span class="user">{{ item.username }}</span>
|
||||
<span class="type">{{ item.type }}</span>
|
||||
<span class="target">{{ item.target }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue-demi'
|
||||
|
||||
const list = reactive([
|
||||
{
|
||||
username: '中小鱼',
|
||||
type: '关注了',
|
||||
target: '誶誶淰'
|
||||
},
|
||||
{
|
||||
username: '何小荷',
|
||||
type: '发表文章',
|
||||
target: 'Vue3 + Typescript + Vite 项目实战笔记'
|
||||
},
|
||||
{
|
||||
username: '誶誶淰',
|
||||
type: '提出问题',
|
||||
target: '主题可以配置吗'
|
||||
},
|
||||
{
|
||||
username: '发呆草',
|
||||
type: '兑换了物品',
|
||||
target: '《奇特的一生》'
|
||||
},
|
||||
{
|
||||
username: '甜筒',
|
||||
type: '关闭了问题',
|
||||
target: '发呆草'
|
||||
},
|
||||
{
|
||||
username: '冷月呆呆',
|
||||
type: '兑换了物品',
|
||||
target: '《高效人士的七个习惯》'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 510px;
|
||||
padding: 0 25px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px 0 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
height: calc(100% - 100px);
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
> div {
|
||||
height: 70px;
|
||||
overflow: hidden;
|
||||
line-height: 70px;
|
||||
border-bottom: 1px solid var(--art-border-color);
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.user {
|
||||
font-weight: 500;
|
||||
color: var(--art-text-gray-800);
|
||||
}
|
||||
|
||||
.type {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.target {
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<div class="title">
|
||||
<h4 class="box-title">新用户</h4>
|
||||
<p class="subtitle">这个月增长<span class="text-success">+20%</span></p>
|
||||
</div>
|
||||
<el-radio-group v-model="radio2">
|
||||
<el-radio-button value="本月" label="本月"></el-radio-button>
|
||||
<el-radio-button value="上月" label="上月"></el-radio-button>
|
||||
<el-radio-button value="今年" label="今年"></el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<art-table
|
||||
class="table"
|
||||
:data="tableData"
|
||||
:pagination="false"
|
||||
size="large"
|
||||
:border="false"
|
||||
:stripe="false"
|
||||
:show-header-background="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column label="头像" prop="avatar" width="150px">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<img class="avatar" :src="scope.row.avatar" />
|
||||
<span class="user-name">{{ scope.row.username }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="地区" prop="province" />
|
||||
<el-table-column label="性别" prop="avatar">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<span style="margin-left: 10px">{{ scope.row.sex === 1 ? '男' : '女' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="进度" width="240">
|
||||
<template #default="scope">
|
||||
<el-progress :percentage="scope.row.pro" :color="scope.row.color" :stroke-width="4" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</art-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, reactive } from 'vue-demi'
|
||||
import avatar1 from '@/assets/img/avatar/avatar1.webp'
|
||||
import avatar2 from '@/assets/img/avatar/avatar2.webp'
|
||||
import avatar3 from '@/assets/img/avatar/avatar3.webp'
|
||||
import avatar4 from '@/assets/img/avatar/avatar4.webp'
|
||||
import avatar5 from '@/assets/img/avatar/avatar5.webp'
|
||||
import avatar6 from '@/assets/img/avatar/avatar6.webp'
|
||||
|
||||
const radio2 = ref('本月')
|
||||
|
||||
const tableData = reactive([
|
||||
{
|
||||
username: '中小鱼',
|
||||
province: '北京',
|
||||
sex: 0,
|
||||
age: 22,
|
||||
percentage: 60,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-primary)) !important',
|
||||
avatar: avatar1
|
||||
},
|
||||
{
|
||||
username: '何小荷',
|
||||
province: '深圳',
|
||||
sex: 1,
|
||||
age: 21,
|
||||
percentage: 20,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-secondary)) !important',
|
||||
avatar: avatar2
|
||||
},
|
||||
{
|
||||
username: '誶誶淰',
|
||||
province: '上海',
|
||||
sex: 1,
|
||||
age: 23,
|
||||
percentage: 60,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-warning)) !important',
|
||||
avatar: avatar3
|
||||
},
|
||||
{
|
||||
username: '发呆草',
|
||||
province: '长沙',
|
||||
sex: 0,
|
||||
age: 28,
|
||||
percentage: 50,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-info)) !important',
|
||||
avatar: avatar4
|
||||
},
|
||||
{
|
||||
username: '甜筒',
|
||||
province: '浙江',
|
||||
sex: 1,
|
||||
age: 26,
|
||||
percentage: 70,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-error)) !important',
|
||||
avatar: avatar5
|
||||
},
|
||||
{
|
||||
username: '冷月呆呆',
|
||||
province: '湖北',
|
||||
sex: 1,
|
||||
age: 25,
|
||||
percentage: 90,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-success)) !important',
|
||||
avatar: avatar6
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
addAnimation()
|
||||
})
|
||||
|
||||
const addAnimation = () => {
|
||||
setTimeout(() => {
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
let item = tableData[i]
|
||||
tableData[i].pro = item.percentage
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.card {
|
||||
// 进度动画
|
||||
.el-progress-bar__inner {
|
||||
transition: all 1s !important;
|
||||
}
|
||||
|
||||
.el-radio-button__original-radio:checked + .el-radio-button__inner {
|
||||
color: var(--el-color-primary) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
width: 100%;
|
||||
height: 510px;
|
||||
overflow: hidden;
|
||||
|
||||
.card-header {
|
||||
padding-left: 25px !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr:last-child td) {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<div class="title">
|
||||
<h4 class="box-title">访问量</h4>
|
||||
<p class="subtitle">今年增长<span class="text-success">+15%</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart" ref="chartRef"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts'
|
||||
import { hexToRgba, getCssVar } from '@/utils/ui'
|
||||
import { EChartsOption } from 'echarts'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
|
||||
const {
|
||||
chartRef,
|
||||
isDark,
|
||||
initChart,
|
||||
updateChart,
|
||||
getAxisLabelStyle,
|
||||
getAxisLineStyle,
|
||||
getAxisTickStyle,
|
||||
getSplitLineStyle
|
||||
} = useChart()
|
||||
|
||||
// 定义真实数据
|
||||
const realData = [50, 25, 40, 20, 70, 35, 65, 30, 35, 20, 40, 44]
|
||||
|
||||
// 初始化动画函数
|
||||
const initChartWithAnimation = () => {
|
||||
// 首先初始化图表,数据为0
|
||||
initChart(options(true))
|
||||
updateChart(options(false))
|
||||
}
|
||||
|
||||
watch(isDark, () => {
|
||||
initChart(options())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChartWithAnimation()
|
||||
})
|
||||
|
||||
const options: (isInitial?: boolean) => EChartsOption = (isInitial) => {
|
||||
const isInit = isInitial || false
|
||||
return {
|
||||
// 添加动画配置
|
||||
animation: true,
|
||||
animationDuration: 0,
|
||||
animationDurationUpdate: 0,
|
||||
grid: {
|
||||
left: '2.2%',
|
||||
right: '3%',
|
||||
bottom: '0%',
|
||||
top: '5px',
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: [
|
||||
'1月',
|
||||
'2月',
|
||||
'3月',
|
||||
'4月',
|
||||
'5月',
|
||||
'6月',
|
||||
'7月',
|
||||
'8月',
|
||||
'9月',
|
||||
'10月',
|
||||
'11月',
|
||||
'12月'
|
||||
],
|
||||
axisTick: getAxisTickStyle(),
|
||||
axisLabel: getAxisLabelStyle(true),
|
||||
axisLine: getAxisLineStyle(true)
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: realData.reduce((prev, curr) => Math.max(prev, curr), 0),
|
||||
axisLabel: getAxisLabelStyle(true),
|
||||
axisLine: getAxisLineStyle(!isDark.value),
|
||||
splitLine: getSplitLineStyle(true)
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '访客',
|
||||
color: getCssVar('--main-color'),
|
||||
type: 'line',
|
||||
stack: '总量',
|
||||
data: isInit ? new Array(12).fill(0) : realData,
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
width: 2.2
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: hexToRgba(getCssVar('--el-color-primary'), 0.15).rgba
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: hexToRgba(getCssVar('--el-color-primary'), 0.01).rgba
|
||||
}
|
||||
])
|
||||
},
|
||||
animationDuration: 0,
|
||||
animationDurationUpdate: 1500
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
padding: 20px 0 30px;
|
||||
|
||||
.card-header {
|
||||
padding: 0 18px !important;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: calc(100% - 80px);
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<div class="title">
|
||||
<h4 class="box-title">代办事项</h4>
|
||||
<p class="subtitle">待处理<span class="text-danger">3</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list">
|
||||
<div v-for="(item, index) in list" :key="index">
|
||||
<p class="title">{{ item.username }}</p>
|
||||
<p class="date subtitle">{{ item.date }}</p>
|
||||
<el-checkbox v-model="item.complate" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue-demi'
|
||||
|
||||
const list = reactive([
|
||||
{
|
||||
username: '查看今天工作内容',
|
||||
date: '上午 09:30',
|
||||
complate: true
|
||||
},
|
||||
{
|
||||
username: '回复邮件',
|
||||
date: '上午 10:30',
|
||||
complate: true
|
||||
},
|
||||
{
|
||||
username: '工作汇报整理',
|
||||
date: '上午 11:00',
|
||||
complate: true
|
||||
},
|
||||
{
|
||||
username: '产品需求会议',
|
||||
date: '下午 02:00',
|
||||
complate: false
|
||||
},
|
||||
{
|
||||
username: '整理会议内容',
|
||||
date: '下午 03:30',
|
||||
complate: false
|
||||
},
|
||||
{
|
||||
username: '明天工作计划',
|
||||
date: '下午 06:30',
|
||||
complate: false
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 510px;
|
||||
padding: 0 25px;
|
||||
|
||||
.list {
|
||||
height: calc(100% - 90px);
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
> div {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 70px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--art-border-color);
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.date {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.el-checkbox {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="ecommerce">
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="24" :lg="16">
|
||||
<Banner />
|
||||
</el-col>
|
||||
<el-col :sm="12" :md="12" :lg="4">
|
||||
<TotalOrderVolume />
|
||||
</el-col>
|
||||
<el-col :sm="12" :md="12" :lg="4">
|
||||
<TotalProducts />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="12" :md="12" :lg="8">
|
||||
<SalesTrend />
|
||||
</el-col>
|
||||
<el-col :sm="12" :md="12" :lg="8">
|
||||
<SalesClassification />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="24" :lg="8">
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="12" :lg="12">
|
||||
<ProductSales />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" :lg="12">
|
||||
<SalesGrowth />
|
||||
</el-col>
|
||||
<el-col :span="24" class="no-margin-bottom">
|
||||
<CartConversionRate />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="12" :lg="8">
|
||||
<HotCommodity />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" :lg="8">
|
||||
<AnnualSales />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="24" :lg="8">
|
||||
<TransactionList />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :md="24" :lg="8">
|
||||
<RecentTransaction />
|
||||
</el-col>
|
||||
<el-col :md="24" :lg="16" class="no-margin-bottom">
|
||||
<HotProductsList />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Banner from './widget/Banner.vue'
|
||||
import TotalOrderVolume from './widget/TotalOrderVolume.vue'
|
||||
import TotalProducts from './widget/TotalProducts.vue'
|
||||
import SalesTrend from './widget/SalesTrend.vue'
|
||||
import SalesClassification from './widget/SalesClassification.vue'
|
||||
import TransactionList from './widget/TransactionList.vue'
|
||||
import HotCommodity from './widget/HotCommodity.vue'
|
||||
import RecentTransaction from './widget/RecentTransaction.vue'
|
||||
import AnnualSales from './widget/AnnualSales.vue'
|
||||
import ProductSales from './widget/ProductSales.vue'
|
||||
import SalesGrowth from './widget/SalesGrowth.vue'
|
||||
import CartConversionRate from './widget/CartConversionRate.vue'
|
||||
import HotProductsList from './widget/HotProductsList.vue'
|
||||
|
||||
defineOptions({ name: 'Ecommerce' })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './style';
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
.ecommerce {
|
||||
:deep(.card) {
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
background-color: var(--art-main-bg-color);
|
||||
border-radius: var(--custom-radius);
|
||||
|
||||
.card-header {
|
||||
padding-bottom: 15px;
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--art-gray-900);
|
||||
|
||||
i {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.icon-text-widget) {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
margin-right: 10px;
|
||||
line-height: 42px;
|
||||
color: var(--main-color);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 8px;
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
p {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-margin-bottom {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.el-col {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="card art-custom-card yearly-card" style="height: 28.2rem">
|
||||
<div class="card-header">
|
||||
<p class="title">年度销售额</p>
|
||||
<p class="subtitle">按季度统计</p>
|
||||
</div>
|
||||
|
||||
<ArtBarChart
|
||||
:showAxisLabel="false"
|
||||
:showAxisLine="false"
|
||||
:showSplitLine="false"
|
||||
:data="[50, 80, 50, 90, 60, 70, 50]"
|
||||
barWidth="26px"
|
||||
height="16rem"
|
||||
/>
|
||||
<div class="icon-text-widget" style="margin-top: 50px">
|
||||
<div class="item">
|
||||
<div class="icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>¥200,858</p>
|
||||
<span>线上销售</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>¥102,927</p>
|
||||
<span>线下销售</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<ArtBasicBanner
|
||||
class="banner"
|
||||
height="13.3rem"
|
||||
:title="`欢迎回来 ${userInfo.userName}`"
|
||||
backgroundColor="var(--el-color-primary-light-9)"
|
||||
titleColor="var(--art-gray-900)"
|
||||
:decoration="false"
|
||||
:meteorConfig="{
|
||||
enabled: true,
|
||||
count: 10
|
||||
}"
|
||||
:buttonConfig="{
|
||||
show: false,
|
||||
text: ''
|
||||
}"
|
||||
:imageConfig="{
|
||||
src: bannerCover,
|
||||
width: '18rem',
|
||||
bottom: '-7.5rem'
|
||||
}"
|
||||
@click="handleBannerClick"
|
||||
>
|
||||
<div class="banner-slot">
|
||||
<div class="item">
|
||||
<p class="title">¥2,340<i class="iconfont-sys text-success"></i></p>
|
||||
<p class="subtitle">今日销售额</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<p class="title">35%<i class="iconfont-sys text-success"></i></p>
|
||||
<p class="subtitle">较昨日</p>
|
||||
</div>
|
||||
</div>
|
||||
</ArtBasicBanner>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import bannerCover from '@imgs/login/lf_icon2.webp'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
const userStore = useUserStore()
|
||||
|
||||
const userInfo = computed(() => userStore.getUserInfo)
|
||||
|
||||
const handleBannerClick = () => {}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.banner {
|
||||
justify-content: center;
|
||||
|
||||
.banner-slot {
|
||||
display: flex;
|
||||
margin-top: 24px;
|
||||
|
||||
.item {
|
||||
margin-right: 30px;
|
||||
|
||||
&:first-of-type {
|
||||
padding-right: 30px;
|
||||
border-right: 1px solid var(--art-gray-300);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 30px;
|
||||
color: var(--art-gray-900) !important;
|
||||
|
||||
i {
|
||||
position: relative;
|
||||
top: -10px;
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-700) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<ArtLineChartCard
|
||||
class="margin-bottom-0"
|
||||
:value="2545"
|
||||
label="购物车转化率"
|
||||
:percentage="1.2"
|
||||
:height="13.5"
|
||||
:chartData="[120, 132, 101, 134, 90, 230, 210]"
|
||||
:showAreaColor="true"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="card art-custom-card weekly-card" style="height: 28.2rem">
|
||||
<div class="card-header">
|
||||
<p class="title">热销商品</p>
|
||||
<p class="subtitle">本周销售排行</p>
|
||||
</div>
|
||||
<ArtLineChart
|
||||
:showAxisLabel="false"
|
||||
:showAxisLine="false"
|
||||
:showSplitLine="false"
|
||||
:showAreaColor="true"
|
||||
:data="[8, 40, 82, 35, 90, 52, 35]"
|
||||
height="9rem"
|
||||
/>
|
||||
<div class="content">
|
||||
<div class="item" v-for="item in weeklyList" :key="item.title">
|
||||
<div class="icon" :class="item.color">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="text">
|
||||
<p class="title">{{ item.title }}</p>
|
||||
<span class="subtitle">{{ item.subtitle }}</span>
|
||||
</div>
|
||||
<div class="value" :class="item.color">
|
||||
<span>+{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const weeklyList = [
|
||||
{
|
||||
icon: '',
|
||||
title: '智能手表Pro',
|
||||
subtitle: '电子产品',
|
||||
value: '1,286件',
|
||||
color: 'bg-primary'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
title: '时尚连衣裙',
|
||||
subtitle: '女装服饰',
|
||||
value: '892件',
|
||||
color: 'bg-success'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
title: '厨房小家电',
|
||||
subtitle: '家居用品',
|
||||
value: '756件',
|
||||
color: 'bg-error'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.weekly-card {
|
||||
.content {
|
||||
margin-top: 40px;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
|
||||
.icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
text-align: center;
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 8px;
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 10px;
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--art-gray-800);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-600);
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
padding: 6px 12px;
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 27.8rem">
|
||||
<div class="card-header">
|
||||
<p class="title">热销产品</p>
|
||||
<p class="subtitle">本月销售情况</p>
|
||||
</div>
|
||||
<div class="table">
|
||||
<el-scrollbar style="height: 21.55rem">
|
||||
<art-table
|
||||
:data="tableData"
|
||||
:pagination="false"
|
||||
style="margin-top: 0 !important"
|
||||
size="large"
|
||||
:border="false"
|
||||
:stripe="false"
|
||||
:show-header-background="false"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column label="产品" prop="product" width="220px">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<img class="product-image" :src="scope.row.image" />
|
||||
<div class="product-info">
|
||||
<div class="product-name">{{ scope.row.name }}</div>
|
||||
<div class="product-category">{{ scope.row.category }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="价格" prop="price">
|
||||
<template #default="scope">
|
||||
<span class="price">¥{{ scope.row.price.toLocaleString() }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" prop="stock">
|
||||
<template #default="scope">
|
||||
<div class="stock-badge" :class="getStockClass(scope.row.stock)">
|
||||
{{ getStockStatus(scope.row.stock) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="销量" prop="sales" />
|
||||
<el-table-column label="销售趋势" width="240">
|
||||
<template #default="scope">
|
||||
<el-progress
|
||||
:percentage="scope.row.pro"
|
||||
:color="scope.row.color"
|
||||
:stroke-width="4"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</art-table>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted } from 'vue'
|
||||
|
||||
// 导入产品图片
|
||||
import product1 from '@/assets/img/3d/icon1.webp'
|
||||
import product2 from '@/assets/img/3d/icon2.webp'
|
||||
import product3 from '@/assets/img/3d/icon3.webp'
|
||||
import product4 from '@/assets/img/3d/icon4.webp'
|
||||
import product5 from '@/assets/img/3d/icon5.webp'
|
||||
import product6 from '@/assets/img/3d/icon6.webp'
|
||||
|
||||
const tableData = reactive([
|
||||
{
|
||||
name: '智能手表 Pro',
|
||||
category: '电子设备',
|
||||
price: 1299,
|
||||
stock: 156,
|
||||
sales: 423,
|
||||
percentage: 75,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-primary)) !important',
|
||||
image: product1
|
||||
},
|
||||
{
|
||||
name: '无线蓝牙耳机',
|
||||
category: '音频设备',
|
||||
price: 499,
|
||||
stock: 89,
|
||||
sales: 652,
|
||||
percentage: 85,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-success)) !important',
|
||||
image: product2
|
||||
},
|
||||
{
|
||||
name: '机械键盘',
|
||||
category: '电脑配件',
|
||||
price: 399,
|
||||
stock: 12,
|
||||
sales: 238,
|
||||
percentage: 45,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-warning)) !important',
|
||||
image: product3
|
||||
},
|
||||
{
|
||||
name: '超薄笔记本电脑',
|
||||
category: '电子设备',
|
||||
price: 5999,
|
||||
stock: 0,
|
||||
sales: 126,
|
||||
percentage: 30,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-error)) !important',
|
||||
image: product4
|
||||
},
|
||||
{
|
||||
name: '智能音箱',
|
||||
category: '智能家居',
|
||||
price: 799,
|
||||
stock: 45,
|
||||
sales: 321,
|
||||
percentage: 60,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-info)) !important',
|
||||
image: product5
|
||||
},
|
||||
{
|
||||
name: '游戏手柄',
|
||||
category: '游戏配件',
|
||||
price: 299,
|
||||
stock: 78,
|
||||
sales: 489,
|
||||
percentage: 70,
|
||||
pro: 0,
|
||||
color: 'rgb(var(--art-secondary)) !important',
|
||||
image: product6
|
||||
}
|
||||
])
|
||||
|
||||
// 根据库存获取状态文本
|
||||
const getStockStatus = (stock: number) => {
|
||||
if (stock === 0) return '缺货'
|
||||
if (stock < 20) return '低库存'
|
||||
if (stock < 50) return '适中'
|
||||
return '充足'
|
||||
}
|
||||
|
||||
// 根据库存获取状态类名
|
||||
const getStockClass = (stock: number) => {
|
||||
if (stock === 0) return 'out-of-stock'
|
||||
if (stock < 20) return 'low-stock'
|
||||
if (stock < 50) return 'medium-stock'
|
||||
return 'in-stock'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
addAnimation()
|
||||
})
|
||||
|
||||
const addAnimation = () => {
|
||||
setTimeout(() => {
|
||||
for (let i = 0; i < tableData.length; i++) {
|
||||
let item = tableData[i]
|
||||
tableData[i].pro = item.percentage
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table {
|
||||
width: 100%;
|
||||
|
||||
.card-header {
|
||||
padding-left: 25px !important;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.product-category {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stock-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.in-stock {
|
||||
color: rgb(var(--art-success));
|
||||
background-color: rgba(var(--art-success-rgb), 0.1);
|
||||
}
|
||||
|
||||
.medium-stock {
|
||||
color: rgb(var(--art-info));
|
||||
background-color: rgba(var(--art-info-rgb), 0.1);
|
||||
}
|
||||
|
||||
.low-stock {
|
||||
color: rgb(var(--art-warning));
|
||||
background-color: rgba(var(--art-warning-rgb), 0.1);
|
||||
}
|
||||
|
||||
.out-of-stock {
|
||||
color: rgb(var(--art-error));
|
||||
background-color: rgba(var(--art-error-rgb), 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 11rem">
|
||||
<div class="card-header">
|
||||
<p class="title" style="font-size: 24px"
|
||||
>14.5k<i class="iconfont-sys text-success"></i></p
|
||||
>
|
||||
<p class="subtitle">销售量</p>
|
||||
</div>
|
||||
|
||||
<ArtBarChart
|
||||
:showAxisLabel="false"
|
||||
:showAxisLine="false"
|
||||
:showSplitLine="false"
|
||||
:data="[50, 80, 50, 90, 60, 70, 50]"
|
||||
barWidth="16px"
|
||||
height="4rem"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<ArtTimelineListCard :list="timelineData" title="最近交易" subtitle="今日订单动态" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const timelineData = [
|
||||
{
|
||||
time: '上午 09:30',
|
||||
status: 'rgb(73, 190, 255)',
|
||||
content: '收到订单 #38291 支付 ¥385.90'
|
||||
},
|
||||
{
|
||||
time: '上午 10:00',
|
||||
status: 'rgb(54, 158, 255)',
|
||||
content: '新商品上架',
|
||||
code: 'SKU-3467'
|
||||
},
|
||||
{
|
||||
time: '上午 12:00',
|
||||
status: 'rgb(103, 232, 207)',
|
||||
content: '向供应商支付了 ¥6495.00'
|
||||
},
|
||||
{
|
||||
time: '下午 14:30',
|
||||
status: 'rgb(255, 193, 7)',
|
||||
content: '促销活动开始',
|
||||
code: 'PROMO-2023'
|
||||
},
|
||||
{
|
||||
time: '下午 15:45',
|
||||
status: 'rgb(255, 105, 105)',
|
||||
content: '订单取消提醒',
|
||||
code: 'ORD-9876'
|
||||
},
|
||||
{
|
||||
time: '下午 17:00',
|
||||
status: 'rgb(103, 232, 207)',
|
||||
content: '完成日销售报表'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="card art-custom-card sales-card" style="height: 26rem">
|
||||
<div class="card-header">
|
||||
<p class="title">销售分类</p>
|
||||
<p class="subtitle">按产品类别</p>
|
||||
</div>
|
||||
<ArtRingChart
|
||||
:data="[
|
||||
{ value: 30, name: '电子产品' },
|
||||
{ value: 55, name: '服装鞋包' },
|
||||
{ value: 36, name: '家居用品' }
|
||||
]"
|
||||
:color="['#4C87F3', '#EDF2FF', '#8BD8FC']"
|
||||
:radius="['70%', '80%']"
|
||||
height="16.5rem"
|
||||
:showLabel="false"
|
||||
:borderRadius="0"
|
||||
centerText="¥300,458"
|
||||
/>
|
||||
<div class="icon-text-widget">
|
||||
<div class="item">
|
||||
<div class="icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>¥500,458</p>
|
||||
<span>总收入</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>¥130,580</p>
|
||||
<span>净利润</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 11rem">
|
||||
<div class="card-header">
|
||||
<p class="title" style="font-size: 24px"
|
||||
>12%<i class="iconfont-sys text-success"></i></p
|
||||
>
|
||||
<p class="subtitle">增长</p>
|
||||
</div>
|
||||
|
||||
<ArtLineChart
|
||||
:showAreaColor="true"
|
||||
:showAxisLabel="false"
|
||||
:showAxisLine="false"
|
||||
:showSplitLine="false"
|
||||
:data="[50, 85, 65, 95, 75, 130, 180]"
|
||||
barWidth="16px"
|
||||
height="4rem"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 26rem">
|
||||
<div class="card-header">
|
||||
<p class="title">销售趋势</p>
|
||||
<p class="subtitle">月度销售对比</p>
|
||||
</div>
|
||||
<ArtDualBarCompareChart
|
||||
:topData="[50, 80, 120, 90, 60]"
|
||||
:bottomData="[30, 60, 90, 70, 40]"
|
||||
:xAxisData="['一月', '二月', '三月', '四月', '五月']"
|
||||
height="18rem"
|
||||
:barWidth="16"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 13.3rem">
|
||||
<div class="card-header">
|
||||
<p class="title" style="font-size: 24px">205,216</p>
|
||||
<p class="subtitle">总订单量</p>
|
||||
</div>
|
||||
<ArtRingChart
|
||||
:data="[
|
||||
{ value: 30, name: '已完成' },
|
||||
{ value: 25, name: '处理中' },
|
||||
{ value: 45, name: '待发货' }
|
||||
]"
|
||||
:color="['#4C87F3', '#93F1B4', '#8BD8FC']"
|
||||
:radius="['56%', '76%']"
|
||||
height="7rem"
|
||||
:showLabel="false"
|
||||
:borderRadius="0"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="card art-custom-card" style="height: 13.3rem">
|
||||
<div class="card-header">
|
||||
<p class="title" style="font-size: 24px">55,231</p>
|
||||
<p class="subtitle">商品总数</p>
|
||||
</div>
|
||||
<ArtBarChart
|
||||
:showAxisLabel="false"
|
||||
:showAxisLine="false"
|
||||
:showSplitLine="false"
|
||||
:data="[50, 80, 40, 90, 60, 70]"
|
||||
height="7rem"
|
||||
barWidth="18px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<ArtDataListCard
|
||||
:maxCount="4"
|
||||
:list="dataList"
|
||||
title="最近活动"
|
||||
subtitle="订单处理状态"
|
||||
:showMoreButton="true"
|
||||
@more="handleMore"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const dataList = [
|
||||
{
|
||||
title: '新订单 #38291',
|
||||
status: '待处理',
|
||||
time: '5分钟',
|
||||
class: 'bg-primary',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '退款申请 #12845',
|
||||
status: '处理中',
|
||||
time: '10分钟',
|
||||
class: 'bg-secondary',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '客户投诉处理',
|
||||
status: '待处理',
|
||||
time: '15分钟',
|
||||
class: 'bg-warning',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '库存不足提醒',
|
||||
status: '紧急',
|
||||
time: '20分钟',
|
||||
class: 'bg-danger',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '订单 #29384 已发货',
|
||||
status: '已完成',
|
||||
time: '20分钟',
|
||||
class: 'bg-success',
|
||||
icon: ''
|
||||
}
|
||||
]
|
||||
|
||||
const handleMore = () => {}
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<ArtException
|
||||
:data="{
|
||||
title: '403',
|
||||
desc: $t('exceptionPage.403'),
|
||||
btnText: $t('exceptionPage.gohome'),
|
||||
imgUrl
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import imgUrl from '@imgs/state/403.png'
|
||||
defineOptions({ name: 'Exception403' })
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<ArtException
|
||||
:data="{
|
||||
title: '404',
|
||||
desc: $t('exceptionPage.404'),
|
||||
btnText: $t('exceptionPage.gohome'),
|
||||
imgUrl
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import imgUrl from '@imgs/state/404.png'
|
||||
defineOptions({ name: 'Exception404' })
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<ArtException
|
||||
:data="{
|
||||
title: '500',
|
||||
desc: $t('exceptionPage.500'),
|
||||
btnText: $t('exceptionPage.gohome'),
|
||||
imgUrl
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import imgUrl from '@imgs/state/500.png'
|
||||
defineOptions({ name: 'Exception500' })
|
||||
</script>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<ArtLayouts>
|
||||
<!-- 顶栏、水平/混合菜单 -->
|
||||
<ArtHeaderBar />
|
||||
<!-- 左侧/双列菜单 -->
|
||||
<ArtSidebarMenu />
|
||||
<!-- 页面内容 -->
|
||||
<ArtPageContent />
|
||||
<!-- 设置面板 -->
|
||||
<ArtSettingsPanel />
|
||||
<!-- 全局搜索 -->
|
||||
<ArtGlobalSearch />
|
||||
<!-- 屏幕锁定 -->
|
||||
<ArtScreenLock />
|
||||
<!-- 聊天窗口 -->
|
||||
<ArtChatWindow />
|
||||
<!-- 礼花效果 -->
|
||||
<ArtFireworksEffect />
|
||||
<!-- 水印效果 -->
|
||||
<ArtWatermark />
|
||||
</ArtLayouts>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './style';
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
@use '@/assets/styles/variables' as *;
|
||||
|
||||
.layouts {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
background: var(--art-bg-color);
|
||||
transition: padding 0.3s ease-in-out;
|
||||
|
||||
.layout-content {
|
||||
box-sizing: border-box;
|
||||
width: calc(100% - 40px);
|
||||
margin: auto;
|
||||
|
||||
// 子页面默认style
|
||||
:deep(.page-content) {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-ipad) {
|
||||
.layouts {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
padding-left: 0 !important;
|
||||
overflow-y: scroll;
|
||||
|
||||
.layout-content {
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-phone) {
|
||||
.layouts {
|
||||
.layout-content {
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="iframe-container" v-loading="isLoading">
|
||||
<iframe
|
||||
ref="iframeRef"
|
||||
:src="iframeUrl"
|
||||
frameborder="0"
|
||||
class="iframe-content"
|
||||
@load="handleIframeLoad"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIframeRoutes } from '@/router/utils/menuToRouter'
|
||||
|
||||
const route = useRoute()
|
||||
const isLoading = ref(true)
|
||||
const iframeUrl = ref('')
|
||||
const iframeRef = ref<HTMLIFrameElement | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
const iframeRoute = getIframeRoutes().find((item: any) => item.path === route.path)
|
||||
|
||||
if (iframeRoute?.meta) {
|
||||
iframeUrl.value = iframeRoute.meta.link || ''
|
||||
}
|
||||
})
|
||||
|
||||
const handleIframeLoad = () => {
|
||||
isLoading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.iframe-container {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.iframe-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 120px);
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<ArtResultPage
|
||||
type="fail"
|
||||
title="提交失败"
|
||||
message="请核对并修改以下信息后,再重新提交。"
|
||||
iconCode=""
|
||||
>
|
||||
<template #content>
|
||||
<p>您提交的内容有如下错误:</p>
|
||||
<p><i class="icon iconfont-sys"></i>您的账户已被冻结</p>
|
||||
<p><i class="icon iconfont-sys"></i>您的账户还不具备申请资格</p>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<el-button type="primary" v-ripple>返回修改</el-button>
|
||||
<el-button v-ripple>查看</el-button>
|
||||
</template>
|
||||
</ArtResultPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'ResultFail' })
|
||||
</script>
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<ArtResultPage
|
||||
type="success"
|
||||
title="提交成功"
|
||||
message="提交结果页用于反馈一系列操作任务的处理结果,如果仅是简单操作,使用 Message 全局提示反馈即可。灰色区域可以显示一些补充的信息。"
|
||||
iconCode=""
|
||||
>
|
||||
<template #content>
|
||||
<p>已提交申请,等待部门审核。</p>
|
||||
</template>
|
||||
<template #buttons>
|
||||
<el-button type="primary" v-ripple>返回修改</el-button>
|
||||
<el-button v-ripple>查看</el-button>
|
||||
<el-button v-ripple>打印</el-button>
|
||||
</template>
|
||||
</ArtResultPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'ResultSuccess' })
|
||||
</script>
|
||||
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<div class="page-content server">
|
||||
<div class="list">
|
||||
<div class="middle">
|
||||
<div class="item" v-for="item in serverList" :key="item.name">
|
||||
<div class="header">
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<span class="ip">{{ item.ip }}</span>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="left">
|
||||
<img src="@imgs/safeguard/server.png" alt="服务器" />
|
||||
<ElButtonGroup class="ml-4">
|
||||
<ElButton type="primary" size="default">开机</ElButton>
|
||||
<ElButton type="danger" size="default">关机</ElButton>
|
||||
<ElButton type="warning" size="default">重启</ElButton>
|
||||
</ElButtonGroup>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div>
|
||||
<p>CPU</p>
|
||||
<ElProgress :percentage="item.cup" :text-inside="true" :stroke-width="17" />
|
||||
</div>
|
||||
<div>
|
||||
<p>RAM</p>
|
||||
<ElProgress
|
||||
:percentage="item.memory"
|
||||
status="success"
|
||||
:text-inside="true"
|
||||
:stroke-width="17"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>SWAP</p>
|
||||
<ElProgress
|
||||
:percentage="item.swap"
|
||||
status="warning"
|
||||
:text-inside="true"
|
||||
:stroke-width="17"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>DISK</p>
|
||||
<ElProgress
|
||||
:percentage="item.disk"
|
||||
status="success"
|
||||
:text-inside="true"
|
||||
:stroke-width="17"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
defineOptions({ name: 'SafeguardServer' })
|
||||
|
||||
interface ServerInfo {
|
||||
name: string
|
||||
ip: string
|
||||
cup: number
|
||||
memory: number
|
||||
swap: number
|
||||
disk: number
|
||||
}
|
||||
|
||||
const serverList = reactive<ServerInfo[]>([
|
||||
{
|
||||
name: '开发服务器',
|
||||
ip: '192.168.1.100',
|
||||
cup: 85,
|
||||
memory: 65,
|
||||
swap: 45,
|
||||
disk: 92
|
||||
},
|
||||
{
|
||||
name: '测试服务器',
|
||||
ip: '192.168.1.101',
|
||||
cup: 32,
|
||||
memory: 78,
|
||||
swap: 90,
|
||||
disk: 45
|
||||
},
|
||||
{
|
||||
name: '预发布服务器',
|
||||
ip: '192.168.1.102',
|
||||
cup: 95,
|
||||
memory: 42,
|
||||
swap: 67,
|
||||
disk: 88
|
||||
},
|
||||
{
|
||||
name: '线上服务器',
|
||||
ip: '192.168.1.103',
|
||||
cup: 58,
|
||||
memory: 93,
|
||||
swap: 25,
|
||||
disk: 73
|
||||
}
|
||||
])
|
||||
|
||||
// 生成随机数据的函数
|
||||
function generateRandomValue(min = 0, max = 100): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
// 更新服务器数据
|
||||
function updateServerData() {
|
||||
serverList.forEach((server) => {
|
||||
server.cup = generateRandomValue()
|
||||
server.memory = generateRandomValue()
|
||||
server.swap = generateRandomValue()
|
||||
server.disk = generateRandomValue()
|
||||
})
|
||||
}
|
||||
|
||||
// 修改 timer 类型为 number | null
|
||||
let timer: number | null = null
|
||||
|
||||
onMounted(() => {
|
||||
timer = window.setInterval(updateServerData, 3000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer !== null) {
|
||||
window.clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.server {
|
||||
.list {
|
||||
width: 100%;
|
||||
|
||||
.middle {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: calc(100% + 20px);
|
||||
|
||||
.item {
|
||||
box-sizing: border-box;
|
||||
width: calc(50% - 20px);
|
||||
margin: 0 20px 20px 0;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 4px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
|
||||
.name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ip {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 40px;
|
||||
|
||||
.left {
|
||||
margin: 0 40px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 190px;
|
||||
}
|
||||
|
||||
.el-button-group {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 1;
|
||||
margin-top: 5px;
|
||||
|
||||
> div {
|
||||
margin: 15px 0;
|
||||
|
||||
p {
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-notebook) {
|
||||
.server {
|
||||
.list {
|
||||
.middle {
|
||||
.item {
|
||||
.header {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.box {
|
||||
padding: 20px;
|
||||
|
||||
.left {
|
||||
margin: 0 20px 0 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-ipad-pro) {
|
||||
.server {
|
||||
.list {
|
||||
.middle {
|
||||
.item {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $device-phone) {
|
||||
.server {
|
||||
.list {
|
||||
.middle {
|
||||
.item {
|
||||
width: 100%;
|
||||
|
||||
.header {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
|
||||
.left {
|
||||
margin: 0;
|
||||
|
||||
img {
|
||||
width: 150px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.el-button-group {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,662 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="menu-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader :showZebra="false" v-model:columns="columnChecks" @refresh="handleRefresh">
|
||||
<template #left>
|
||||
<!-- 按钮权限:后端控制模式,使用自定义指令 -->
|
||||
<ElButton v-auth="'add'" @click="showModel('menu', null, true)" v-ripple>
|
||||
添加菜单
|
||||
</ElButton>
|
||||
<ElButton @click="toggleExpand" v-ripple>
|
||||
{{ isExpanded ? '收起' : '展开' }}
|
||||
</ElButton>
|
||||
<!-- 按钮权限:前端控制模式,使用 hasAuth 方法 -->
|
||||
<!-- <ElButton v-if="hasAuth('B_CODE1')" @click="showModel('menu', null, true)" v-ripple>
|
||||
添加菜单
|
||||
</ElButton> -->
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
rowKey="path"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:data="filteredTableData"
|
||||
:marginTop="10"
|
||||
:stripe="false"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ElDialog :title="dialogTitle" v-model="dialogVisible" width="700px" align-center>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="85px">
|
||||
<ElFormItem label="菜单类型">
|
||||
<ElRadioGroup v-model="labelPosition" :disabled="disableMenuType">
|
||||
<ElRadioButton value="menu" label="menu">菜单</ElRadioButton>
|
||||
<ElRadioButton value="button" label="button">权限</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<template v-if="labelPosition === 'menu'">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="菜单名称" prop="name">
|
||||
<ElInput v-model="form.name" placeholder="菜单名称"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="路由地址" prop="path">
|
||||
<ElInput v-model="form.path" placeholder="路由地址"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限标识" prop="label">
|
||||
<ElInput v-model="form.label" placeholder="权限标识"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="图标" prop="icon">
|
||||
<ArtIconSelector v-model="form.icon" :iconType="iconType" width="100%" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="菜单排序" prop="sort" style="width: 100%">
|
||||
<ElInputNumber
|
||||
v-model="form.sort"
|
||||
style="width: 100%"
|
||||
@change="handleChange"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="外部链接" prop="link">
|
||||
<ElInput
|
||||
v-model="form.link"
|
||||
placeholder="外部链接/内嵌地址(https://www.baidu.com)"
|
||||
></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="是否启用" prop="isEnable">
|
||||
<ElSwitch v-model="form.isEnable"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="页面缓存" prop="keepAlive">
|
||||
<ElSwitch v-model="form.keepAlive"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="是否显示" prop="isHidden">
|
||||
<ElSwitch v-model="form.isHidden"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="是否内嵌" prop="isMenu">
|
||||
<ElSwitch v-model="form.isIframe"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
|
||||
<template v-if="labelPosition === 'button'">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限名称" prop="authName">
|
||||
<ElInput v-model="form.authName" placeholder="权限名称"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限标识" prop="authLabel">
|
||||
<ElInput v-model="form.authLabel" placeholder="权限标识"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限排序" prop="authSort" style="width: 100%">
|
||||
<ElInputNumber
|
||||
v-model="form.authSort"
|
||||
style="width: 100%"
|
||||
@change="handleChange"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取 消</ElButton>
|
||||
<ElButton type="primary" @click="submitForm()">确 定</ElButton>
|
||||
</span>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { IconTypeEnum } from '@/enums/appEnum'
|
||||
import { formatMenuTitle } from '@/router/utils/utils'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { ElPopover, ElButton } from 'element-plus'
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { SearchFormItem } from '@/types'
|
||||
|
||||
defineOptions({ name: 'Menus' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const { menuList } = storeToRefs(useMenuStore())
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
name: '',
|
||||
route: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
// 增加实际应用的搜索条件状态
|
||||
const appliedFilters = reactive({ ...initialSearchState })
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
Object.assign(appliedFilters, { ...initialSearchState })
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
// 将当前输入的筛选条件应用到实际搜索
|
||||
Object.assign(appliedFilters, { ...formFilters })
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '菜单名称',
|
||||
prop: 'name',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '路由地址',
|
||||
prop: 'route',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 构建菜单类型标签
|
||||
const buildMenuTypeTag = (row: AppRouteRecord) => {
|
||||
if (row.children && row.children.length > 0) {
|
||||
return 'info'
|
||||
} else if (row.meta?.link && row.meta?.isIframe) {
|
||||
return 'success'
|
||||
} else if (row.path) {
|
||||
return 'primary'
|
||||
} else if (row.meta?.link) {
|
||||
return 'warning'
|
||||
}
|
||||
}
|
||||
|
||||
// 构建菜单类型文本
|
||||
const buildMenuTypeText = (row: AppRouteRecord) => {
|
||||
if (row.children && row.children.length > 0) {
|
||||
return '目录'
|
||||
} else if (row.meta?.link && row.meta?.isIframe) {
|
||||
return '内嵌'
|
||||
} else if (row.path) {
|
||||
return '菜单'
|
||||
} else if (row.meta?.link) {
|
||||
return '外链'
|
||||
}
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'meta.title',
|
||||
label: '菜单名称',
|
||||
minWidth: 120,
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return formatMenuTitle(row.meta?.title)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'type',
|
||||
label: '菜单类型',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h(ElTag, { type: buildMenuTypeTag(row) }, () => buildMenuTypeText(row))
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'path',
|
||||
label: '路由',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return row.meta?.link || row.path || ''
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'meta.authList',
|
||||
label: '可操作权限',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h(
|
||||
'div',
|
||||
{},
|
||||
row.meta.authList?.map((item: { title: string; auth_mark: string }, index: number) => {
|
||||
return h(
|
||||
ElPopover,
|
||||
{
|
||||
placement: 'top-start',
|
||||
title: '操作',
|
||||
width: 200,
|
||||
trigger: 'click',
|
||||
key: index
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
h('div', { style: 'margin: 0; text-align: right' }, [
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
onClick: () => showModel('button', item)
|
||||
},
|
||||
{ default: () => '编辑' }
|
||||
),
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'danger',
|
||||
onClick: () => deleteAuth()
|
||||
},
|
||||
{ default: () => '删除' }
|
||||
)
|
||||
]),
|
||||
reference: () => h(ElButton, { class: 'small-btn' }, { default: () => item.title })
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'date',
|
||||
label: '编辑时间',
|
||||
formatter: () => '2022-3-12 12:00:00'
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '隐藏菜单',
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: row.meta.isHide ? 'danger' : 'info' }, () =>
|
||||
row.meta.isHide ? '是' : '否'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h('div', [
|
||||
// 这里写两组权限标识判断是为了方便演示,在实际开发中可以删除其中一组
|
||||
// 前端模式权限标识
|
||||
hasAuth('B_CODE1') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'add',
|
||||
onClick: () => showModel('menu')
|
||||
}),
|
||||
hasAuth('B_CODE2') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
hasAuth('B_CODE3') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteMenu()
|
||||
}),
|
||||
// 后端模式权限标识
|
||||
hasAuth('add') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'add',
|
||||
onClick: () => showModel('menu')
|
||||
}),
|
||||
hasAuth('edit') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
hasAuth('delete') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteMenu()
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const handleRefresh = () => {
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const form = reactive({
|
||||
// 菜单
|
||||
name: '',
|
||||
path: '',
|
||||
label: '',
|
||||
icon: '',
|
||||
isEnable: true,
|
||||
sort: 1,
|
||||
isMenu: true,
|
||||
keepAlive: true,
|
||||
isHidden: true,
|
||||
link: '',
|
||||
isIframe: false,
|
||||
// 权限 (修改这部分)
|
||||
authName: '',
|
||||
authLabel: '',
|
||||
authIcon: '',
|
||||
authSort: 1
|
||||
})
|
||||
const iconType = ref(IconTypeEnum.UNICODE)
|
||||
|
||||
const labelPosition = ref('menu')
|
||||
const rules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
path: [{ required: true, message: '请输入路由地址', trigger: 'blur' }],
|
||||
label: [{ required: true, message: '输入权限标识', trigger: 'blur' }],
|
||||
// 修改这部分
|
||||
authName: [{ required: true, message: '请输入权限名称', trigger: 'blur' }],
|
||||
authLabel: [{ required: true, message: '请输入权限权限标识', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const tableData = ref<AppRouteRecord[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
|
||||
const getTableData = () => {
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
tableData.value = menuList.value
|
||||
loading.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 过滤后的表格数据
|
||||
const filteredTableData = computed(() => {
|
||||
// 递归搜索函数
|
||||
const searchMenu = (items: AppRouteRecord[]): AppRouteRecord[] => {
|
||||
return items.filter((item) => {
|
||||
// 获取搜索关键词,转换为小写并去除首尾空格
|
||||
const searchName = appliedFilters.name?.toLowerCase().trim() || ''
|
||||
const searchRoute = appliedFilters.route?.toLowerCase().trim() || ''
|
||||
|
||||
// 获取菜单标题和路径,确保它们存在
|
||||
const menuTitle = formatMenuTitle(item.meta?.title || '').toLowerCase()
|
||||
const menuPath = (item.path || '').toLowerCase()
|
||||
|
||||
// 使用 includes 进行模糊匹配
|
||||
const nameMatch = !searchName || menuTitle.includes(searchName)
|
||||
const routeMatch = !searchRoute || menuPath.includes(searchRoute)
|
||||
|
||||
// 如果有子菜单,递归搜索
|
||||
if (item.children && item.children.length > 0) {
|
||||
const matchedChildren = searchMenu(item.children)
|
||||
// 如果子菜单有匹配项,保留当前菜单
|
||||
if (matchedChildren.length > 0) {
|
||||
item.children = matchedChildren
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return nameMatch && routeMatch
|
||||
})
|
||||
}
|
||||
|
||||
return searchMenu(tableData.value)
|
||||
})
|
||||
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const dialogTitle = computed(() => {
|
||||
const type = labelPosition.value === 'menu' ? '菜单' : '权限'
|
||||
return isEdit.value ? `编辑${type}` : `新建${type}`
|
||||
})
|
||||
|
||||
const showDialog = (type: string, row: AppRouteRecord) => {
|
||||
showModel('menu', row, true)
|
||||
}
|
||||
|
||||
const handleChange = () => {}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
// const menuStore = useMenuStore()
|
||||
// const params =
|
||||
// labelPosition.value === 'menu'
|
||||
// ? {
|
||||
// title: form.name,
|
||||
// path: form.path,
|
||||
// name: form.label,
|
||||
// icon: form.icon,
|
||||
// sort: form.sort,
|
||||
// isEnable: form.isEnable,
|
||||
// isMenu: form.isMenu,
|
||||
// keepAlive: form.keepAlive,
|
||||
// isHidden: form.isHidden,
|
||||
// link: form.link
|
||||
// }
|
||||
// : {
|
||||
// title: form.authName,
|
||||
// name: form.authLabel,
|
||||
// icon: form.authIcon,
|
||||
// sort: form.authSort
|
||||
// }
|
||||
|
||||
if (isEdit.value) {
|
||||
// await menuStore.updateMenu(params)
|
||||
} else {
|
||||
// await menuStore.addMenu(params)
|
||||
}
|
||||
|
||||
ElMessage.success(`${isEdit.value ? '编辑' : '新增'}成功`)
|
||||
dialogVisible.value = false
|
||||
} catch {
|
||||
ElMessage.error(`${isEdit.value ? '编辑' : '新增'}失败`)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showModel = (type: string, row?: any, lock: boolean = false) => {
|
||||
dialogVisible.value = true
|
||||
labelPosition.value = type
|
||||
isEdit.value = false
|
||||
lockMenuType.value = lock
|
||||
resetForm()
|
||||
|
||||
if (row) {
|
||||
isEdit.value = true
|
||||
nextTick(() => {
|
||||
// 回显数据
|
||||
if (type === 'menu') {
|
||||
// 菜单数据回显
|
||||
form.name = formatMenuTitle(row.meta.title)
|
||||
form.path = row.path
|
||||
form.label = row.name
|
||||
form.icon = row.meta.icon
|
||||
form.sort = row.meta.sort || 1
|
||||
form.isMenu = row.meta.isMenu
|
||||
form.keepAlive = row.meta.keepAlive
|
||||
form.isHidden = row.meta.isHidden || true
|
||||
form.isEnable = row.meta.isEnable || true
|
||||
form.link = row.meta.link
|
||||
form.isIframe = row.meta.isIframe || false
|
||||
} else {
|
||||
// 权限按钮数据回显
|
||||
form.authName = row.title
|
||||
form.authLabel = row.auth_mark
|
||||
form.authIcon = row.icon || ''
|
||||
form.authSort = row.sort || 1
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formRef.value?.resetFields()
|
||||
Object.assign(form, {
|
||||
// 菜单
|
||||
name: '',
|
||||
path: '',
|
||||
label: '',
|
||||
icon: '',
|
||||
sort: 1,
|
||||
isMenu: true,
|
||||
keepAlive: true,
|
||||
isHidden: true,
|
||||
link: '',
|
||||
isIframe: false,
|
||||
// 权限
|
||||
authName: '',
|
||||
authLabel: '',
|
||||
authIcon: '',
|
||||
authSort: 1
|
||||
})
|
||||
}
|
||||
|
||||
const deleteMenu = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该菜单吗?删除后无法恢复', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
ElMessage.success('删除成功')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAuth = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该权限吗?删除后无法恢复', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
ElMessage.success('删除成功')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改计算属性,增加锁定控制参数
|
||||
const disableMenuType = computed(() => {
|
||||
// 编辑权限时锁定为权限类型
|
||||
if (isEdit.value && labelPosition.value === 'button') return true
|
||||
// 编辑菜单时锁定为菜单类型
|
||||
if (isEdit.value && labelPosition.value === 'menu') return true
|
||||
// 顶部添加菜单按钮时锁定为菜单类型
|
||||
if (!isEdit.value && labelPosition.value === 'menu' && lockMenuType.value) return true
|
||||
return false
|
||||
})
|
||||
|
||||
// 添加一个控制变量
|
||||
const lockMenuType = ref(false)
|
||||
|
||||
const isExpanded = ref(false)
|
||||
const tableRef = ref()
|
||||
|
||||
const toggleExpand = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
nextTick(() => {
|
||||
if (tableRef.value) {
|
||||
tableRef.value[isExpanded.value ? 'expandAll' : 'collapseAll']()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu-page {
|
||||
.svg-icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
overflow: hidden;
|
||||
vertical-align: -8px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
|
||||
:deep(.small-btn) {
|
||||
height: 30px !important;
|
||||
padding: 0 10px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1>菜单-1</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1>菜单-2-1</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1>菜单-3-1</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<h1>菜单-3-2-1</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,344 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput placeholder="部门名称"></ElInput>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple>搜索</ElButton>
|
||||
<ElButton @click="showDialog('add')" v-ripple>新增角色</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable :data="roleList" index>
|
||||
<template #default>
|
||||
<ElTableColumn label="角色名称" prop="roleName" />
|
||||
<ElTableColumn label="角色编码" prop="roleCode" />
|
||||
<ElTableColumn label="描述" prop="des" />
|
||||
<ElTableColumn label="启用" prop="enable">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.enable ? 'primary' : 'info'">
|
||||
{{ scope.row.enable ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="date">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.date) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn fixed="right" label="操作" width="200px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button link @click="showPermissionDialog()"> 菜单权限 </el-button>
|
||||
<el-button link @click="showDialog('edit', scope.row)"> 编辑 </el-button>
|
||||
<el-button link @click="deleteRole()"> 删除 </el-button>
|
||||
<!-- <ArtButtonMore
|
||||
:list="[
|
||||
{ key: 'permission', label: '菜单权限' },
|
||||
{ key: 'edit', label: '编辑角色' },
|
||||
{ key: 'delete', label: '删除角色' }
|
||||
]"
|
||||
@click="buttonMoreClick($event, scope.row)"
|
||||
/> -->
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增角色' : '编辑角色'"
|
||||
width="30%"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="角色名称" prop="roleName">
|
||||
<ElInput v-model="form.roleName" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="角色编码" prop="roleCode">
|
||||
<ElInput v-model="form.roleCode" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="描述" prop="roleStatus">
|
||||
<ElInput v-model="form.des" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="启用">
|
||||
<ElSwitch v-model="form.enable" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<ElDialog
|
||||
v-model="permissionDialog"
|
||||
title="菜单权限"
|
||||
width="520px"
|
||||
align-center
|
||||
class="el-dialog-border"
|
||||
>
|
||||
<ElScrollbar height="70vh">
|
||||
<ElTree
|
||||
ref="treeRef"
|
||||
:data="processedMenuList"
|
||||
show-checkbox
|
||||
node-key="name"
|
||||
:default-expand-all="isExpandAll"
|
||||
:default-checked-keys="[1, 2, 3]"
|
||||
:props="defaultProps"
|
||||
@check="handleTreeCheck"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div style="display: flex; align-items: center">
|
||||
<span v-if="data.isAuth">
|
||||
{{ data.label }}
|
||||
</span>
|
||||
<span v-else>{{ defaultProps.label(data) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElTree>
|
||||
</ElScrollbar>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton>
|
||||
<ElButton @click="toggleSelectAll" style="margin-left: 8px">{{
|
||||
isSelectAll ? '取消全选' : '全部选择'
|
||||
}}</ElButton>
|
||||
<ElButton type="primary" @click="savePermission">保存</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { formatMenuTitle } from '@/router/utils/utils'
|
||||
// import { ButtonMoreItem } from '@/components/core/forms/ArtButtonMore.vue'
|
||||
import { Role, ROLE_LIST_DATA } from '@/mock/temp/formData'
|
||||
|
||||
defineOptions({ name: 'Role' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const permissionDialog = ref(false)
|
||||
const { menuList } = storeToRefs(useMenuStore())
|
||||
const treeRef = ref()
|
||||
const isExpandAll = ref(true)
|
||||
const isSelectAll = ref(false)
|
||||
|
||||
// 处理菜单数据,将 authList 转换为子节点
|
||||
const processedMenuList = computed(() => {
|
||||
const processNode = (node: any) => {
|
||||
const processed = { ...node }
|
||||
|
||||
// 如果有 authList,将其转换为子节点
|
||||
if (node.meta && node.meta.authList && node.meta.authList.length) {
|
||||
const authNodes = node.meta.authList.map((auth: any) => ({
|
||||
id: `${node.id}_${auth.auth_mark}`,
|
||||
name: `${node.name}_${auth.auth_mark}`,
|
||||
label: auth.title,
|
||||
auth_mark: auth.auth_mark,
|
||||
isAuth: true,
|
||||
checked: auth.checked || false
|
||||
}))
|
||||
|
||||
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
|
||||
}
|
||||
|
||||
// 递归处理子节点
|
||||
if (processed.children) {
|
||||
processed.children = processed.children.map(processNode)
|
||||
}
|
||||
|
||||
return processed
|
||||
}
|
||||
|
||||
return menuList.value.map(processNode)
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: '请输入角色名称', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
des: [{ required: true, message: '请输入角色描述', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const form = reactive<Role>({
|
||||
roleName: '',
|
||||
roleCode: '',
|
||||
des: '',
|
||||
date: '',
|
||||
enable: true
|
||||
})
|
||||
|
||||
const roleList = ref<Role[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
|
||||
const getTableData = () => {
|
||||
roleList.value = ROLE_LIST_DATA
|
||||
}
|
||||
|
||||
const dialogType = ref('add')
|
||||
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
form.roleName = row.roleName
|
||||
form.roleCode = row.roleCode
|
||||
form.des = row.des
|
||||
form.date = row.date
|
||||
form.enable = row.enable
|
||||
} else {
|
||||
form.roleName = ''
|
||||
form.roleCode = ''
|
||||
form.des = ''
|
||||
form.date = ''
|
||||
form.enable = true
|
||||
}
|
||||
}
|
||||
|
||||
// const buttonMoreClick = (item: ButtonMoreItem, row: any) => {
|
||||
// if (item.key === 'permission') {
|
||||
// showPermissionDialog()
|
||||
// } else if (item.key === 'edit') {
|
||||
// showDialog('edit', row)
|
||||
// } else if (item.key === 'delete') {
|
||||
// deleteRole()
|
||||
// }
|
||||
// }
|
||||
|
||||
const showPermissionDialog = () => {
|
||||
permissionDialog.value = true
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
children: 'children',
|
||||
label: (data: any) => formatMenuTitle(data.meta?.title) || ''
|
||||
}
|
||||
|
||||
const deleteRole = () => {
|
||||
ElMessageBox.confirm('确定删除该角色吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
const message = dialogType.value === 'add' ? '新增成功' : '修改成功'
|
||||
ElMessage.success(message)
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const savePermission = () => {
|
||||
ElMessage.success('权限保存成功')
|
||||
permissionDialog.value = false
|
||||
}
|
||||
|
||||
const toggleExpandAll = () => {
|
||||
const tree = treeRef.value
|
||||
if (!tree) return
|
||||
|
||||
// 使用store.nodesMap直接控制所有节点的展开状态
|
||||
const nodes = tree.store.nodesMap
|
||||
for (const node in nodes) {
|
||||
nodes[node].expanded = !isExpandAll.value
|
||||
}
|
||||
|
||||
isExpandAll.value = !isExpandAll.value
|
||||
}
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
const tree = treeRef.value
|
||||
if (!tree) return
|
||||
|
||||
if (!isSelectAll.value) {
|
||||
// 全选:获取所有节点的key并设置为选中
|
||||
const allKeys = getAllNodeKeys(processedMenuList.value)
|
||||
tree.setCheckedKeys(allKeys)
|
||||
} else {
|
||||
// 取消全选:清空所有选中
|
||||
tree.setCheckedKeys([])
|
||||
}
|
||||
|
||||
isSelectAll.value = !isSelectAll.value
|
||||
}
|
||||
|
||||
const getAllNodeKeys = (nodes: any[]): string[] => {
|
||||
const keys: string[] = []
|
||||
const traverse = (nodeList: any[]) => {
|
||||
nodeList.forEach((node) => {
|
||||
if (node.name) {
|
||||
keys.push(node.name)
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
traverse(node.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
traverse(nodes)
|
||||
return keys
|
||||
}
|
||||
|
||||
const handleTreeCheck = () => {
|
||||
const tree = treeRef.value
|
||||
if (!tree) return
|
||||
|
||||
// 使用树组件的getCheckedKeys方法获取选中的节点
|
||||
const checkedKeys = tree.getCheckedKeys()
|
||||
const allKeys = getAllNodeKeys(processedMenuList.value)
|
||||
|
||||
// 判断是否全选:选中的节点数量等于总节点数量
|
||||
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
|
||||
}
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date)
|
||||
.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
.replace(/\//g, '-')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.svg-icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
overflow: hidden;
|
||||
vertical-align: -8px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,444 @@
|
||||
<template>
|
||||
<div class="page-content user">
|
||||
<div class="content">
|
||||
<div class="left-wrap">
|
||||
<div class="user-wrap box-style">
|
||||
<img class="bg" src="@imgs/user/bg.webp" />
|
||||
<img class="avatar" src="@imgs/user/avatar.webp" />
|
||||
<h2 class="name">{{ userInfo.userName }}</h2>
|
||||
<p class="des">Art Design Pro 是一款漂亮的后台管理系统模版.</p>
|
||||
|
||||
<div class="outer-info">
|
||||
<div>
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>jdkjjfnndf@mall.com</span>
|
||||
</div>
|
||||
<div>
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>交互专家</span>
|
||||
</div>
|
||||
<div>
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>广东省深圳市</span>
|
||||
</div>
|
||||
<div>
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>字节跳动-某某平台部-UED</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lables">
|
||||
<h3>标签</h3>
|
||||
<div>
|
||||
<div v-for="item in lableList" :key="item">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <el-carousel class="gallery" height="160px"
|
||||
:interval="5000"
|
||||
indicator-position="none"
|
||||
>
|
||||
<el-carousel-item class="item" v-for="item in galleryList" :key="item">
|
||||
<img :src="item"/>
|
||||
</el-carousel-item>
|
||||
</el-carousel> -->
|
||||
</div>
|
||||
<div class="right-wrap">
|
||||
<div class="info box-style">
|
||||
<h1 class="title">基本设置</h1>
|
||||
|
||||
<ElForm
|
||||
:model="form"
|
||||
class="form"
|
||||
ref="ruleFormRef"
|
||||
:rules="rules"
|
||||
label-width="86px"
|
||||
label-position="top"
|
||||
>
|
||||
<ElRow>
|
||||
<ElFormItem label="姓名" prop="realName">
|
||||
<el-input v-model="form.realName" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="性别" prop="sex" class="right-input">
|
||||
<ElSelect v-model="form.sex" placeholder="Select" :disabled="!isEdit">
|
||||
<ElOption
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElRow>
|
||||
|
||||
<ElRow>
|
||||
<ElFormItem label="昵称" prop="nikeName">
|
||||
<ElInput v-model="form.nikeName" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="邮箱" prop="email" class="right-input">
|
||||
<ElInput v-model="form.email" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
</ElRow>
|
||||
|
||||
<ElRow>
|
||||
<ElFormItem label="手机" prop="mobile">
|
||||
<ElInput v-model="form.mobile" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="地址" prop="address" class="right-input">
|
||||
<ElInput v-model="form.address" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
</ElRow>
|
||||
|
||||
<ElFormItem label="个人介绍" prop="des" :style="{ height: '130px' }">
|
||||
<ElInput type="textarea" :rows="4" v-model="form.des" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
|
||||
<div class="el-form-item-right">
|
||||
<ElButton type="primary" style="width: 90px" v-ripple @click="edit">
|
||||
{{ isEdit ? '保存' : '编辑' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
|
||||
<div class="info box-style" style="margin-top: 20px">
|
||||
<h1 class="title">更改密码</h1>
|
||||
|
||||
<ElForm :model="pwdForm" class="form" label-width="86px" label-position="top">
|
||||
<ElFormItem label="当前密码" prop="password">
|
||||
<ElInput
|
||||
v-model="pwdForm.password"
|
||||
type="password"
|
||||
:disabled="!isEditPwd"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="新密码" prop="newPassword">
|
||||
<ElInput
|
||||
v-model="pwdForm.newPassword"
|
||||
type="password"
|
||||
:disabled="!isEditPwd"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="确认新密码" prop="confirmPassword">
|
||||
<ElInput
|
||||
v-model="pwdForm.confirmPassword"
|
||||
type="password"
|
||||
:disabled="!isEditPwd"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<div class="el-form-item-right">
|
||||
<ElButton type="primary" style="width: 90px" v-ripple @click="editPwd">
|
||||
{{ isEditPwd ? '保存' : '编辑' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { ElForm, FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'UserCenter' })
|
||||
|
||||
const userStore = useUserStore()
|
||||
const userInfo = computed(() => userStore.getUserInfo)
|
||||
|
||||
const isEdit = ref(false)
|
||||
const isEditPwd = ref(false)
|
||||
const date = ref('')
|
||||
const form = reactive({
|
||||
realName: 'John Snow',
|
||||
nikeName: '皮卡丘',
|
||||
email: '59301283@mall.com',
|
||||
mobile: '18888888888',
|
||||
address: '广东省深圳市宝安区西乡街道101栋201',
|
||||
sex: '2',
|
||||
des: 'Art Design Pro 是一款漂亮的后台管理系统模版.'
|
||||
})
|
||||
|
||||
const pwdForm = reactive({
|
||||
password: '123456',
|
||||
newPassword: '123456',
|
||||
confirmPassword: '123456'
|
||||
})
|
||||
|
||||
const ruleFormRef = ref<FormInstance>()
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
realName: [
|
||||
{ required: true, message: '请输入昵称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 30 个字符', trigger: 'blur' }
|
||||
],
|
||||
nikeName: [
|
||||
{ required: true, message: '请输入昵称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '长度在 2 到 30 个字符', trigger: 'blur' }
|
||||
],
|
||||
email: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
|
||||
mobile: [{ required: true, message: '请输入手机号码', trigger: 'blur' }],
|
||||
address: [{ required: true, message: '请输入地址', trigger: 'blur' }],
|
||||
sex: [{ type: 'array', required: true, message: '请选择性别', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: '1',
|
||||
label: '男'
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
label: '女'
|
||||
}
|
||||
]
|
||||
|
||||
const lableList: Array<string> = ['专注设计', '很有想法', '辣~', '大长腿', '川妹子', '海纳百川']
|
||||
|
||||
onMounted(() => {
|
||||
getDate()
|
||||
})
|
||||
|
||||
const getDate = () => {
|
||||
const d = new Date()
|
||||
const h = d.getHours()
|
||||
let text = ''
|
||||
|
||||
if (h >= 6 && h < 9) {
|
||||
text = '早上好'
|
||||
} else if (h >= 9 && h < 11) {
|
||||
text = '上午好'
|
||||
} else if (h >= 11 && h < 13) {
|
||||
text = '中午好'
|
||||
} else if (h >= 13 && h < 18) {
|
||||
text = '下午好'
|
||||
} else if (h >= 18 && h < 24) {
|
||||
text = '晚上好'
|
||||
} else if (h >= 0 && h < 6) {
|
||||
text = '很晚了,早点睡'
|
||||
}
|
||||
|
||||
date.value = text
|
||||
}
|
||||
|
||||
const edit = () => {
|
||||
isEdit.value = !isEdit.value
|
||||
}
|
||||
|
||||
const editPwd = () => {
|
||||
isEditPwd.value = !isEditPwd.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.user {
|
||||
.icon {
|
||||
width: 1.4em;
|
||||
height: 1.4em;
|
||||
overflow: hidden;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
$box-radius: calc(var(--custom-radius) + 4px);
|
||||
|
||||
.box-style {
|
||||
border: 1px solid var(--art-border-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
|
||||
.left-wrap {
|
||||
width: 450px;
|
||||
margin-right: 25px;
|
||||
|
||||
.user-wrap {
|
||||
position: relative;
|
||||
height: 600px;
|
||||
padding: 35px 40px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: $box-radius;
|
||||
|
||||
.bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-top: 120px;
|
||||
object-fit: cover;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-top: 20px;
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.des {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.outer-info {
|
||||
width: 300px;
|
||||
margin: auto;
|
||||
margin-top: 30px;
|
||||
text-align: left;
|
||||
|
||||
> div {
|
||||
margin-top: 10px;
|
||||
|
||||
span {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lables {
|
||||
margin-top: 40px;
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin-top: 15px;
|
||||
|
||||
> div {
|
||||
padding: 3px 6px;
|
||||
margin: 0 10px 10px 0;
|
||||
font-size: 12px;
|
||||
background: var(--art-main-bg-color);
|
||||
border: 1px solid var(--art-border-color);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gallery {
|
||||
margin-top: 25px;
|
||||
border-radius: 10px;
|
||||
|
||||
.item {
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-wrap {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border-radius: $box-radius;
|
||||
|
||||
.info {
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: $box-radius;
|
||||
|
||||
.title {
|
||||
padding: 15px 25px;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
color: var(--art-text-gray-800);
|
||||
border-bottom: 1px solid var(--art-border-color);
|
||||
}
|
||||
|
||||
.form {
|
||||
box-sizing: border-box;
|
||||
padding: 30px 25px;
|
||||
|
||||
> .el-row {
|
||||
.el-form-item {
|
||||
width: calc(50% - 10px);
|
||||
}
|
||||
|
||||
.el-input,
|
||||
.el-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.right-input {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.el-form-item-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
|
||||
.el-button {
|
||||
width: 110px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-ipad-vertical) {
|
||||
.page-content {
|
||||
.content {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
|
||||
.left-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.right-wrap {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,477 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="account-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" @refresh="handleRefresh">
|
||||
<template #left>
|
||||
<ElButton @click="showDialog('add')">新增用户</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '添加用户' : '编辑用户'"
|
||||
width="30%"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
|
||||
<ElFormItem label="用户名" prop="username">
|
||||
<ElInput v-model="formData.username" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="手机号" prop="phone">
|
||||
<ElInput v-model="formData.phone" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="性别" prop="gender">
|
||||
<ElSelect v-model="formData.gender">
|
||||
<ElOption label="男" value="男" />
|
||||
<ElOption label="女" value="女" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="角色" prop="role">
|
||||
<ElSelect v-model="formData.role" multiple>
|
||||
<ElOption
|
||||
v-for="role in roleList"
|
||||
:key="role.roleCode"
|
||||
:value="role.roleCode"
|
||||
:label="role.roleName"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { ROLE_LIST_DATA, ACCOUNT_TABLE_DATA } from '@/mock/temp/formData'
|
||||
|
||||
import { ElDialog, FormInstance, ElTag } from 'element-plus'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { UserService } from '@/api/usersApi'
|
||||
import { SearchChangeParams, SearchFormItem } from '@/types'
|
||||
const { width } = useWindowSize()
|
||||
|
||||
defineOptions({ name: 'User' }) // 定义组件名称,用于 KeepAlive 缓存控制
|
||||
|
||||
const dialogType = ref('add')
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
name: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
level: '',
|
||||
email: '',
|
||||
date: '',
|
||||
daterange: '',
|
||||
status: '1'
|
||||
}
|
||||
|
||||
const roleList = ref<any[]>([])
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
pagination.currentPage = 1 // 重置到第一页
|
||||
getUserList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1 // 搜索时重置到第一页
|
||||
getUserList()
|
||||
}
|
||||
|
||||
// 表单项变更处理
|
||||
const handleFormChange = (params: SearchChangeParams): void => {
|
||||
console.log('表单项变更:', params)
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '用户名',
|
||||
prop: 'name',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
|
||||
{
|
||||
label: '电话',
|
||||
prop: 'phone',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '用户等级',
|
||||
prop: 'level',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true
|
||||
},
|
||||
options: () => [
|
||||
{ label: '普通用户', value: 'normal' },
|
||||
{ label: 'VIP用户', value: 'vip' },
|
||||
{ label: '高级VIP', value: 'svip' },
|
||||
{ label: '企业用户', value: 'enterprise', disabled: true }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '地址',
|
||||
prop: 'address',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
{
|
||||
label: '邮箱',
|
||||
prop: 'email',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
},
|
||||
onChange: handleFormChange
|
||||
},
|
||||
// 支持 9 种日期类型定义
|
||||
// 具体可参考 src/components/core/forms/art-search-bar/widget/art-search-date/README.md
|
||||
{
|
||||
prop: 'date',
|
||||
label: '日期',
|
||||
type: 'date',
|
||||
config: {
|
||||
type: 'date',
|
||||
placeholder: '请选择日期'
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'daterange',
|
||||
label: '日期范围',
|
||||
type: 'daterange',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
type: 'radio',
|
||||
options: [
|
||||
{ label: '在线', value: '1' },
|
||||
{ label: '离线', value: '2' }
|
||||
],
|
||||
onChange: handleFormChange
|
||||
}
|
||||
]
|
||||
|
||||
// 获取标签类型
|
||||
// 1: 在线 2: 离线 3: 异常 4: 注销
|
||||
const getTagType = (status: string) => {
|
||||
switch (status) {
|
||||
case '1':
|
||||
return 'success'
|
||||
case '2':
|
||||
return 'info'
|
||||
case '3':
|
||||
return 'warning'
|
||||
case '4':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// 构建标签文本
|
||||
const buildTagText = (status: string) => {
|
||||
let text = ''
|
||||
if (status === '1') {
|
||||
text = '在线'
|
||||
} else if (status === '2') {
|
||||
text = '离线'
|
||||
} else if (status === '3') {
|
||||
text = '异常'
|
||||
} else if (status === '4') {
|
||||
text = '注销'
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
// 重置表单验证状态
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
formData.username = row.username
|
||||
formData.phone = row.userPhone
|
||||
formData.gender = row.gender === 1 ? '男' : '女'
|
||||
|
||||
// 将用户角色代码数组直接赋值给formData.role
|
||||
formData.role = Array.isArray(row.userRoles) ? row.userRoles : []
|
||||
} else {
|
||||
formData.username = ''
|
||||
formData.phone = ''
|
||||
formData.gender = '男'
|
||||
formData.role = []
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const deleteUser = () => {
|
||||
ElMessageBox.confirm('确定要注销该用户吗?', '注销用户', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
ElMessage.success('注销成功')
|
||||
})
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ type: 'selection' }, // 勾选列
|
||||
// { type: 'expand', label: '展开', width: 80 }, // 展开列
|
||||
// { type: 'index', label: '序号', width: 80 }, // 序号列
|
||||
{
|
||||
prop: 'avatar',
|
||||
label: '用户名',
|
||||
minWidth: width.value < 500 ? 220 : '',
|
||||
formatter: (row: any) => {
|
||||
return h('div', { class: 'user', style: 'display: flex; align-items: center' }, [
|
||||
h('img', { class: 'avatar', src: row.avatar }),
|
||||
h('div', {}, [
|
||||
h('p', { class: 'user-name' }, row.userName),
|
||||
h('p', { class: 'email' }, row.userEmail)
|
||||
])
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'userGender',
|
||||
label: '性别',
|
||||
sortable: true,
|
||||
formatter: (row) => (row.userGender === 1 ? '男' : '女')
|
||||
},
|
||||
{ prop: 'userPhone', label: '手机号' },
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: getTagType(row.status) }, () => buildTagText(row.status))
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'createTime',
|
||||
label: '创建日期',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 150,
|
||||
// fixed: 'right', // 固定在右侧
|
||||
// disabled: true,
|
||||
formatter: (row: any) => {
|
||||
return h('div', [
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteUser()
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
phone: '',
|
||||
gender: '',
|
||||
role: [] as string[]
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getUserList()
|
||||
getRoleList()
|
||||
})
|
||||
|
||||
// 获取用户列表数据
|
||||
const getUserList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
|
||||
const { records, current, size, total } = await UserService.getUserList({
|
||||
current: currentPage,
|
||||
size: pageSize
|
||||
})
|
||||
|
||||
// 使用本地头像替换接口返回的头像
|
||||
tableData.value = records.map((item: any, index: number) => ({
|
||||
...item,
|
||||
avatar: ACCOUNT_TABLE_DATA[index % ACCOUNT_TABLE_DATA.length].avatar
|
||||
}))
|
||||
|
||||
// 更新分页信息
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getRoleList = () => {
|
||||
roleList.value = ROLE_LIST_DATA
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
getUserList()
|
||||
}
|
||||
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
||||
],
|
||||
gender: [{ required: true, message: '请选择性别', trigger: 'change' }],
|
||||
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success(dialogType.value === 'add' ? '添加成功' : '更新成功')
|
||||
dialogVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getUserList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getUserList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.account-page {
|
||||
:deep(.user) {
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
> div {
|
||||
margin-left: 10px;
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
color: var(--art-text-gray-800);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<div class="banners">
|
||||
<h1 class="page-title">基础 & 自定义按钮+背景色</h1>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :md="12">
|
||||
<ArtBasicBanner
|
||||
title="数据中心运行状态"
|
||||
subtitle="系统访问量同比增长 23%,所有服务运行稳定,数据监控正常。"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="12">
|
||||
<ArtBasicBanner
|
||||
title="欢迎使用 Art Design Pro"
|
||||
subtitle="基于 Vue 3 + TypeScript + Element Plus 构建的现代化管理系统。"
|
||||
titleColor="#333"
|
||||
subtitleColor="#666"
|
||||
backgroundColor="#D4F1F7"
|
||||
:buttonConfig="{
|
||||
show: true,
|
||||
text: '开始探索',
|
||||
color: 'rgb(var(--art-success))',
|
||||
textColor: '#fff',
|
||||
radius: '6px'
|
||||
}"
|
||||
@buttonClick="handleBannerClick"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<h1 class="page-title">自定义图片 & 使用 slot 自定义内容</h1>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :md="12">
|
||||
<ArtBasicBanner
|
||||
title="探索星空计划"
|
||||
subtitle="加入我们的天文观测活动,发现宇宙的奥秘"
|
||||
backgroundColor="#FF8AAB"
|
||||
:buttonConfig="{
|
||||
show: true,
|
||||
text: '立即参与',
|
||||
color: '#FF5A89',
|
||||
textColor: '#fff'
|
||||
}"
|
||||
:imageConfig="{
|
||||
src: icon3
|
||||
}"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="12">
|
||||
<ArtBasicBanner
|
||||
backgroundColor="#70B1FF"
|
||||
:imageConfig="{
|
||||
src: icon5
|
||||
}"
|
||||
>
|
||||
<template #title>
|
||||
<h2 style="margin: 0; font-size: 1.6rem; color: #fff !important">智能组件系统</h2>
|
||||
</template>
|
||||
|
||||
<template #subtitle>
|
||||
<div style="margin-top: 12px">
|
||||
<p style="position: relative; z-index: 10; font-style: italic"
|
||||
>灵活配置,强大扩展,支持自定义插槽内容</p
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #button>
|
||||
<div style="margin-top: 12px">
|
||||
<el-button type="primary" color="#04A1FF"> 查看文档 </el-button>
|
||||
</div>
|
||||
</template>
|
||||
</ArtBasicBanner>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<h1 class="page-title">抽象配置方案(Preset 模式)</h1>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :md="12">
|
||||
<ArtBasicBanner v-bind="PresetBanners.marketing" />
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="12">
|
||||
<ArtBasicBanner v-bind="PresetBanners.info" />
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<h1 class="page-title">卡片横幅</h1>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :md="12" :lg="6">
|
||||
<ArtCardBanner
|
||||
title="系统运行正常"
|
||||
description="所有核心服务运行稳定,响应时间在正常范围内。"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="12" :lg="6">
|
||||
<ArtCardBanner
|
||||
:image="icon2"
|
||||
title="重要消息通知"
|
||||
description="您有 3 条待处理的重要消息,请及时查看。"
|
||||
:button="{
|
||||
show: true,
|
||||
text: '查看详情',
|
||||
color: 'rgb(var(--art-warning))',
|
||||
textColor: '#fff'
|
||||
}"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="12" :lg="6">
|
||||
<ArtCardBanner
|
||||
:image="icon3"
|
||||
title="数据分析报告"
|
||||
description="本周业务数据分析报告已完成,请查看关键指标。"
|
||||
:button="{
|
||||
show: true,
|
||||
text: '下载报告',
|
||||
color: 'rgb(var(--art-error))',
|
||||
textColor: '#fff'
|
||||
}"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="12" :lg="6">
|
||||
<ArtCardBanner
|
||||
:image="icon4"
|
||||
title="版本更新提醒"
|
||||
description="Art Design Pro v2.1.0 已发布,包含性能优化和新功能。"
|
||||
:button="{
|
||||
show: true,
|
||||
text: '立即更新',
|
||||
color: 'rgb(var(--art-primary))',
|
||||
textColor: '#fff'
|
||||
}"
|
||||
:cancelButton="{
|
||||
show: true,
|
||||
text: '稍后提醒',
|
||||
color: '#eee',
|
||||
textColor: '#333'
|
||||
}"
|
||||
@click="handleConfirm"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import icon2 from '@imgs/3d/icon2.webp'
|
||||
import icon3 from '@imgs/3d/icon3.webp'
|
||||
import icon4 from '@imgs/3d/icon4.webp'
|
||||
import icon5 from '@imgs/3d/icon7.webp'
|
||||
import { ElRow } from 'element-plus'
|
||||
|
||||
const handleBannerClick = () => {
|
||||
console.log('banner clicked')
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
console.log('confirm clicked')
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
console.log('cancel clicked')
|
||||
}
|
||||
|
||||
const PresetBanners = {
|
||||
marketing: {
|
||||
title: '限时优惠活动',
|
||||
subtitle: '精选商品 48 小时闪购,最高享受 7 折优惠,数量有限!',
|
||||
titleColor: 'var(--art-gray-900)',
|
||||
subtitleColor: 'var(--art-gray-900)',
|
||||
backgroundColor: 'rgb(var(--art-success), 0.1)',
|
||||
meteorConfig: { enabled: true, count: 15 },
|
||||
buttonConfig: {
|
||||
show: true,
|
||||
text: '立即抢购',
|
||||
color: 'rgb(var(--art-success), 0.9)',
|
||||
textColor: '#fff'
|
||||
}
|
||||
},
|
||||
info: {
|
||||
title: '服务到期提醒',
|
||||
subtitle: '您的高级服务将在 7 天后到期,请及时续费以继续享受完整功能。',
|
||||
titleColor: 'var(--art-gray-900)',
|
||||
subtitleColor: 'var(--art-gray-900)',
|
||||
backgroundColor: 'rgb(var(--art-primary), 0.1)',
|
||||
meteorConfig: { enabled: true, count: 15 },
|
||||
buttonConfig: {
|
||||
show: true,
|
||||
text: '立即续费',
|
||||
color: 'rgb(var(--art-secondary), 0.9)',
|
||||
textColor: '#fff'
|
||||
}
|
||||
}
|
||||
} as const
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.banners {
|
||||
padding-top: 20px;
|
||||
|
||||
.page-title {
|
||||
margin: 20px 0 15px;
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.el-col {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,294 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 日历主体 -->
|
||||
<ElCalendar v-model="currentDate">
|
||||
<template #date-cell="{ data }">
|
||||
<div
|
||||
class="calendar-cell"
|
||||
:class="{ 'is-selected': data.isSelected }"
|
||||
@click="handleCellClick(data.day)"
|
||||
>
|
||||
<!-- 日期显示 -->
|
||||
<p class="calendar-date">{{ formatDate(data.day) }}</p>
|
||||
|
||||
<!-- 事件列表 -->
|
||||
<div class="calendar-events">
|
||||
<div
|
||||
v-for="event in getEvents(data.day)"
|
||||
:key="`${event.date}-${event.content}`"
|
||||
class="calendar-event"
|
||||
@click.stop="handleEventClick(event)"
|
||||
>
|
||||
<div class="event-tag" :class="[`${event.type || 'bg-primary'}`]">
|
||||
{{ event.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElCalendar>
|
||||
|
||||
<!-- 事件编辑弹窗 -->
|
||||
<ElDialog v-model="dialogVisible" :title="dialogTitle" width="600px" @closed="resetForm">
|
||||
<ElForm :model="eventForm" label-width="80px">
|
||||
<ElFormItem label="活动标题" required>
|
||||
<ElInput v-model="eventForm.content" placeholder="请输入活动标题" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="事件颜色">
|
||||
<ElRadioGroup v-model="eventForm.type">
|
||||
<ElRadio v-for="type in eventTypes" :key="type.value" :value="type.value">
|
||||
{{ type.label }}
|
||||
</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="开始日期" required>
|
||||
<ElDatePicker
|
||||
style="width: 100%"
|
||||
v-model="eventForm.date"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="结束日期">
|
||||
<ElDatePicker
|
||||
style="width: 100%"
|
||||
v-model="eventForm.endDate"
|
||||
type="date"
|
||||
placeholder="选择结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
:min-date="eventForm.date"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<ElButton v-if="isEditing" type="danger" @click="handleDeleteEvent"> 删除 </ElButton>
|
||||
<ElButton type="primary" @click="handleSaveEvent">
|
||||
{{ isEditing ? '更新' : '添加' }}
|
||||
</ElButton>
|
||||
</span>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 类型定义
|
||||
interface CalendarEvent {
|
||||
date: string
|
||||
endDate?: string
|
||||
content: string
|
||||
type?: 'bg-primary' | 'bg-success' | 'bg-warning' | 'bg-danger'
|
||||
}
|
||||
|
||||
// 常量定义
|
||||
const eventTypes = [
|
||||
{ label: '基本', value: 'bg-primary' },
|
||||
{ label: '成功', value: 'bg-success' },
|
||||
{ label: '警告', value: 'bg-warning' },
|
||||
{ label: '危险', value: 'bg-danger' }
|
||||
] as const
|
||||
|
||||
// 状态管理
|
||||
const currentDate = ref(new Date('2025-02-07'))
|
||||
const events = ref<CalendarEvent[]>([
|
||||
{ date: '2025-02-01', content: '产品需求评审', type: 'bg-primary' },
|
||||
{
|
||||
date: '2025-02-03',
|
||||
endDate: '2025-02-05',
|
||||
content: '项目周报会议(跨日期)',
|
||||
type: 'bg-primary'
|
||||
},
|
||||
{ date: '2025-02-10', content: '瑜伽课程', type: 'bg-success' },
|
||||
{ date: '2025-02-15', content: '团队建设活动', type: 'bg-primary' },
|
||||
{ date: '2025-02-20', content: '健身训练', type: 'bg-success' },
|
||||
{ date: '2025-02-20', content: '代码评审', type: 'bg-danger' },
|
||||
{ date: '2025-02-20', content: '团队午餐', type: 'bg-primary' },
|
||||
{ date: '2025-02-20', content: '项目进度汇报', type: 'bg-warning' },
|
||||
{ date: '2025-02-28', content: '月度总结会', type: 'bg-warning' }
|
||||
])
|
||||
|
||||
// 弹窗状态管理
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('添加事件')
|
||||
const editingEventIndex = ref<number>(-1)
|
||||
const eventForm = ref<CalendarEvent>({
|
||||
date: '',
|
||||
endDate: '',
|
||||
content: '',
|
||||
type: 'bg-primary'
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const isEditing = computed(() => editingEventIndex.value >= 0)
|
||||
|
||||
// 工具函数
|
||||
const formatDate = (date: string) => date.split('-')[2]
|
||||
|
||||
const getEvents = (day: string) => {
|
||||
return events.value.filter((event) => {
|
||||
const eventDate = new Date(event.date)
|
||||
const currentDate = new Date(day)
|
||||
const endDate = event.endDate ? new Date(event.endDate) : new Date(event.date)
|
||||
|
||||
return currentDate >= eventDate && currentDate <= endDate
|
||||
})
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
eventForm.value = {
|
||||
date: '',
|
||||
endDate: '',
|
||||
content: '',
|
||||
type: 'bg-primary'
|
||||
}
|
||||
editingEventIndex.value = -1
|
||||
}
|
||||
|
||||
// 事件处理函数
|
||||
const handleCellClick = (day: string) => {
|
||||
dialogTitle.value = '添加事件'
|
||||
eventForm.value = {
|
||||
date: day,
|
||||
content: '',
|
||||
type: 'bg-primary'
|
||||
}
|
||||
editingEventIndex.value = -1
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEventClick = (event: CalendarEvent) => {
|
||||
dialogTitle.value = '编辑事件'
|
||||
eventForm.value = { ...event }
|
||||
editingEventIndex.value = events.value.findIndex(
|
||||
(e) => e.date === event.date && e.content === event.content
|
||||
)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSaveEvent = () => {
|
||||
if (!eventForm.value.content || !eventForm.value.date) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isEditing.value) {
|
||||
events.value[editingEventIndex.value] = { ...eventForm.value }
|
||||
} else {
|
||||
events.value.push({ ...eventForm.value })
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const handleDeleteEvent = () => {
|
||||
if (isEditing.value) {
|
||||
events.value.splice(editingEventIndex.value, 1)
|
||||
dialogVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-content {
|
||||
height: 100%;
|
||||
|
||||
:deep(.el-calendar) {
|
||||
height: 100%;
|
||||
|
||||
.el-calendar__body {
|
||||
height: calc(100% - 70px);
|
||||
}
|
||||
|
||||
.el-calendar-table {
|
||||
height: 100%;
|
||||
|
||||
.is-selected {
|
||||
// 选中日期的背景颜色
|
||||
background-color: var(--el-color-warning-light-9) !important;
|
||||
}
|
||||
|
||||
.el-calendar-day {
|
||||
height: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-cell {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 120px;
|
||||
max-height: 120px;
|
||||
padding: 4px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
.calendar-date {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.calendar-events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
max-height: 85px;
|
||||
padding-right: 4px;
|
||||
margin-top: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.event-tag {
|
||||
min-width: 100px;
|
||||
padding: 6px 12px;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&.event-continues::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 4px;
|
||||
font-size: 12px;
|
||||
content: '►';
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dialog__body) {
|
||||
padding-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,452 @@
|
||||
<template>
|
||||
<div class="cards">
|
||||
<h1 class="page-title">统计卡片(文字)</h1>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :md="6" v-for="card in statsCards" :key="card.id">
|
||||
<ArtStatsCard
|
||||
:icon="card.icon"
|
||||
:title="card.title"
|
||||
:description="card.description"
|
||||
:iconSize="card.iconSize"
|
||||
:iconBgRadius="8"
|
||||
iconColor="#fff"
|
||||
:iconBgColor="card.iconBgColor"
|
||||
:showArrow="card.showArrow"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<h1 class="page-title">统计卡片(数字滚动)</h1>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :md="6" v-for="card in statsCards" :key="card.id">
|
||||
<ArtStatsCard
|
||||
:icon="card.icon"
|
||||
:count="card.count"
|
||||
:description="card.description"
|
||||
:iconSize="card.iconSize"
|
||||
iconColor="#fff"
|
||||
:iconBgColor="card.iconBgColor"
|
||||
:showArrow="card.showArrow"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<h1 class="page-title">统计卡片(自定义样式)</h1>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :md="6" v-for="card in statsCards" :key="card.id">
|
||||
<ArtStatsCard
|
||||
:icon="card.icon"
|
||||
:title="card.title"
|
||||
:description="card.description"
|
||||
:iconColor="card.iconColor"
|
||||
:textColor="card.textColor"
|
||||
:backgroundColor="card.backgroundColor"
|
||||
:showArrow="card.showArrow"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<h1 class="page-title">进度卡片</h1>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :md="6" v-for="card in progressCards" :key="card.id">
|
||||
<ArtProgressCard :percentage="card.percentage" :title="card.title" :color="card.color" />
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<h1 class="page-title">进度卡片(icon)</h1>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :md="6" v-for="card in progressCards" :key="card.id">
|
||||
<ArtProgressCard
|
||||
:percentage="card.percentage"
|
||||
:title="card.title"
|
||||
:color="card.color"
|
||||
:icon="card.icon"
|
||||
:iconColor="card.iconColor"
|
||||
:iconBgColor="card.iconBgColor"
|
||||
:iconSize="card.iconSize"
|
||||
:iconBgRadius="8"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<h1 class="page-title">图表卡片(小图表)</h1>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :md="6">
|
||||
<ArtLineChartCard
|
||||
:isMiniChart="true"
|
||||
:value="2545"
|
||||
label="新用户"
|
||||
date="过去7天"
|
||||
:percentage="1.2"
|
||||
:height="9.5"
|
||||
:chartData="[120, 132, 101, 134, 90, 230, 210]"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="6">
|
||||
<ArtBarChartCard
|
||||
:isMiniChart="true"
|
||||
:value="15480"
|
||||
label="浏览量"
|
||||
date="过去 14 天"
|
||||
:percentage="-4.15"
|
||||
:height="9.5"
|
||||
:barWidth="'45%'"
|
||||
:chartData="[120, 100, 150, 140, 90, 120, 130]"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="6">
|
||||
<ArtLineChartCard
|
||||
:isMiniChart="true"
|
||||
:value="2545"
|
||||
label="粉丝数"
|
||||
date="过去 30 天"
|
||||
:percentage="1.2"
|
||||
:height="9.5"
|
||||
:showAreaColor="true"
|
||||
:chartData="[150, 180, 160, 200, 180, 220, 240]"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="6">
|
||||
<ArtDonutChartCard
|
||||
:value="36358"
|
||||
title="粉丝量"
|
||||
:percentage="18"
|
||||
percentageLabel="较去年"
|
||||
:data="[50, 40]"
|
||||
:height="9.5"
|
||||
currentValue="2022"
|
||||
previousValue="2021"
|
||||
:radius="['50%', '70%']"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<h1 class="page-title">图表卡片(大图表)</h1>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :md="6">
|
||||
<ArtLineChartCard
|
||||
:value="2545"
|
||||
label="新用户"
|
||||
:percentage="1.2"
|
||||
:height="11"
|
||||
:chartData="[120, 132, 101, 134, 90, 230, 210]"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="6">
|
||||
<ArtBarChartCard
|
||||
:value="15480"
|
||||
label="浏览量"
|
||||
:percentage="-4.15"
|
||||
:height="11"
|
||||
:chartData="[120, 100, 150, 140, 90, 120, 130, 110]"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="6">
|
||||
<ArtLineChartCard
|
||||
:value="2545"
|
||||
label="粉丝数"
|
||||
:percentage="1.2"
|
||||
:height="11"
|
||||
:showAreaColor="true"
|
||||
:chartData="[150, 180, 160, 200, 180, 220, 240]"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="6">
|
||||
<ArtDonutChartCard
|
||||
:value="36358"
|
||||
title="粉丝量"
|
||||
:percentage="-18"
|
||||
percentageLabel="较2021年"
|
||||
:data="[70, 30]"
|
||||
:height="11"
|
||||
currentValue="12月"
|
||||
previousValue="11月"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<h1 class="page-title">数据列表卡片</h1>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :lg="8">
|
||||
<ArtDataListCard :list="dataList" title="待办事项" subtitle="今日待处理任务" />
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="8">
|
||||
<ArtDataListCard
|
||||
:maxCount="4"
|
||||
:list="dataList"
|
||||
title="最近活动"
|
||||
subtitle="近期活动列表"
|
||||
:showMoreButton="true"
|
||||
@more="handleMore"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :lg="8">
|
||||
<ArtTimelineListCard :list="timelineData" title="最近交易" subtitle="2024年12月20日" />
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<h1 class="page-title">图片卡片</h1>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :sm="12" :md="6" v-for="card in imageCards" :key="card.id">
|
||||
<ArtImageCard
|
||||
:imageUrl="card.imageUrl"
|
||||
:title="card.title"
|
||||
:category="card.category"
|
||||
:readTime="card.readTime"
|
||||
:views="card.views"
|
||||
:comments="card.comments"
|
||||
:date="card.date"
|
||||
@click="handleImageCardClick(card)"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import cover1 from '@imgs/cover/img1.webp'
|
||||
import cover2 from '@imgs/cover/img2.webp'
|
||||
import cover3 from '@imgs/cover/img3.webp'
|
||||
import cover4 from '@imgs/cover/img4.webp'
|
||||
|
||||
const statsCards = [
|
||||
{
|
||||
id: 1,
|
||||
title: '销售产品',
|
||||
count: 1235,
|
||||
description: '鞋子、牛仔裤、派对服装、手表',
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-primary))',
|
||||
iconSize: 20,
|
||||
iconBgColor: 'rgb(var(--art-info))',
|
||||
textColor: 'rgb(var(--art-primary))',
|
||||
backgroundColor: 'rgb(var(--art-bg-primary))',
|
||||
showArrow: false
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '活跃用户',
|
||||
count: 5000,
|
||||
description: '日活跃用户超过5,000+',
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-warning))',
|
||||
iconSize: 20,
|
||||
iconBgColor: 'rgb(var(--art-success))',
|
||||
textColor: 'rgb(var(--art-warning))',
|
||||
backgroundColor: 'rgb(var(--art-bg-warning))',
|
||||
showArrow: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '总收入',
|
||||
count: 35000,
|
||||
description: '月收入超过¥350,000+',
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-secondary))',
|
||||
iconSize: 20,
|
||||
iconBgColor: 'rgb(var(--art-secondary))',
|
||||
textColor: 'rgb(var(--art-secondary))',
|
||||
backgroundColor: 'rgb(var(--art-bg-secondary))',
|
||||
showArrow: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '客户评价',
|
||||
count: 4800,
|
||||
description: '平均评分4.8/5',
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-error))',
|
||||
iconSize: 20,
|
||||
iconBgColor: 'rgb(var(--art-error))',
|
||||
textColor: 'rgb(var(--art-error))',
|
||||
backgroundColor: 'rgb(var(--art-bg-error))',
|
||||
showArrow: false
|
||||
}
|
||||
]
|
||||
|
||||
const progressCards = [
|
||||
{
|
||||
id: 1,
|
||||
title: '完成进度',
|
||||
percentage: 75,
|
||||
color: 'rgb(var(--art-success))',
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-success))',
|
||||
iconBgColor: 'rgb(var(--art-bg-success))',
|
||||
iconSize: 20
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '项目进度',
|
||||
percentage: 65,
|
||||
color: 'rgb(var(--art-primary))',
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-primary))',
|
||||
iconBgColor: 'rgb(var(--art-bg-primary))',
|
||||
iconSize: 20
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '学习进度',
|
||||
percentage: 45,
|
||||
color: 'rgb(var(--art-error))',
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-error))',
|
||||
iconBgColor: 'rgb(var(--art-bg-error))',
|
||||
iconSize: 20
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '任务进度',
|
||||
percentage: 90,
|
||||
color: 'rgb(var(--art-secondary))',
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-secondary))',
|
||||
iconBgColor: 'rgb(var(--art-bg-secondary))',
|
||||
iconSize: 20
|
||||
}
|
||||
]
|
||||
|
||||
const imageCards = [
|
||||
{
|
||||
id: 1,
|
||||
imageUrl: cover1,
|
||||
title: 'AI技术在医疗领域的创新应用与发展前景',
|
||||
category: '社交',
|
||||
readTime: '2分钟',
|
||||
views: 9125,
|
||||
comments: 3,
|
||||
date: '12月19日 周一'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
imageUrl: cover2,
|
||||
title: '大数据分析助力企业决策的实践案例',
|
||||
category: '技术',
|
||||
readTime: '3分钟',
|
||||
views: 7234,
|
||||
comments: 5,
|
||||
date: '12月20日 周二'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
imageUrl: cover3,
|
||||
title: '区块链技术在供应链管理中的应用',
|
||||
category: '科技',
|
||||
readTime: '4分钟',
|
||||
views: 5678,
|
||||
comments: 8,
|
||||
date: '12月21日 周三'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
imageUrl: cover4,
|
||||
title: '云计算技术发展趋势与未来展望',
|
||||
category: '云技术',
|
||||
readTime: '5分钟',
|
||||
views: 4321,
|
||||
comments: 6,
|
||||
date: '12月22日 周四'
|
||||
}
|
||||
]
|
||||
|
||||
const dataList = [
|
||||
{
|
||||
title: '新加坡之行',
|
||||
status: '进行中',
|
||||
time: '5分钟',
|
||||
class: 'bg-primary',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '归档数据',
|
||||
status: '进行中',
|
||||
time: '10分钟',
|
||||
class: 'bg-secondary',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '客户会议',
|
||||
status: '待处理',
|
||||
time: '15分钟',
|
||||
class: 'bg-warning',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '筛选任务团队',
|
||||
status: '进行中',
|
||||
time: '20分钟',
|
||||
class: 'bg-danger',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
title: '发送信封给小王',
|
||||
status: '已完成',
|
||||
time: '20分钟',
|
||||
class: 'bg-success',
|
||||
icon: ''
|
||||
}
|
||||
]
|
||||
|
||||
const timelineData = [
|
||||
{
|
||||
time: '上午 09:30',
|
||||
status: 'rgb(73, 190, 255)',
|
||||
content: '收到 John Doe 支付的 385.90 美元'
|
||||
},
|
||||
{
|
||||
time: '上午 10:00',
|
||||
status: 'rgb(54, 158, 255)',
|
||||
content: '新销售记录',
|
||||
code: 'ML-3467'
|
||||
},
|
||||
{
|
||||
time: '上午 12:00',
|
||||
status: 'rgb(103, 232, 207)',
|
||||
content: '向 Michael 支付了 64.95 美元'
|
||||
},
|
||||
{
|
||||
time: '下午 14:30',
|
||||
status: 'rgb(255, 193, 7)',
|
||||
content: '系统维护通知',
|
||||
code: 'MT-2023'
|
||||
},
|
||||
{
|
||||
time: '下午 15:45',
|
||||
status: 'rgb(255, 105, 105)',
|
||||
content: '紧急订单取消提醒',
|
||||
code: 'OR-9876'
|
||||
},
|
||||
{
|
||||
time: '下午 17:00',
|
||||
status: 'rgb(103, 232, 207)',
|
||||
content: '完成每日销售报表'
|
||||
}
|
||||
]
|
||||
|
||||
const handleMore = () => {}
|
||||
|
||||
const handleImageCardClick = (card: any) => {
|
||||
console.log(card)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cards {
|
||||
padding: 20px 0;
|
||||
|
||||
.page-title {
|
||||
margin: 20px 0 15px;
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.el-col {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="charts">
|
||||
<h1 class="page-title">图表</h1>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :xs="24" :md="12" :lg="8">
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<span>柱状图</span>
|
||||
</div>
|
||||
<ArtBarChart
|
||||
:data="[50, 80, 120, 90, 60, 70, 100]"
|
||||
:xAxisData="['一月', '二月', '三月', '四月', '五月', '六月', '七月']"
|
||||
/>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :md="12" :lg="8">
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<span>折线图</span>
|
||||
</div>
|
||||
<ArtLineChart
|
||||
:data="[58, 15, 82, 35, 90, 82, 85]"
|
||||
:xAxisData="['一月', '二月', '三月', '四月', '五月', '六月', '七月']"
|
||||
/>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :md="12" :lg="8">
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<span>折线图(渐变背景)</span>
|
||||
</div>
|
||||
<ArtLineChart
|
||||
:data="[58, 15, 82, 35, 90, 82, 85]"
|
||||
:xAxisData="['一月', '二月', '三月', '四月', '五月', '六月', '七月']"
|
||||
:showAreaColor="true"
|
||||
/>
|
||||
</div>
|
||||
</ElCol>
|
||||
|
||||
<ElCol :xs="24" :md="12" :lg="8">
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<span>散点图</span>
|
||||
</div>
|
||||
<ArtScatterChart
|
||||
:data="[
|
||||
{ value: [1, 3] },
|
||||
{ value: [2, 4] },
|
||||
{ value: [3, 5] },
|
||||
{ value: [4, 6] },
|
||||
{ value: [5, 7] },
|
||||
{ value: [6, 8] },
|
||||
{ value: [7, 7] },
|
||||
{ value: [8, 9] },
|
||||
{ value: [9, 8] },
|
||||
{ value: [10, 6] },
|
||||
{ value: [11, 7] },
|
||||
{ value: [12, 8] }
|
||||
]"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :md="12" :lg="8">
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<span>环形图</span>
|
||||
</div>
|
||||
<ArtRingChart
|
||||
:data="[
|
||||
{ value: 30, name: '分类A' },
|
||||
{ value: 25, name: '分类B' },
|
||||
{ value: 45, name: '分类C' }
|
||||
]"
|
||||
:color="['#4C87F3', '#93F1B4', '#8BD8FC']"
|
||||
/>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :md="12" :lg="8">
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<span>饼图</span>
|
||||
</div>
|
||||
<ArtRingChart
|
||||
:data="[
|
||||
{ value: 30, name: '分类A' },
|
||||
{ value: 25, name: '分类B' },
|
||||
{ value: 45, name: '分类C' }
|
||||
]"
|
||||
:color="['#4C87F3', '#93F1B4', '#8BD8FC']"
|
||||
:radius="['0%', '80%']"
|
||||
/>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :md="12" :lg="8">
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<span>k线图</span>
|
||||
</div>
|
||||
<ArtKLineChart
|
||||
:data="[
|
||||
{ time: '2024-01-01', open: 20, close: 23, high: 25, low: 18 },
|
||||
{ time: '2024-01-02', open: 23, close: 21, high: 24, low: 20 },
|
||||
{ time: '2024-01-03', open: 21, close: 25, high: 26, low: 21 }
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :md="12" :lg="8">
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<span>雷达图</span>
|
||||
</div>
|
||||
<ArtRadarChart
|
||||
:indicator="[
|
||||
{ name: '销售', max: 100 },
|
||||
{ name: '管理', max: 100 },
|
||||
{ name: '技术', max: 100 },
|
||||
{ name: '客服', max: 100 },
|
||||
{ name: '开发', max: 100 }
|
||||
]"
|
||||
:data="[
|
||||
{
|
||||
name: '预算分配',
|
||||
value: [80, 70, 90, 85, 75]
|
||||
},
|
||||
{
|
||||
name: '实际开销',
|
||||
value: [70, 75, 85, 80, 70]
|
||||
}
|
||||
]"
|
||||
:colors="['#8BD8FC', '#409EFF']"
|
||||
/>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :md="12" :lg="8">
|
||||
<div class="card art-custom-card">
|
||||
<div class="card-header">
|
||||
<span>柱状图(水平)</span>
|
||||
</div>
|
||||
<ArtHBarChart
|
||||
:data="[50, 80, 120, 90, 60]"
|
||||
:xAxisData="['产品A', '产品B', '产品C', '产品D', '产品E']"
|
||||
/>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.charts {
|
||||
padding-top: 20px;
|
||||
|
||||
.page-title {
|
||||
margin: 20px 0 15px;
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 20px;
|
||||
background-color: var(--art-main-bg-color);
|
||||
border-radius: var(--custom-radius);
|
||||
|
||||
.card-header {
|
||||
padding-bottom: 15px;
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-col {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,771 @@
|
||||
<template>
|
||||
<div class="chat" :style="{ height: containerMinHeight }">
|
||||
<ElRow>
|
||||
<ElCol :span="12">
|
||||
<div class="grid-content ep-bg-purple" />
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<div class="grid-content ep-bg-purple-light" />
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<div class="person-list">
|
||||
<div class="person-item-header">
|
||||
<div class="user-info">
|
||||
<ElAvatar :size="50" :src="selectedPerson?.avatar" />
|
||||
<div class="user-details">
|
||||
<div class="name">{{ selectedPerson?.name }}</div>
|
||||
<div class="email">{{ selectedPerson?.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<ElInput v-model="searchQuery" placeholder="搜索联系人" prefix-icon="Search" clearable />
|
||||
</div>
|
||||
<ElDropdown trigger="click" placement="bottom-start">
|
||||
<span class="sort-btn">
|
||||
排序方式
|
||||
<ElIcon class="el-icon--right">
|
||||
<arrow-down />
|
||||
</ElIcon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem>按时间排序</ElDropdownItem>
|
||||
<ElDropdownItem>按名称排序</ElDropdownItem>
|
||||
<ElDropdownItem>全部标为已读</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
<ElScrollbar>
|
||||
<div
|
||||
v-for="item in personList"
|
||||
:key="item.id"
|
||||
class="person-item"
|
||||
:class="{ active: selectedPerson?.id === item.id }"
|
||||
@click="selectPerson(item)"
|
||||
>
|
||||
<div class="avatar-wrapper">
|
||||
<ElAvatar :size="40" :src="item.avatar">
|
||||
{{ item.name.charAt(0) }}
|
||||
</ElAvatar>
|
||||
<div class="status-dot" :class="{ online: item.online }"></div>
|
||||
</div>
|
||||
<div class="person-info">
|
||||
<div class="info-top">
|
||||
<span class="person-name">{{ item.name }}</span>
|
||||
<span class="last-time">{{ item.lastTime }}</span>
|
||||
</div>
|
||||
<div class="info-bottom">
|
||||
<span class="email">{{ item.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
<div class="chat-modal">
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<span class="name">Art Bot</span>
|
||||
<div class="status">
|
||||
<div class="dot" :class="{ online: isOnline, offline: !isOnline }"></div>
|
||||
<span class="status-text">{{ isOnline ? '在线' : '离线' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="btn">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="btn">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="btn">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-container">
|
||||
<!-- 聊天消息区域 -->
|
||||
<div class="chat-messages" ref="messageContainer">
|
||||
<template v-for="(message, index) in messages" :key="index">
|
||||
<div :class="['message-item', message.isMe ? 'message-right' : 'message-left']">
|
||||
<ElAvatar :size="32" :src="message.avatar" class="message-avatar" />
|
||||
<div class="message-content">
|
||||
<div class="message-info">
|
||||
<span class="sender-name">{{ message.sender }}</span>
|
||||
<span class="message-time">{{ message.time }}</span>
|
||||
</div>
|
||||
<div class="message-text">{{ message.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 聊天输入区域 -->
|
||||
<div class="chat-input">
|
||||
<ElInput
|
||||
v-model="messageText"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="输入消息"
|
||||
resize="none"
|
||||
@keyup.enter.prevent="sendMessage"
|
||||
>
|
||||
<template #append>
|
||||
<div class="input-actions">
|
||||
<ElButton :icon="Paperclip" circle plain />
|
||||
<ElButton :icon="Picture" circle plain />
|
||||
<ElButton type="primary" @click="sendMessage" v-ripple>发送</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElInput>
|
||||
<div class="chat-input-actions">
|
||||
<div class="left">
|
||||
<i class="iconfont-sys"></i>
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<ElButton type="primary" @click="sendMessage" v-ripple>发送</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Picture, Paperclip } from '@element-plus/icons-vue'
|
||||
import { mittBus } from '@/utils/sys'
|
||||
import meAvatar from '@/assets/img/avatar/avatar5.webp'
|
||||
import aiAvatar from '@/assets/img/avatar/avatar10.webp'
|
||||
import avatar2 from '@/assets/img/avatar/avatar2.webp'
|
||||
import avatar3 from '@/assets/img/avatar/avatar3.webp'
|
||||
import avatar4 from '@/assets/img/avatar/avatar4.webp'
|
||||
import avatar5 from '@/assets/img/avatar/avatar5.webp'
|
||||
import avatar6 from '@/assets/img/avatar/avatar6.webp'
|
||||
import avatar7 from '@/assets/img/avatar/avatar7.webp'
|
||||
import avatar8 from '@/assets/img/avatar/avatar8.webp'
|
||||
import avatar9 from '@/assets/img/avatar/avatar9.webp'
|
||||
import avatar10 from '@/assets/img/avatar/avatar10.webp'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
|
||||
const { containerMinHeight } = useCommon()
|
||||
|
||||
const searchQuery = ref('')
|
||||
|
||||
// 抽屉显示状态
|
||||
const isDrawerVisible = ref(false)
|
||||
// 是否在线
|
||||
const isOnline = ref(true)
|
||||
|
||||
interface Person {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
online?: boolean
|
||||
lastTime: string
|
||||
unread?: number
|
||||
}
|
||||
|
||||
const selectedPerson = ref<Person | null>(null)
|
||||
|
||||
const personList = ref<Person[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: '梅洛迪·梅西',
|
||||
email: 'melody@altbox.com',
|
||||
avatar: meAvatar,
|
||||
online: true,
|
||||
lastTime: '20小时前',
|
||||
unread: 0
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '马克·史密斯',
|
||||
email: 'max@kt.com',
|
||||
avatar: avatar2,
|
||||
online: true,
|
||||
lastTime: '2周前',
|
||||
unread: 6
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '肖恩·宾',
|
||||
email: 'sean@dellito.com',
|
||||
avatar: avatar3,
|
||||
online: false,
|
||||
lastTime: '5小时前',
|
||||
unread: 5
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '爱丽丝·约翰逊',
|
||||
email: 'alice@domain.com',
|
||||
avatar: avatar4,
|
||||
online: true,
|
||||
lastTime: '1小时前',
|
||||
unread: 2
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '鲍勃·布朗',
|
||||
email: 'bob@domain.com',
|
||||
avatar: avatar5,
|
||||
online: false,
|
||||
lastTime: '3天前',
|
||||
unread: 1
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '查理·戴维斯',
|
||||
email: 'charlie@domain.com',
|
||||
avatar: avatar6,
|
||||
online: true,
|
||||
lastTime: '10分钟前',
|
||||
unread: 0
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: '戴安娜·普林斯',
|
||||
email: 'diana@domain.com',
|
||||
avatar: avatar7,
|
||||
online: true,
|
||||
lastTime: '15分钟前',
|
||||
unread: 3
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: '伊桑·亨特',
|
||||
email: 'ethan@domain.com',
|
||||
avatar: avatar8,
|
||||
online: true,
|
||||
lastTime: '5分钟前',
|
||||
unread: 0
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: '杰西卡·琼斯',
|
||||
email: 'jessica@domain.com',
|
||||
avatar: avatar9,
|
||||
online: false,
|
||||
lastTime: '1天前',
|
||||
unread: 4
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: '彼得·帕克',
|
||||
email: 'peter@domain.com',
|
||||
avatar: avatar10,
|
||||
online: true,
|
||||
lastTime: '2小时前',
|
||||
unread: 1
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: '克拉克·肯特',
|
||||
email: 'clark@domain.com',
|
||||
avatar: avatar3,
|
||||
online: true,
|
||||
lastTime: '30分钟前',
|
||||
unread: 2
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: '布鲁斯·韦恩',
|
||||
email: 'bruce@domain.com',
|
||||
avatar: avatar5,
|
||||
online: false,
|
||||
lastTime: '3天前',
|
||||
unread: 0
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: '韦德·威尔逊',
|
||||
email: 'wade@domain.com',
|
||||
avatar: avatar6,
|
||||
online: true,
|
||||
lastTime: '10分钟前',
|
||||
unread: 5
|
||||
}
|
||||
])
|
||||
|
||||
const selectPerson = (person: Person) => {
|
||||
selectedPerson.value = person
|
||||
}
|
||||
|
||||
// 消息相关数据
|
||||
const messageText = ref('')
|
||||
const messages = ref([
|
||||
{
|
||||
id: 1,
|
||||
sender: 'Art Bot',
|
||||
content: '你好!我是你的AI助手,有什么我可以帮你的吗?',
|
||||
time: '10:00',
|
||||
isMe: false,
|
||||
avatar: aiAvatar
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
sender: 'Ricky',
|
||||
content: '我想了解一下系统的使用方法。',
|
||||
time: '10:01',
|
||||
isMe: true,
|
||||
avatar: meAvatar
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
sender: 'Art Bot',
|
||||
content: '好的,我来为您介绍系统的主要功能。首先,您可以通过左侧菜单访问不同的功能模块...',
|
||||
time: '10:02',
|
||||
isMe: false,
|
||||
avatar: aiAvatar
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
sender: 'Ricky',
|
||||
content: '听起来很不错,能具体讲讲数据分析部分吗?',
|
||||
time: '10:05',
|
||||
isMe: true,
|
||||
avatar: meAvatar
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
sender: 'Art Bot',
|
||||
content: '当然可以。数据分析模块可以帮助您实时监控关键指标,并生成详细的报表...',
|
||||
time: '10:06',
|
||||
isMe: false,
|
||||
avatar: aiAvatar
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
sender: 'Ricky',
|
||||
content: '太好了,那我如何开始使用呢?',
|
||||
time: '10:08',
|
||||
isMe: true,
|
||||
avatar: meAvatar
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
sender: 'Art Bot',
|
||||
content: '您可以先创建一个项目,然后在项目中添加相关的数据源,系统会自动进行分析。',
|
||||
time: '10:09',
|
||||
isMe: false,
|
||||
avatar: aiAvatar
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
sender: 'Ricky',
|
||||
content: '明白了,谢谢你的帮助!',
|
||||
time: '10:10',
|
||||
isMe: true,
|
||||
avatar: meAvatar
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
sender: 'Art Bot',
|
||||
content: '不客气,有任何问题随时联系我。',
|
||||
time: '10:11',
|
||||
isMe: false,
|
||||
avatar: aiAvatar
|
||||
}
|
||||
])
|
||||
|
||||
const messageId = ref(10) // 用于生成唯一的消息ID
|
||||
|
||||
const userAvatar = ref(meAvatar) // 使用导入的头像
|
||||
|
||||
// 发送消息
|
||||
const sendMessage = () => {
|
||||
const text = messageText.value.trim()
|
||||
if (!text) return
|
||||
|
||||
messages.value.push({
|
||||
id: messageId.value++,
|
||||
sender: 'Ricky',
|
||||
content: text,
|
||||
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
||||
isMe: true,
|
||||
avatar: userAvatar.value
|
||||
})
|
||||
|
||||
messageText.value = ''
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
const messageContainer = ref<HTMLElement | null>(null)
|
||||
const scrollToBottom = () => {
|
||||
setTimeout(() => {
|
||||
if (messageContainer.value) {
|
||||
messageContainer.value.scrollTop = messageContainer.value.scrollHeight
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const openChat = () => {
|
||||
isDrawerVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
scrollToBottom()
|
||||
mittBus.on('openChat', openChat)
|
||||
|
||||
selectedPerson.value = personList.value[0]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.chat-modal {
|
||||
.el-overlay {
|
||||
background-color: rgb(0 0 0 / 20%) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
background-color: var(--art-main-bg-color);
|
||||
border: 1px solid var(--art-border-color);
|
||||
border-radius: 10px;
|
||||
|
||||
.person-list {
|
||||
box-sizing: border-box;
|
||||
width: 360px;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
border-right: 1px solid var(--art-border-color);
|
||||
|
||||
.person-item-header {
|
||||
padding-bottom: 20px;
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
|
||||
.user-details {
|
||||
.name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--art-gray-900);
|
||||
}
|
||||
|
||||
.email {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--art-gray-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-box {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.sort-btn {
|
||||
margin-top: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.person-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
margin-right: 12px;
|
||||
|
||||
.status-dot {
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
bottom: 1px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background-color: var(--el-color-error);
|
||||
border-radius: 50%;
|
||||
|
||||
&.online {
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.person-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.info-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.person-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.last-time {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.info-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.email {
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.unread-badge {
|
||||
:deep(.el-badge__content) {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-modal {
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 16px 0;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.header-left {
|
||||
.name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
margin-top: 6px;
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
&.online {
|
||||
background-color: var(--el-color-success);
|
||||
}
|
||||
|
||||
&.offline {
|
||||
background-color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
color: var(--art-gray-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.btn {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--art-gray-200);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
color: var(--art-text-gray-700);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 85px);
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
padding: 30px 16px;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px !important;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.message-text {
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-900);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&.message-left {
|
||||
justify-content: flex-start;
|
||||
|
||||
.message-content {
|
||||
align-items: flex-start;
|
||||
|
||||
.message-info {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
background-color: var(--art-gray-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.message-right {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.message-content {
|
||||
align-items: flex-end;
|
||||
|
||||
.message-info {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
background-color: #e9f3ff;
|
||||
background-color: rgb(var(--art-bg-secondary));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 70%;
|
||||
|
||||
.message-info {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
.message-time {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.sender-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.message-text {
|
||||
padding: 10px 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
padding: 16px; // 增加填充以提升输入区域的布局
|
||||
|
||||
.input-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.chat-input-actions {
|
||||
display: flex;
|
||||
align-items: center; // 修正为单数
|
||||
justify-content: space-between;
|
||||
margin-top: 12px;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 20px;
|
||||
font-size: 16px;
|
||||
color: var(--art-gray-500);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保发送按钮与输入框对齐
|
||||
el-button {
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-ipad-pro) {
|
||||
.chat {
|
||||
flex-direction: column;
|
||||
|
||||
.person-list {
|
||||
width: 100%;
|
||||
height: 170px;
|
||||
border-right: none;
|
||||
|
||||
.person-item-header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-modal {
|
||||
height: calc(70% - 30px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="page-content" v-loading="loading" element-loading-text="加载中...">
|
||||
<ArtMapChart @onRenderComplete="onLoaded" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const loading = ref(true)
|
||||
const onLoaded = () => {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const ArtMapChart = defineAsyncComponent(() => import('@/components/core/charts/ArtMapChart.vue'))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<div class="pricing-container">
|
||||
<div class="pricing-header">
|
||||
<h1 class="title">超过 53,476 位信赖的开发者</h1>
|
||||
<h2 class="subtitle">以及众多科技巨头的选择</h2>
|
||||
</div>
|
||||
|
||||
<div class="pricing-cards">
|
||||
<ElRow :gutter="20" justify="center">
|
||||
<ElCol v-for="plan in pricingPlans" :key="plan.type" :xs="24" :sm="12" :md="6">
|
||||
<ElCard class="pricing-card" :class="{ popular: plan.isPopular }" shadow="never">
|
||||
<div class="card-header">
|
||||
<h3>{{ plan.title }}</h3>
|
||||
<p class="description">{{ plan.description }}</p>
|
||||
<div class="price">
|
||||
<span class="amount">¥{{ plan.price }}</span>
|
||||
<span class="period">/一次性付款</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="features">
|
||||
<div v-for="(feature, index) in plan.features" :key="index" class="feature-item">
|
||||
<ElIcon :class="feature.available ? 'available' : 'unavailable'">
|
||||
<Check v-if="feature.available" />
|
||||
<Close v-else />
|
||||
</ElIcon>
|
||||
<span>{{ feature.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<ElButton type="primary" class="purchase-btn" v-ripple>立即购买</ElButton>
|
||||
</div>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Check, Close } from '@element-plus/icons-vue'
|
||||
|
||||
interface Feature {
|
||||
text: string
|
||||
available: boolean
|
||||
}
|
||||
|
||||
interface PricingPlan {
|
||||
type: string
|
||||
title: string
|
||||
description: string
|
||||
price: number
|
||||
isPopular: boolean
|
||||
features: Feature[]
|
||||
}
|
||||
|
||||
const pricingPlans = ref<PricingPlan[]>([
|
||||
{
|
||||
type: 'single',
|
||||
title: '单次使用版',
|
||||
description: '适用于单个最终产品,最终用户无需付费。',
|
||||
price: 349,
|
||||
isPopular: false,
|
||||
features: [
|
||||
{ text: '完整源代码', available: true },
|
||||
{ text: '技术文档', available: true },
|
||||
{ text: 'SaaS应用授权', available: false },
|
||||
{ text: '单个项目使用', available: true },
|
||||
{ text: '一年技术支持', available: true },
|
||||
{ text: '一年免费更新', available: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'multiple',
|
||||
title: '多次使用版',
|
||||
description: '适用于无限个最终产品,最终用户无需付费。',
|
||||
price: 629,
|
||||
isPopular: false,
|
||||
features: [
|
||||
{ text: '完整源代码', available: true },
|
||||
{ text: '技术文档', available: true },
|
||||
{ text: 'SaaS应用授权', available: false },
|
||||
{ text: '无限项目使用', available: true },
|
||||
{ text: '一年技术支持', available: true },
|
||||
{ text: '一年免费更新', available: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'extended',
|
||||
title: '扩展授权版',
|
||||
description: '适用于单个最终产品,最终用户需要付费。',
|
||||
price: 2099,
|
||||
isPopular: false,
|
||||
features: [
|
||||
{ text: '完整源代码', available: true },
|
||||
{ text: '技术文档', available: true },
|
||||
{ text: 'SaaS应用授权', available: true },
|
||||
{ text: '单个项目使用', available: true },
|
||||
{ text: '一年技术支持', available: true },
|
||||
{ text: '一年免费更新', available: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'unlimited',
|
||||
title: '无限授权版',
|
||||
description: '适用于无限个最终产品,最终用户需要付费。',
|
||||
price: 3499,
|
||||
isPopular: false,
|
||||
features: [
|
||||
{ text: '完整源代码', available: true },
|
||||
{ text: '技术文档', available: true },
|
||||
{ text: 'SaaS应用授权', available: true },
|
||||
{ text: '无限项目使用', available: true },
|
||||
{ text: '一年技术支持', available: true },
|
||||
{ text: '一年免费更新', available: true }
|
||||
]
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pricing-container {
|
||||
padding: 5rem 1rem !important;
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
|
||||
.pricing-header {
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
|
||||
.title {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.pricing-cards {
|
||||
margin-top: 80px;
|
||||
|
||||
.el-col {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pricing-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
|
||||
&.popular {
|
||||
position: relative;
|
||||
border: 2px solid var(--el-color-primary);
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-color-primary);
|
||||
content: '热门';
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.3rem;
|
||||
color: var(--art-text-gray-900) !important;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: -webkit-box;
|
||||
height: 40px;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
color: var(--art-text-gray-600);
|
||||
text-overflow: ellipsis;
|
||||
border-bottom: 1px solid var(--art-border-color);
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.price {
|
||||
margin-top: 30px;
|
||||
|
||||
.amount {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.period {
|
||||
margin-left: 10px;
|
||||
font-size: 14px;
|
||||
color: var(--art-text-gray-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.features {
|
||||
flex-grow: 1;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
|
||||
.el-icon {
|
||||
margin-right: 10px;
|
||||
|
||||
&.available {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.unavailable {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
margin-top: auto;
|
||||
text-align: center;
|
||||
|
||||
.purchase-btn {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-notebook) {
|
||||
.pricing-container {
|
||||
padding: 2rem 0 !important;
|
||||
|
||||
.pricing-cards {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-phone) {
|
||||
.pricing-container {
|
||||
.pricing-header {
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.el-col {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElButton @contextmenu.prevent="showMenu"> 右键触发菜单 </ElButton>
|
||||
|
||||
<!-- 右键菜单组件 -->
|
||||
<ArtMenuRight
|
||||
ref="menuRef"
|
||||
:menu-items="menuItems"
|
||||
:menu-width="180"
|
||||
:submenu-width="140"
|
||||
:border-radius="10"
|
||||
@select="handleSelect"
|
||||
@show="onMenuShow"
|
||||
@hide="onMenuHide"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
import ArtMenuRight from '@/components/core/others/art-menu-right/index.vue'
|
||||
import type { MenuItemType } from '@/components/core/others/art-menu-right/index.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const menuRef = ref<InstanceType<typeof ArtMenuRight>>()
|
||||
const lastAction = ref('')
|
||||
|
||||
// 右键菜单选项
|
||||
const menuItems = computed((): MenuItemType[] => [
|
||||
{
|
||||
key: 'copy',
|
||||
label: '复制',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
key: 'paste',
|
||||
label: '粘贴',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
key: 'cut',
|
||||
label: '剪切',
|
||||
icon: '',
|
||||
showLine: true
|
||||
},
|
||||
{
|
||||
key: 'export',
|
||||
label: '导出选项',
|
||||
icon: '',
|
||||
children: [
|
||||
{
|
||||
key: 'exportExcel',
|
||||
label: '导出 Excel',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
key: 'exportPdf',
|
||||
label: '导出 PDF',
|
||||
icon: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'edit',
|
||||
label: '编辑选项',
|
||||
icon: '',
|
||||
children: [
|
||||
{
|
||||
key: 'rename',
|
||||
label: '重命名',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
key: 'duplicate',
|
||||
label: '复制副本',
|
||||
icon: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'share',
|
||||
label: '分享',
|
||||
icon: '',
|
||||
showLine: true
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
key: 'disabled',
|
||||
label: '禁用选项',
|
||||
icon: '',
|
||||
disabled: true
|
||||
}
|
||||
])
|
||||
|
||||
const handleSelect = (item: MenuItemType) => {
|
||||
lastAction.value = `${item.label} (${item.key})`
|
||||
ElMessage.success(`执行操作: ${item.label}`)
|
||||
console.log('选择了菜单项:', item)
|
||||
}
|
||||
|
||||
const showMenu = (e: MouseEvent) => {
|
||||
console.log('触发右键菜单', e)
|
||||
// 确保阻止默认行为
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
// 延迟一帧执行,确保事件处理完成
|
||||
nextTick(() => {
|
||||
menuRef.value?.show(e)
|
||||
})
|
||||
}
|
||||
|
||||
const onMenuShow = () => {
|
||||
console.log('菜单显示')
|
||||
}
|
||||
|
||||
const onMenuHide = () => {
|
||||
console.log('菜单隐藏')
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,103 @@
|
||||
<!-- 文档:https://github.com/PanJiaChen/vue-countTo -->
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 基础用法 -->
|
||||
<h2>基础用法</h2>
|
||||
<CountTo :endVal="count1" :duration="1000"></CountTo>
|
||||
|
||||
<!-- 带前缀后缀 -->
|
||||
<h2>带前缀后缀</h2>
|
||||
<CountTo prefix="¥" suffix="元" :startVal="0" :endVal="count2" :duration="2000"></CountTo>
|
||||
|
||||
<!-- 小数点和分隔符 -->
|
||||
<h2>小数点和分隔符</h2>
|
||||
<CountTo
|
||||
:startVal="0"
|
||||
:endVal="count3"
|
||||
:decimals="2"
|
||||
decimal="."
|
||||
separator=","
|
||||
:duration="2500"
|
||||
></CountTo>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<h2>控制按钮</h2>
|
||||
<CountTo
|
||||
ref="countTo"
|
||||
:startVal="0"
|
||||
:endVal="count4"
|
||||
:duration="3000"
|
||||
:autoplay="false"
|
||||
></CountTo>
|
||||
|
||||
<div class="mt-4">
|
||||
<ElButtonGroup>
|
||||
<ElButton @click="start" v-ripple>开始</ElButton>
|
||||
<ElButton @click="pause" v-ripple>暂停</ElButton>
|
||||
<ElButton @click="reset" v-ripple>重置</ElButton>
|
||||
</ElButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { CountTo } from 'vue3-count-to'
|
||||
|
||||
const count1 = ref(1000)
|
||||
const count2 = ref(19999.99)
|
||||
const count3 = ref(2023.45)
|
||||
const count4 = ref(5000)
|
||||
|
||||
const countTo = ref()
|
||||
const isCounting = ref(false)
|
||||
|
||||
// 控制方法
|
||||
const start = () => {
|
||||
if (isCounting.value) return
|
||||
|
||||
try {
|
||||
countTo.value?.reset()
|
||||
countTo.value?.start()
|
||||
isCounting.value = true
|
||||
} catch (error) {
|
||||
console.error('启动计数器失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const pause = () => {
|
||||
if (!isCounting.value) return
|
||||
|
||||
try {
|
||||
countTo.value?.pause()
|
||||
isCounting.value = false
|
||||
} catch (error) {
|
||||
console.error('暂停计数器失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
try {
|
||||
countTo.value?.reset()
|
||||
isCounting.value = false
|
||||
} catch (error) {
|
||||
console.error('重置计数器失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 20px 0;
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<!-- https://vue-draggable-plus.pages.dev/ -->
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCard shadow="never" style="width: 300px; margin-right: 20px">
|
||||
<template #header>
|
||||
<span class="card-header">基础示例</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<VueDraggable ref="el" v-model="userList">
|
||||
<div class="demo1-item" v-for="item in userList" :key="item.name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</VueDraggable>
|
||||
</template>
|
||||
</ElCard>
|
||||
|
||||
<ElCard shadow="never" style="width: 300px">
|
||||
<template #header>
|
||||
<span class="card-header">过渡动画</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<VueDraggable v-model="userList" target=".sort-target" :scroll="true">
|
||||
<TransitionGroup type="transition" tag="ul" name="fade" class="sort-target">
|
||||
<li v-for="item in userList" :key="item.name" class="demo1-item">
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</TransitionGroup>
|
||||
</VueDraggable>
|
||||
</template>
|
||||
</ElCard>
|
||||
</ElRow>
|
||||
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<span class="card-header">表格拖拽排序</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<VueDraggable target="tbody" v-model="userList" :animation="150">
|
||||
<ArtTable :data="userList" :pagination="false">
|
||||
<ElTableColumn label="姓名" prop="name" />
|
||||
<ElTableColumn label="角色" prop="role" />
|
||||
</ArtTable>
|
||||
</VueDraggable>
|
||||
</template>
|
||||
</ElCard>
|
||||
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<span class="card-header">指定元素拖拽排序</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<VueDraggable target="tbody" handle=".handle" v-model="userList" :animation="150">
|
||||
<ArtTable :data="userList" :pagination="false">
|
||||
<ElTableColumn label="姓名" prop="name" />
|
||||
<ElTableColumn label="角色" prop="role" />
|
||||
<ElTableColumn label="操作" width="100">
|
||||
<ElButton size="default" class="handle"> 移动 </ElButton>
|
||||
</ElTableColumn>
|
||||
</ArtTable>
|
||||
</VueDraggable>
|
||||
</template>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
|
||||
const userList = ref([
|
||||
{
|
||||
name: '孙悟空',
|
||||
role: '斗战胜佛'
|
||||
},
|
||||
{
|
||||
name: '猪八戒',
|
||||
role: '净坛使者'
|
||||
},
|
||||
{
|
||||
name: '沙僧',
|
||||
role: '金身罗汉'
|
||||
},
|
||||
{
|
||||
name: '唐僧',
|
||||
role: '旃檀功德佛'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.demo1-item {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
cursor: move;
|
||||
background-color: rgba(var(--art-gray-200-rgb), 0.8);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.el-card {
|
||||
margin-bottom: 30px;
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fade-move,
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: scaleY(0.01) translate(30px, 0);
|
||||
}
|
||||
|
||||
.fade-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ArtExcelImport @import-success="handleImportSuccess" @import-error="handleImportError">
|
||||
<template #import-text> 上传 Excel </template>
|
||||
</ArtExcelImport>
|
||||
|
||||
<ArtExcelExport
|
||||
style="margin-left: 10px"
|
||||
:data="tableData"
|
||||
filename="用户数据"
|
||||
sheetName="用户列表"
|
||||
type="success"
|
||||
:headers="headers"
|
||||
@export-success="handleExportSuccess"
|
||||
@export-error="handleExportError"
|
||||
>
|
||||
导出 Excel
|
||||
</ArtExcelExport>
|
||||
|
||||
<ElButton type="danger" @click="handleClear" v-ripple>清除数据</ElButton>
|
||||
|
||||
<ArtTable :data="tableData" style="margin-top: 10px">
|
||||
<ElTableColumn
|
||||
v-for="key in Object.keys(headers)"
|
||||
:key="key"
|
||||
:prop="key"
|
||||
:label="headers[key as keyof typeof headers]"
|
||||
/>
|
||||
</ArtTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface TableData {
|
||||
name: string
|
||||
age: number
|
||||
city: string
|
||||
}
|
||||
|
||||
const handleImportSuccess = (data: any[]) => {
|
||||
// 将导入的数据转换为正确的格式
|
||||
const formattedData = data.map((item) => ({
|
||||
name: item['姓名'],
|
||||
age: Number(item['年龄']),
|
||||
city: item['城市']
|
||||
}))
|
||||
tableData.value = formattedData
|
||||
|
||||
// tableData.value = data
|
||||
}
|
||||
|
||||
const handleImportError = (error: Error) => {
|
||||
// 处理导入错误
|
||||
console.error('导入失败:', error)
|
||||
}
|
||||
|
||||
// 使用类型化的ref
|
||||
const tableData = ref<TableData[]>([
|
||||
{ name: '李四', age: 20, city: '上海' },
|
||||
{ name: '张三', age: 25, city: '北京' },
|
||||
{ name: '王五', age: 30, city: '广州' },
|
||||
{ name: '赵六', age: 35, city: '深圳' },
|
||||
{ name: '孙七', age: 28, city: '杭州' },
|
||||
{ name: '周八', age: 32, city: '成都' },
|
||||
{ name: '吴九', age: 27, city: '武汉' },
|
||||
{ name: '郑十', age: 40, city: '南京' },
|
||||
{ name: '刘一', age: 22, city: '重庆' },
|
||||
{ name: '陈二', age: 33, city: '西安' }
|
||||
])
|
||||
|
||||
// 自定义表头映射
|
||||
const headers = {
|
||||
name: '姓名',
|
||||
age: '年龄',
|
||||
city: '城市'
|
||||
}
|
||||
|
||||
const handleExportSuccess = () => {
|
||||
ElMessage.success('导出成功')
|
||||
}
|
||||
|
||||
const handleExportError = (error: Error) => {
|
||||
ElMessage.error(`导出失败: ${error.message}`)
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
tableData.value = []
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<div class="action-buttons">
|
||||
<ElButton :disabled="isLaunching" v-ripple @click="handleSingleLaunch"
|
||||
>✨ 放个小烟花</ElButton
|
||||
>
|
||||
<ElButton :disabled="isLaunching" v-ripple @click="handleImageLaunch(bp)"
|
||||
>🎉 打开幸运红包</ElButton
|
||||
>
|
||||
<ElButton :disabled="isLaunching" v-ripple @click="handleMultipleLaunch('')"
|
||||
>🎆 璀璨烟火秀</ElButton
|
||||
>
|
||||
<ElButton :disabled="isLaunching" v-ripple @click="handleImageLaunch(sd)"
|
||||
>❄️ 飘点小雪花</ElButton
|
||||
>
|
||||
<ElButton :disabled="isLaunching" v-ripple @click="handleMultipleLaunch(sd)"
|
||||
>❄️ 浪漫暴风雪</ElButton
|
||||
>
|
||||
</div>
|
||||
|
||||
<ElDescriptions
|
||||
title="礼花组件说明"
|
||||
direction="vertical"
|
||||
:column="1"
|
||||
border
|
||||
style="margin-top: 50px"
|
||||
>
|
||||
<ElDescriptionsItem label="显示时机">
|
||||
礼花效果组件全局注册了,在节假日的时候,会自动显示,你可以通过配置文件来控制显示时机
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="礼花样式">
|
||||
默认显示几何图形,可以配置图片,图片需要提前在 components/Ceremony/Fireworks 文件预先定义
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="节日配置">
|
||||
在 src/config/festival.ts 文件中,可以配置节日和对应的礼花样式
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="快捷键">
|
||||
command + shift + p 或者 ctrl + shift + p
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mittBus } from '@/utils/sys'
|
||||
|
||||
import bp from '@imgs/ceremony/hb.png'
|
||||
import sd from '@imgs/ceremony/sd.png'
|
||||
|
||||
const timerRef = ref<ReturnType<typeof setInterval> | null>(null)
|
||||
const isLaunching = ref(false)
|
||||
|
||||
const triggerFireworks = (count: number, src: string) => {
|
||||
// 清除之前的定时器
|
||||
if (timerRef.value) {
|
||||
clearInterval(timerRef.value)
|
||||
timerRef.value = null
|
||||
}
|
||||
|
||||
isLaunching.value = true // 开始发射时设置状态
|
||||
|
||||
let fired = 0
|
||||
timerRef.value = setInterval(() => {
|
||||
mittBus.emit('triggerFireworks', src)
|
||||
fired++
|
||||
|
||||
// 达到指定次数后清除定时器
|
||||
if (fired >= count) {
|
||||
clearInterval(timerRef.value!)
|
||||
timerRef.value = null
|
||||
isLaunching.value = false // 发射完成后解除禁用
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 简化后的处理函数
|
||||
const handleSingleLaunch = () => {
|
||||
mittBus.emit('triggerFireworks')
|
||||
}
|
||||
|
||||
const handleMultipleLaunch = (src: string) => {
|
||||
triggerFireworks(10, src)
|
||||
}
|
||||
|
||||
const handleImageLaunch = (src: string) => {
|
||||
mittBus.emit('triggerFireworks', src)
|
||||
}
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
if (timerRef.value) {
|
||||
clearInterval(timerRef.value)
|
||||
timerRef.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.action-buttons {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<div class="form">
|
||||
<ElSelect v-model="iconType" placeholder="Select" style="width: 240px">
|
||||
<ElOption
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div class="colors-icon">
|
||||
<ElCheckbox v-model="isColorsIcon" label="彩色图标" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="list">
|
||||
<ul class="icon-list">
|
||||
<li v-for="icon in systemIconClasses" :key="icon.className" @click="copyIcon(icon)">
|
||||
<i
|
||||
class="iconfont-sys"
|
||||
v-if="iconType === 'unicode'"
|
||||
v-html="icon.unicode"
|
||||
:style="getIconStyle()"
|
||||
></i>
|
||||
<i :class="`iconfont-sys ${icon.className}`" v-else :style="getIconStyle()"></i>
|
||||
<span>{{ iconType === 'unicode' ? icon.unicode : icon.className }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { extractIconClasses, IconfontType } from '@/utils/constants'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const iconType = ref('unicode')
|
||||
const options = [
|
||||
{
|
||||
value: 'unicode',
|
||||
label: 'Unicode'
|
||||
},
|
||||
{
|
||||
value: 'fontClass',
|
||||
label: 'Font class'
|
||||
}
|
||||
]
|
||||
const systemIconClasses = ref<IconfontType[]>([])
|
||||
|
||||
const isColorsIcon = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
systemIconClasses.value = extractIconClasses()
|
||||
})
|
||||
|
||||
const copyIcon = (text: IconfontType) => {
|
||||
if (!text) return
|
||||
|
||||
let copyipt = document.createElement('input')
|
||||
copyipt.setAttribute(
|
||||
'value',
|
||||
(iconType.value === 'unicode' ? text.unicode : text.className) || ''
|
||||
)
|
||||
document.body.appendChild(copyipt)
|
||||
copyipt.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(copyipt)
|
||||
|
||||
ElMessage.success(`已复制`)
|
||||
}
|
||||
|
||||
const getRandomColor = () => {
|
||||
const colors = ['#2d8cf0', '#19be6b', '#ff9900', '#f24965', '#9463f7']
|
||||
return colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
|
||||
const getIconStyle = () => {
|
||||
return isColorsIcon.value ? { color: getRandomColor() } : { color: 'var(--art-text-gray-700)' }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
$border-color: #eee;
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.colors-icon {
|
||||
box-sizing: border-box;
|
||||
height: var(--el-component-custom-height);
|
||||
padding: 0 30px;
|
||||
margin-left: 10px;
|
||||
border: 1px solid var(--art-border-dashed-color);
|
||||
border-radius: calc(var(--custom-radius) / 3 + 2px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 20px;
|
||||
|
||||
.icon-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
width: calc(100% + 16px);
|
||||
|
||||
li {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
aspect-ratio: 1 / 1;
|
||||
padding: 0 8px;
|
||||
margin: 0 16px 16px 0;
|
||||
overflow: hidden;
|
||||
color: rgba(#fff, 0.8);
|
||||
text-align: center;
|
||||
border: 1px solid rgb(var(--art-gray-300-rgb), 0.8);
|
||||
border-radius: 12px !important;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background: var(--art-gray-100);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 26px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--art-text-gray-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-notebook) {
|
||||
.page-content {
|
||||
.list {
|
||||
.icon-list {
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-ipad-vertical) {
|
||||
.page-content {
|
||||
.list {
|
||||
.icon-list {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $device-phone) {
|
||||
.page-content {
|
||||
.list {
|
||||
.icon-list {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<div class="select">
|
||||
<div class="item">
|
||||
<h3>Unicode</h3>
|
||||
<ArtIconSelector v-model="icon1" :iconType="IconTypeEnum.UNICODE" />
|
||||
</div>
|
||||
<div class="item">
|
||||
<h3>ClassName</h3>
|
||||
<ArtIconSelector v-model="icon2" :iconType="IconTypeEnum.CLASS_NAME" width="260px" />
|
||||
</div>
|
||||
<div class="item">
|
||||
<h3>禁用</h3>
|
||||
<ArtIconSelector
|
||||
v-model="icon3"
|
||||
:iconType="IconTypeEnum.CLASS_NAME"
|
||||
width="260px"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { IconTypeEnum } from '@/enums/appEnum'
|
||||
|
||||
const icon1 = ref('')
|
||||
const icon2 = ref('iconsys-baitianmoshi3')
|
||||
const icon3 = ref('iconsys-baitianmoshi3')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.select {
|
||||
.item {
|
||||
margin-bottom: 30px;
|
||||
|
||||
h3 {
|
||||
padding-bottom: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ArtCutterImg
|
||||
v-model:imgUrl="imageUrl"
|
||||
:boxWidth="540"
|
||||
:boxHeight="300"
|
||||
:cutWidth="360"
|
||||
:cutHeight="200"
|
||||
:quality="1"
|
||||
:tool="true"
|
||||
:watermarkText="'My Watermark'"
|
||||
watermarkColor="#ff0000"
|
||||
:showPreview="true"
|
||||
:originalGraph="false"
|
||||
:title="'图片裁剪'"
|
||||
:previewTitle="'预览效果'"
|
||||
@error="handleError"
|
||||
@imageLoadComplete="handleLoadComplete"
|
||||
@imageLoadError="handleLoadError"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import lockImg from '@imgs/lock/lock_screen_1.webp'
|
||||
const imageUrl = ref(lockImg)
|
||||
|
||||
const handleError = (error: any) => {
|
||||
console.error('裁剪错误:', error)
|
||||
}
|
||||
|
||||
const handleLoadComplete = (result: any) => {
|
||||
console.log('图片加载完成:', result)
|
||||
}
|
||||
|
||||
const handleLoadError = (error: any) => {
|
||||
console.error('图片加载失败:', error)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="6" v-for="preset in qrcodePresets" :key="preset.title">
|
||||
<ElCard class="qrcode-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ preset.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="qrcode-preview">
|
||||
<QrcodeVue :value="qrValue" v-bind="preset.config" />
|
||||
</div>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import type { Level, RenderAs, ImageSettings } from 'qrcode.vue'
|
||||
|
||||
// 二维码内容
|
||||
const qrValue = ref('https://www.lingchen.kim')
|
||||
const isShowLogo = ref(false)
|
||||
|
||||
// 预设二维码样式配置
|
||||
const qrcodePresets = [
|
||||
{
|
||||
title: '渲染成 img 标签',
|
||||
config: {
|
||||
size: 160,
|
||||
level: 'H' as Level,
|
||||
renderAs: 'canvas' as RenderAs,
|
||||
margin: 0,
|
||||
background: '#ffffff',
|
||||
foreground: '#000000'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '渲染成 canvas 标签',
|
||||
config: {
|
||||
size: 160,
|
||||
level: 'H' as Level,
|
||||
renderAs: 'canvas' as RenderAs,
|
||||
margin: 0,
|
||||
background: '#ffffff',
|
||||
foreground: '#000000'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '自定义颜色',
|
||||
config: {
|
||||
size: 160,
|
||||
level: 'H' as Level,
|
||||
renderAs: 'canvas' as RenderAs,
|
||||
margin: 0,
|
||||
background: '#f0f0f0',
|
||||
foreground: '#4080ff'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '带有Logo',
|
||||
config: {
|
||||
size: 160,
|
||||
level: 'H' as Level,
|
||||
renderAs: 'canvas' as RenderAs,
|
||||
margin: 0,
|
||||
background: '#ffffff',
|
||||
foreground: '#000000',
|
||||
imageSettings: {
|
||||
src: 'https://www.lingchen.kim/art-design-pro/assets/avatar-DJIoI-3F.png',
|
||||
width: 40,
|
||||
height: 40,
|
||||
excavate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 二维码配置
|
||||
const qrcodeConfig = reactive({
|
||||
size: 160,
|
||||
level: 'H' as Level,
|
||||
renderAs: 'canvas' as RenderAs,
|
||||
margin: 0,
|
||||
background: '#ffffff',
|
||||
foreground: '#000000',
|
||||
imageSettings: {
|
||||
src: 'https://www.lingchen.kim/art-design-pro/assets/avatar-DJIoI-3F.png',
|
||||
width: 40,
|
||||
height: 40,
|
||||
excavate: true
|
||||
} as ImageSettings
|
||||
})
|
||||
|
||||
// 监听是否显示 logo
|
||||
watch(isShowLogo, (val) => {
|
||||
if (!val) {
|
||||
qrcodeConfig.imageSettings = {} as ImageSettings
|
||||
} else {
|
||||
qrcodeConfig.imageSettings = {
|
||||
src: 'https://www.lingchen.kim/art-design-pro/assets/avatar-DJIoI-3F.png',
|
||||
width: 40,
|
||||
height: 40,
|
||||
excavate: true
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
padding: 20px;
|
||||
|
||||
.qrcode-card {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.qrcode-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 基础用法 -->
|
||||
<ArtTextScroll
|
||||
text="Art Design Pro 是一款专注于用户体验和视觉设计的后台管理系统模版 <a target='_blank' href='https://www.lingchen.kim/art-design-pro/docs/'>点击我 </a>访问官方文档"
|
||||
showClose
|
||||
/>
|
||||
|
||||
<!-- 使用不同的类型 -->
|
||||
<ArtTextScroll type="success" text="这是一条成功类型的滚动公告" showClose />
|
||||
|
||||
<ArtTextScroll type="warning" text="这是一条警告类型的滚动公告" showClose />
|
||||
|
||||
<ArtTextScroll type="danger" text="这是一条危险类型的滚动公告" showClose />
|
||||
|
||||
<ArtTextScroll type="info" text="这是一条信息类型的滚动公告" showClose />
|
||||
|
||||
<!-- 可关闭 -->
|
||||
<ArtTextScroll text="这是一条可关闭的滚动公告" showClose @close="handleClose" />
|
||||
|
||||
<!-- 使用打字机效果 -->
|
||||
<ArtTextScroll
|
||||
text="这是一条打字机效果的滚动公告,打字机速度为 200 毫秒"
|
||||
typewriter
|
||||
:typewriter-speed="200"
|
||||
/>
|
||||
|
||||
<!-- 自定义速度和方向 -->
|
||||
<ArtTextScroll text="这是一条速度较慢、向右滚动的公告" :speed="30" direction="right" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const handleClose = () => {
|
||||
console.log('关闭')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
:deep(.text-scroll-container) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<div class="video-container">
|
||||
<ArtVideoPlayer
|
||||
playerId="my-video-1"
|
||||
:videoUrl="videoUrl"
|
||||
:posterUrl="posterUrl"
|
||||
:autoplay="false"
|
||||
:volume="1"
|
||||
:playbackRates="[0.5, 1, 1.5, 2]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import lockImg from '@imgs/lock/lock_screen_1.webp'
|
||||
|
||||
// 视频源和封面图片URL
|
||||
const videoUrl = ref(
|
||||
'//lf3-static.bytednsdoc.com/obj/eden-cn/nupenuvpxnuvo/xgplayer_doc/xgplayer-demo.mp4'
|
||||
)
|
||||
const posterUrl = ref(lockImg)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.video-container {
|
||||
max-width: 600px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ArtWangEditor v-model="editorHtml" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const editorHtml = ref(`<h1>欢迎使用富文本编辑器</h1>
|
||||
<p>这是一个段落示例,下面展示了一些常用的富文本格式:</p>
|
||||
|
||||
<h2>文本样式</h2>
|
||||
<p><strong>这是加粗的文字</strong></p>
|
||||
<p><em>这是斜体文字</em></p>
|
||||
|
||||
<h2>列表示例</h2>
|
||||
<ul>
|
||||
<li>无序列表项 1</li>
|
||||
<li>无序列表项 2</li>
|
||||
<li>无序列表项 3</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<ol>
|
||||
<li>有序列表项 1</li>
|
||||
<li>有序列表项 2</li>
|
||||
<li>有序列表项 3</li>
|
||||
</ol>
|
||||
|
||||
<h2>引用示例</h2>
|
||||
<blockquote style="border-left: 4px solid #ccc; margin-left: 0; padding-left: 1em;">
|
||||
这是一段引用文字,可以用来突出显示重要内容。
|
||||
</blockquote>
|
||||
|
||||
<h2>表格示例</h2>
|
||||
<table border="1" cellpadding="5">
|
||||
<tr>
|
||||
<th>表头 1</th>
|
||||
<th>表头 2</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>单元格 1</td>
|
||||
<td>单元格 2</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>代码示例</h2>
|
||||
<pre><code class="language-javascript">// JavaScript 代码示例
|
||||
function greeting(name) {
|
||||
return \`Hello, \${name}!\`;
|
||||
}
|
||||
|
||||
console.log(greeting('世界'));</code></pre>
|
||||
|
||||
<pre><code class="language-python"># Python 代码示例
|
||||
def fibonacci(n):
|
||||
if n <= 1:
|
||||
return n
|
||||
return fibonacci(n-1) + fibonacci(n-2)
|
||||
|
||||
print(fibonacci(10))</code></pre>
|
||||
|
||||
<h2>链接示例</h2>
|
||||
<p><a href="https://www.lingchen.kim/art-design-pro/docs/">这是一个超链接</a></p>
|
||||
`)
|
||||
</script>
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<!-- 基础文字水印 -->
|
||||
<ElCard class="card" shadow="never">
|
||||
<template #header>基础文字水印</template>
|
||||
<ElWatermark content="Art Design Pro" :font="{ color: 'rgba(128, 128, 128, 0.2)' }">
|
||||
<div style="height: 200px"></div>
|
||||
</ElWatermark>
|
||||
</ElCard>
|
||||
|
||||
<!-- 多行文字水印 -->
|
||||
<ElCard class="card" shadow="never">
|
||||
<template #header>多行文字水印</template>
|
||||
<ElWatermark
|
||||
:content="['Art Design Pro', '专注用户体验,视觉设计']"
|
||||
:font="{ fontSize: 16, color: 'rgba(128, 128, 128, 0.2)' }"
|
||||
>
|
||||
<div style="height: 200px"></div>
|
||||
</ElWatermark>
|
||||
</ElCard>
|
||||
|
||||
<!-- 图片水印 -->
|
||||
<ElCard class="card" shadow="never">
|
||||
<template #header>图片水印</template>
|
||||
<ElWatermark :image="watermarkImage" :opacity="0.2" :width="80" :height="20">
|
||||
<div style="height: 200px"></div>
|
||||
</ElWatermark>
|
||||
</ElCard>
|
||||
|
||||
<!-- 自定义样式水印 -->
|
||||
<ElCard class="card" shadow="never">
|
||||
<template #header>自定义样式水印</template>
|
||||
<ElWatermark
|
||||
content="Art Design Pro"
|
||||
:font="{
|
||||
fontSize: 20,
|
||||
fontFamily: 'Arial',
|
||||
color: 'rgba(255, 0, 0, 0.3)'
|
||||
}"
|
||||
:rotate="-22"
|
||||
:gap="[100, 100]"
|
||||
>
|
||||
<div style="height: 200px"></div>
|
||||
</ElWatermark>
|
||||
</ElCard>
|
||||
|
||||
<ElButton
|
||||
:type="settingStore.watermarkVisible ? 'danger' : 'primary'"
|
||||
@click="handleWatermarkVisible"
|
||||
>
|
||||
{{ settingStore.watermarkVisible ? '隐藏全局水印' : '显示全局水印' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
// 这里替换成你的实际logo图片地址
|
||||
const watermarkImage = ref('https://element-plus.org/images/element-plus-logo.svg')
|
||||
|
||||
const handleWatermarkVisible = () => {
|
||||
useSettingStore().setWatermarkVisible(!settingStore.watermarkVisible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
padding: 20px;
|
||||
|
||||
.el-card {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,660 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput placeholder="工单编号"></ElInput>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple>搜索</ElButton>
|
||||
<ElButton @click="showDialog('add')" v-ripple>新增</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable ref="tableRef" row-key="id" :loading="loading" :data="tableData" :currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize" :total="pagination.total" @size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange">
|
||||
<template #default>
|
||||
<ElTableColumn label="工单编号" prop="code" />
|
||||
<ElTableColumn label="问题分类" prop="categorie_id">
|
||||
<template #default="scope">
|
||||
<div v-html="scope.row.categorie?scope.row.categorie.name:''"></div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="问题描述" prop="content">
|
||||
<template #default="scope">
|
||||
<div v-html="scope.row.content"></div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status == 1 ? 'info' : scope.row.status == 2 ?'primary' :scope.row.status == 4 ?'success' : 'danger'">
|
||||
{{ scope.row.status == 1 ? '待受理' : scope.row.status == 2 ? '处理中' :
|
||||
scope.row.status == 4 ? "已完成":'关闭' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" />
|
||||
<ElTableColumn fixed="right" label="操作" width="260px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button v-if="scope.row.status == 1" link @click="showDialog('edit', scope.row)"> 编辑 </el-button>
|
||||
<el-button link @click="statusEdit(scope.row)"> 状态编辑 </el-button>
|
||||
<el-button link @click="openChat(scope.row)"> 查看/回复 </el-button>
|
||||
<!-- <el-button link @click="deleteRole()"> 删除 </el-button> -->
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ElDialog v-model="dialogVisible" :title="dialogType === 'add' ? '新增' : '编辑'" width="60%" align-center>
|
||||
<ElForm ref="formRef" :model="form" label-width="160px">
|
||||
<ElFormItem label="问题分类" prop="categorie_id">
|
||||
<ElSelect v-model="form.categorie_id">
|
||||
<ElOption v-for="type in categorieSelectList" :key="type.id" :value="type.id" :label="type.name" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="问题描述" prop="content">
|
||||
<!-- <ElInput v-model="form.des" type="textarea" :rows="3" /> -->
|
||||
<!-- 富文本编辑器 -->
|
||||
<ArtWangEditor class="el-top" v-model="editorHtml" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="上传附件" prop="attachment">
|
||||
<ElUpload
|
||||
v-model:file-list="fileList"
|
||||
class="upload-demo"
|
||||
:action="actionImageUplode"
|
||||
:on-success="handleUploadSuccess"
|
||||
multiple
|
||||
:limit="3">
|
||||
<el-button type="primary">Click to upload</el-button>
|
||||
</ElUpload>
|
||||
<!-- 附件预览区 -->
|
||||
<div class="attachment-preview" v-if="fileList.length">
|
||||
<div class="attachment-list">
|
||||
<div v-for="(file, idx) in fileList" :key="file.name" class="attachment-item">
|
||||
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)">
|
||||
<img :src="file.file_url?file.file_url:file.response.data.file_url" :alt="file.name" class="attachment-thumb" @click="handleImageClick(imageList.findIndex(url => url === file.file_url?file.file_url:file.response.data.file_url))" />
|
||||
</template>
|
||||
<template v-else-if="/\.(mp4|webm|ogg)$/i.test(file.name)">
|
||||
<video :src="file.file_url?file.file_url:file.response.data.file_url" controls class="attachment-thumb" @click.prevent="handleVideoClick(file.file_url?file.file_url:file.response.data.file_url)"></video>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a :href="file.file_url?file.file_url:file.response.data.file_url" target="_blank">{{ file.name }}</a>
|
||||
</template>
|
||||
<div class="attachment-filename">{{ file.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
<el-drawer v-model="isDrawerVisible" :size="isMobile ? '100%' : '480px'" :with-header="false">
|
||||
<div class="chat-container">
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<span class="name">工单记录</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-icon class="icon-close" :size="20" @click="closeChat">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 聊天消息区域 -->
|
||||
<div class="chat-messages" ref="messageContainer">
|
||||
<template v-for="(message, index) in messages" :key="index">
|
||||
<div :class="['message-item', message.isMe ? 'message-right' : 'message-left']">
|
||||
<el-avatar :size="32" :src="message.isMe ? meAvatar : aiAvatar" class="message-avatar" />
|
||||
<div class="message-content">
|
||||
<div class="message-info">
|
||||
<span class="sender-name">{{ message.user_id }}</span>
|
||||
<span class="message-time">{{ message.updated_at }}</span>
|
||||
</div>
|
||||
<div class="message-text">{{ message.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="chat-input">
|
||||
<el-input v-model="messageText" type="textarea" :rows="3" placeholder="输入消息" resize="none"
|
||||
@keyup.enter.prevent="sendMessage" />
|
||||
<ElInput v-model="ticket_id" type="hidden" />
|
||||
<div class="chat-input-actions">
|
||||
<el-button type="primary" @click="sendMessage" v-ripple>发送</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<ElDialog v-model="dialogVisiblestatus" title="工单状态" width="40%" align-center>
|
||||
<ElForm ref="formRef" :model="statusform" label-width="120px">
|
||||
<ElFormItem label="状态" prop="status">
|
||||
<ElSelect v-model="statusform.status">
|
||||
<ElOption v-for="type in statusList" :key="type.value" :value="type.value" :label="type.text" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="描述" prop="msg">
|
||||
<ElInput v-model="statusform.msg" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisiblestatus = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="statusSubmit()">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 图片预览弹窗 -->
|
||||
<ElImageViewer v-if="imageViewerVisible" :url-list="imageList" :initial-index="imageViewerIndex" @close="imageViewerVisible = false" />
|
||||
<!-- 视频预览弹窗 -->
|
||||
<ElDialog v-model="videoDialogVisible" width="60%" top="10vh" :show-close="true" append-to-body>
|
||||
<video v-if="currentVideoUrl" :src="currentVideoUrl" controls autoplay style="width:100%;max-height:70vh;display:block;"></video>
|
||||
</ElDialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { Order } from '@/mock/temp/formData'
|
||||
import { WorkService } from '@/api/workApi'
|
||||
import { ElImageViewer } from 'element-plus'
|
||||
defineOptions({ name: 'Order' })
|
||||
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
|
||||
const loading = ref(false)
|
||||
const editorHtml = ref('') // 编辑器内容
|
||||
const dialogVisible = ref(false)
|
||||
const dialogVisiblestatus = ref(false)
|
||||
const typeList = ref<any[]>([])
|
||||
const formRef = ref<FormInstance>()
|
||||
const { width } = useWindowSize()
|
||||
const isMobile = computed(() => width.value < 500)
|
||||
// 抽屉显示状态
|
||||
const isDrawerVisible = ref(false)
|
||||
import meAvatar from '@/assets/img/avatar/avatar5.webp'
|
||||
import aiAvatar from '@/assets/img/avatar/avatar10.webp'
|
||||
// 消息相关数据
|
||||
const messages = ref<any[]>([])
|
||||
const messageText = ref('')
|
||||
// 滚动到底部
|
||||
const messageContainer = ref<HTMLElement | null>(null)
|
||||
const scrollToBottom = () => {
|
||||
setTimeout(() => {
|
||||
if (messageContainer.value) {
|
||||
messageContainer.value.scrollTop = messageContainer.value.scrollHeight
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
const openChat = (row: any) => {
|
||||
isDrawerVisible.value = true
|
||||
getConversationList(row.id)
|
||||
}
|
||||
|
||||
const closeChat = () => {
|
||||
isDrawerVisible.value = false
|
||||
}
|
||||
//获取状态 下拉框 getStatus
|
||||
const statusList = ref<any>()
|
||||
const getstatusList = async () => {
|
||||
try {
|
||||
// 这里调用获取品牌列表的接口
|
||||
const response = await WorkService.getStatus()
|
||||
statusList.value = response || []
|
||||
} catch (error) { }
|
||||
}
|
||||
//获取工单分类
|
||||
const categorieSelectList = ref<any>()
|
||||
const getcategorieSelectList = async () => {
|
||||
try {
|
||||
// 这里调用获取品牌列表的接口
|
||||
const response = await WorkService.getcategorieselect()
|
||||
categorieSelectList.value = response || []
|
||||
} catch (error) { }
|
||||
}
|
||||
|
||||
|
||||
|
||||
const fileList = ref<any[]>([])
|
||||
const form = reactive<Order>({
|
||||
id: '',
|
||||
categorie_id: '',
|
||||
content: '',
|
||||
attachment: []
|
||||
})
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
const paginationmsg = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
getWorkList()
|
||||
})
|
||||
|
||||
// 获取列表数据
|
||||
const getWorkList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
|
||||
const { data, current, size, total } = await WorkService.getWorkList({
|
||||
current: currentPage,
|
||||
size: pageSize
|
||||
})
|
||||
tableData.value = data.map((item: any, index: number) => ({
|
||||
...item
|
||||
}))
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
//获取工单记录 getconversationList
|
||||
const ticket_id =ref()
|
||||
const getConversationList = async (id:any) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = paginationmsg
|
||||
ticket_id.value = id
|
||||
const { data, current, size, total } = await WorkService.getconversationList({
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
ticket_id: id
|
||||
})
|
||||
messages.value = data.map((item: any, index: number) => ({
|
||||
...item
|
||||
}))
|
||||
Object.assign(paginationmsg, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
// 发送工单记录消息
|
||||
const sendMessage = async () => {
|
||||
const text = messageText.value.trim()
|
||||
const ids = ticket_id.value
|
||||
if (!text) return
|
||||
|
||||
const { message } = await WorkService.conversationAdd({
|
||||
content: text,
|
||||
ticket_id: ids,
|
||||
})
|
||||
ElMessage.success(message)
|
||||
messageText.value = ''
|
||||
getConversationList(ids)
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
|
||||
const dialogType = ref('add')
|
||||
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
getcategorieSelectList()
|
||||
if (type === 'edit' && row) {
|
||||
form.id = row.id
|
||||
form.categorie_id = row.categorie_id
|
||||
form.content = row.content
|
||||
editorHtml.value = row.content
|
||||
// 回显附件
|
||||
fileList.value = (row.attachments || []).map((item: { original_name: any; storage_path_full: any }) => ({
|
||||
name: item.original_name,
|
||||
file_url: item.storage_path_full,
|
||||
status: 'success',
|
||||
response: { data: item }
|
||||
}))
|
||||
// 如果你有 uploadedFiles 也要同步
|
||||
uploadedFiles.value = row.attachments || []
|
||||
} else {
|
||||
// 新增时清空表单和上传文件
|
||||
form.categorie_id = ''
|
||||
form.content = ''
|
||||
editorHtml.value = ''
|
||||
fileList.value = []
|
||||
uploadedFiles.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const statusform = reactive({
|
||||
id: '',
|
||||
status: '',
|
||||
msg: ''
|
||||
})
|
||||
|
||||
const statusEdit = (row: any) => {
|
||||
getstatusList()
|
||||
dialogVisiblestatus.value = true
|
||||
statusform.id = row.id
|
||||
}
|
||||
const deleteRole = () => {
|
||||
ElMessageBox.confirm('确定删除该角色吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(() => {
|
||||
ElMessage.success('删除成功')
|
||||
})
|
||||
}
|
||||
const delCodeTrim = (content: string): string => {
|
||||
return content.replace(/(\s*)<\/code>/g, '</code>')
|
||||
}
|
||||
//添加工单
|
||||
|
||||
const uploadedFiles = ref([])
|
||||
const handleUploadSuccess = (response: any, file: any, fileList: { filter: (arg0: (item: any) => boolean) => { (): any; new(): any; map: { (arg0: (item: any) => { original_name: any; storage_path: any } | null): { (): any; new(): any; filter: { (arg0: BooleanConstructor): never[]; new(): any } }; new(): any } } }) => {
|
||||
// 只处理上传成功的文件
|
||||
uploadedFiles.value = fileList.filter(item => item.status === 'success').map(item => {
|
||||
const data = item.response?.data
|
||||
if (data) {
|
||||
return {
|
||||
id: data.id?data.id:'',
|
||||
original_name: data.original_name,
|
||||
storage_path: data.storage_path?data.storage_path:data.file_name
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
const { categorie_id} = form
|
||||
editorHtml.value = delCodeTrim(editorHtml.value)
|
||||
if(dialogType.value === 'add'){
|
||||
const { message } = await WorkService.WorkAdd({
|
||||
content: editorHtml.value,
|
||||
categorie_id: categorie_id,
|
||||
attachment:uploadedFiles.value
|
||||
})
|
||||
ElMessage.success(message)
|
||||
dialogVisible.value = false
|
||||
getWorkList()
|
||||
}else{
|
||||
const { message } = await WorkService.Workedit({
|
||||
id:form.id,
|
||||
content: editorHtml.value,
|
||||
categorie_id: categorie_id,
|
||||
attachment:uploadedFiles.value
|
||||
})
|
||||
ElMessage.success(message)
|
||||
dialogVisible.value = false
|
||||
getWorkList()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getWorkList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getWorkList()
|
||||
}
|
||||
//修改状态
|
||||
const statusSubmit = async () => {
|
||||
// loading.value = true
|
||||
const { msg, status, id } = statusform
|
||||
const { message } = await WorkService.Workstatus({
|
||||
id,
|
||||
msg,
|
||||
status
|
||||
})
|
||||
ElMessage.success(message)
|
||||
dialogVisiblestatus.value = false
|
||||
getWorkList()
|
||||
}
|
||||
|
||||
// 图片预览相关
|
||||
const imageViewerVisible = ref(false)
|
||||
const imageViewerIndex = ref(0)
|
||||
const imageList = computed(() => fileList.value.filter(file => /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)).map(file => file.file_url?file.file_url:file.response.data.file_url))
|
||||
function handleImageClick(idx: number) {
|
||||
imageViewerIndex.value = idx
|
||||
imageViewerVisible.value = true
|
||||
}
|
||||
// 视频预览相关
|
||||
const videoDialogVisible = ref(false)
|
||||
const currentVideoUrl = ref('')
|
||||
function handleVideoClick(url: string) {
|
||||
currentVideoUrl.value = url
|
||||
videoDialogVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.svg-icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
overflow: hidden;
|
||||
vertical-align: -8px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.header-left {
|
||||
.name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
.icon-close {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 70px);
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
padding: 30px 16px;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px !important;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.message-text {
|
||||
font-size: 14px;
|
||||
color: var(--art-gray-900);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&.message-left {
|
||||
justify-content: flex-start;
|
||||
|
||||
.message-content {
|
||||
align-items: flex-start;
|
||||
|
||||
.message-info {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
background-color: #f8f5ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.message-right {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.message-content {
|
||||
align-items: flex-end;
|
||||
|
||||
.message-info {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
background-color: #e9f3ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 70%;
|
||||
|
||||
.message-info {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
.message-time {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.sender-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.message-text {
|
||||
padding: 10px 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
padding: 16px 16px 0;
|
||||
|
||||
.input-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.chat-input-actions {
|
||||
display: flex;
|
||||
align-items: center; // 修正为单数
|
||||
justify-content: right;
|
||||
margin-top: 12px;
|
||||
|
||||
// 确保发送按钮与输入框对齐
|
||||
el-button {
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.chat-container {
|
||||
.chat-messages {
|
||||
.message-item {
|
||||
&.message-left {
|
||||
.message-text {
|
||||
background-color: #232323 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.message-right {
|
||||
.message-text {
|
||||
background-color: #182331 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
margin-top: 12px;
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
.attachment-item {
|
||||
width: 130px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: #fafbfc;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
||||
padding: 10px 8px 6px 8px;
|
||||
transition: box-shadow 0.2s, transform 0.2s;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.10);
|
||||
transform: translateY(-2px) scale(1.03);
|
||||
}
|
||||
}
|
||||
.attachment-thumb {
|
||||
max-width: 110px;
|
||||
max-height: 90px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #eee;
|
||||
margin-bottom: 6px;
|
||||
object-fit: cover;
|
||||
background: #fff;
|
||||
}
|
||||
.attachment-filename {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput placeholder="名称"></ElInput>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple>搜索</ElButton>
|
||||
<ElButton @click="showDialog('add')" v-ripple>新增</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="ID" prop="id" />
|
||||
<ElTableColumn label="分类名称" prop="name" />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status ? 'primary' : 'info'">
|
||||
{{ scope.row.status ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" />
|
||||
<ElTableColumn fixed="right" label="操作" width="200px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button link @click="showDialog('edit', scope.row)"> 编辑 </el-button>
|
||||
<el-button link @click="deleteRole(scope.row)"> 删除 </el-button>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增' : '编辑'"
|
||||
width="40%"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="160px">
|
||||
<ElFormItem label="分类名称" prop="name">
|
||||
<ElInput v-model="form.name" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { Ordertype} from '@/mock/temp/formData'
|
||||
import { WorkService } from '@/api/workApi'
|
||||
defineOptions({ name: 'Type ' })
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const typeList = ref<any[]>([])
|
||||
const formRef = ref<FormInstance>()
|
||||
const rules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const form = reactive<Ordertype>({
|
||||
name: '',
|
||||
id:''
|
||||
})
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
getWorkList()
|
||||
})
|
||||
|
||||
// 获取列表数据
|
||||
const getWorkList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
|
||||
const { data, current, size, total } = await WorkService.getCategorieList({
|
||||
current: currentPage,
|
||||
size: pageSize
|
||||
})
|
||||
tableData.value = data.map((item: any, index: number) => ({
|
||||
...item
|
||||
}))
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const dialogType = ref('add')
|
||||
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
form.name = row.name
|
||||
form.id = row.id
|
||||
} else {
|
||||
form.name = ''
|
||||
}
|
||||
}
|
||||
const deleteRole = (row?: any) => {
|
||||
ElMessageBox.confirm('确定删除该分类吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
const { message } = await WorkService.Workcategoriedelete({
|
||||
id: row.id,
|
||||
})
|
||||
ElMessage.success(message)
|
||||
getWorkList()
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
if(dialogType.value === 'add'){
|
||||
const { message } = await WorkService.WorkcategorieAdd({
|
||||
name: form.name,
|
||||
})
|
||||
ElMessage.success(message)
|
||||
dialogVisible.value = false
|
||||
getWorkList()
|
||||
}else{
|
||||
const { message } = await WorkService.Workcategorieedit({
|
||||
name: form.name,
|
||||
id:form.id
|
||||
})
|
||||
ElMessage.success(message)
|
||||
dialogVisible.value = false
|
||||
getWorkList()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.svg-icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
overflow: hidden;
|
||||
vertical-align: -8px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user