Initial commit: One Pipe System

完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sexygoat
2026-01-22 16:35:33 +08:00
commit 222e5bb11a
495 changed files with 145440 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
<template>
<div class="banners">
<h1 class="page-title">基础横幅</h1>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :md="12">
<ArtBasicBanner
title="欢迎回来,管理员!"
subtitle="今日系统访问量增长了23%,运行状态良好。"
buttonText="查看详情"
buttonColor="rgb(var(--art-secondary))"
buttonTextColor="#fff"
@click="handleBannerClick"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12">
<ArtBasicBanner
title="欢迎使用本系统!"
subtitle="这是一个基于Vue3和Element Plus的后台管理系统模板。"
buttonText="开始使用"
buttonColor="rgb(var(--art-success))"
buttonTextColor="#fff"
backgroundColor="#D4F1F7"
titleColor="#333"
subtitleColor="#666"
@click="handleBannerClick"
/>
</ElCol>
</ElRow>
<h1 class="page-title">基础横幅图片</h1>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :md="12">
<ArtBasicBanner
title="欢迎回来,管理员!"
subtitle="今日系统访问量增长了23%,运行状态良好。"
buttonText="查看详情"
buttonColor="#FF3E76"
backgroundColor="#FF80A4"
buttonTextColor="#fff"
:backgroundImage="icon3"
@click="handleBannerClick"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12">
<ArtBasicBanner
title="欢迎使用本系统!"
subtitle="这是一个基于Vue3和Element Plus的后台管理系统模板。"
buttonText="开始使用"
buttonColor="#1385FF"
buttonTextColor="#fff"
backgroundColor="#88A7FF"
:backgroundImage="icon5"
@click="handleBannerClick"
/>
</ElCol>
</ElRow>
<h1 class="page-title">卡片横幅</h1>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :md="12" :lg="6">
<ArtCardBanner
title="系统状态良好"
description="所有服务运行正常,无异常情况。"
buttonText="查看详情"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12" :lg="6">
<ArtCardBanner
:icon="icon2"
title="新消息提醒"
description="您有3条未读消息需要处理。"
buttonText="立即查看"
buttonColor="rgb(var(--art-warning))"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12" :lg="6">
<ArtCardBanner
:icon="icon3"
title="数据分析报告"
description="本周数据分析报告已生成完毕。"
buttonText="下载报告"
buttonColor="rgb(var(--art-error))"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12" :lg="6">
<ArtCardBanner
:icon="icon4"
title="系统更新提示"
description="新版本V2.1.0已发布,建议及时更新。"
buttonText="更新"
buttonColor="rgb(var(--art-primary))"
:showCancel="true"
cancelButtonText="取消"
cancelButtonColor="#eee"
cancelButtonTextColor="#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')
}
</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>

View File

@@ -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>

View File

@@ -0,0 +1,442 @@
<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"
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"
/>
</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"
: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"
:data="[50, 40]"
:height="9.5"
currentYear="2022"
previousYear="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"
:data="[70, 30]"
:height="11"
currentYear="2022"
previousYear="2021"
/>
</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"
/>
</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: '&#xe812;',
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: '&#xe724;',
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: '&#xe70e;',
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: '&#xe82d;',
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: '&#xe812;',
iconColor: 'rgb(var(--art-success))',
iconBgColor: 'rgb(var(--art-bg-success))',
iconSize: 20
},
{
id: 2,
title: '项目进度',
percentage: 65,
color: 'rgb(var(--art-primary))',
icon: '&#xe724;',
iconColor: 'rgb(var(--art-primary))',
iconBgColor: 'rgb(var(--art-bg-primary))',
iconSize: 20
},
{
id: 3,
title: '学习进度',
percentage: 45,
color: 'rgb(var(--art-error))',
icon: '&#xe724;',
iconColor: 'rgb(var(--art-error))',
iconBgColor: 'rgb(var(--art-bg-error))',
iconSize: 20
},
{
id: 4,
title: '任务进度',
percentage: 90,
color: 'rgb(var(--art-secondary))',
icon: '&#xe724;',
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: '&#xe6f2;'
},
{
title: '归档数据',
status: '进行中',
time: '10分钟',
class: 'bg-secondary',
icon: '&#xe806;'
},
{
title: '客户会议',
status: '待处理',
time: '15分钟',
class: 'bg-warning',
icon: '&#xe6fb;'
},
{
title: '筛选任务团队',
status: '进行中',
time: '20分钟',
class: 'bg-danger',
icon: '&#xe813;'
},
{
title: '发送信封给小王',
status: '已完成',
time: '20分钟',
class: 'bg-success',
icon: '&#xe70c;'
}
]
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 = () => {}
</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>

View File

@@ -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>

View File

@@ -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">&#xe776;</i>
</div>
<div class="btn">
<i class="iconfont-sys">&#xe778;</i>
</div>
<div class="btn">
<i class="iconfont-sys">&#xe6df;</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">&#xe634;</i>
<i class="iconfont-sys">&#xe809;</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>

View File

@@ -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>

View File

@@ -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>