项目创建

This commit is contained in:
浅念
2025-07-04 17:16:38 +08:00
parent ce47b39fa5
commit 719911918f
372 changed files with 105035 additions and 32 deletions
+47
View File
@@ -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>
+43
View File
@@ -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">&#xe703;</i>
</div>
<div class="btn art-custom-card" @click="goPage(WEB_LINKS.INTRODUCE)">
<span>文档</span>
<i class="iconfont-sys">&#xe703;</i>
</div>
<div class="btn art-custom-card" @click="goPage(WEB_LINKS.GITHUB_HOME)">
<span>Github</span>
<i class="iconfont-sys">&#xe703;</i>
</div>
<div class="btn art-custom-card" @click="goPage(WEB_LINKS.BLOG)">
<span>博客</span>
<i class="iconfont-sys">&#xe703;</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: '&#xe721;',
startVal: 0,
duration: 1000,
num: 9120,
change: '+20%'
},
{
des: '在线访客数',
icon: '&#xe724;',
startVal: 0,
duration: 1000,
num: 182,
change: '+10%'
},
{
des: '点击量',
icon: '&#xe7aa;',
startVal: 0,
duration: 1000,
num: 9520,
change: '-12%'
},
{
des: '新用户',
icon: '&#xe82a;',
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>