completepage: 🍻 收入和支出页面完成

This commit is contained in:
Bunny 2024-11-23 23:01:37 +08:00
parent 42662ccb26
commit 998906ba27
56 changed files with 1405 additions and 180 deletions

View File

@ -20,7 +20,7 @@ VITE_BASE_API_RETRY=5
VITE_BASE_API_RETRY_DELAY=3000
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN=false
VITE_CDN=true
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认

View File

@ -42,7 +42,7 @@ export const buildEnvironment = () => {
manualChunks: id => {
// 如果是包含在包中则打包成 vendor
if (id.includes('node_modules')) {
return `vendor`;
return id.toString().split('node_modules/')[1].split('/')[1].toString();
}
},
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@ -8,6 +8,11 @@ export const fetchGetUserBillList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `bill/noManage/getUserBillList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/** 账单信息---账单收入和支出 */
export const fetchGetExpendOrIncome = (data: any) => {
return http.request<BaseResult<object>>('get', 'bill/noManage/getExpendOrIncome', { params: data });
};
/** 账单信息---添加账单信息 */
export const fetchAddUserBill = (data: any) => {
return http.request<BaseResult<object>>('post', 'bill/noManage/addUserBill', { data });

View File

@ -0,0 +1,166 @@
<script lang="tsx" setup>
import { useRenderIcon } from '@/components/CommonIcon/src/hooks';
import PureTable from '@pureadmin/table';
import { $t } from '@/plugins/i18n';
import { ElTag, ElText } from 'element-plus';
import { onMounted, reactive, ref, watch } from 'vue';
import { fetchGetUserBillList } from '@/api/v1/financial/user/billUser';
import Empty from './empty.svg?component';
const props = defineProps({
financialType: { type: Number as PropType<any>, default: undefined },
startDate: { type: String as PropType<any>, default: undefined },
endDate: { type: String as PropType<any>, default: undefined },
});
const columns: TableColumnList = [
{ type: 'index', index: (index: number) => index + 1, label: '序号', width: 60 },
//
{
label: $t('amount'),
prop: 'amount',
sortable: true,
formatter({ type, amount }) {
return type === -1 ? (
<ElText size={'large'} type={'danger'} style={{ fontWeight: 800 }}>
- {amount}
</ElText>
) : (
<ElText size={'large'} type={'success'} style={{ fontWeight: 800 }}>
+ {amount}
</ElText>
);
},
width: 200,
},
//
{
label: $t('categoryName'),
prop: 'categoryName',
width: 150,
formatter({ categoryName }) {
return <ElTag effect={'plain'}>{categoryName}</ElTag>;
},
},
//
{ label: $t('description'), prop: 'description' },
//
{
label: $t('transactionDate'),
prop: 'transactionDate',
width: 190,
sortable: true,
formatter({ transactionDate }) {
return (
<ElText type={'success'} style={{ fontWeight: 800 }}>
{transactionDate}
</ElText>
);
},
},
];
const form = reactive({
// 1 - -1 -
type: props.financialType,
//
startDate: props.startDate,
//
endDate: props.endDate,
pagination: {
pageSize: 10,
currentPage: 1,
layout: 'prev, pager, next',
total: 1,
align: 'center',
hideOnSinglePage: false,
},
});
const dataList = ref([]);
const loading = ref(false);
/* 初始化数据 */
async function onSearch() {
loading.value = true;
const result = await fetchGetUserBillList({ ...form, ...form.pagination });
dataList.value = result.data.list;
form.pagination.currentPage = result.data.pageNo;
form.pagination.pageSize = result.data.pageSize;
form.pagination.total = result.data.total;
loading.value = false;
}
/* 修改分页 */
async function onCurrentChange(page: number) {
form.pagination.currentPage = page;
await onSearch();
}
onMounted(() => {
watch(
() => props,
async () => {
form.startDate = props.startDate;
form.endDate = props.endDate;
await onSearch();
},
{ immediate: true, deep: true },
);
});
</script>
<template>
<pure-table
:columns="columns"
:data="dataList"
:loading="loading"
:loading-config="{ background: 'transparent' }"
:pagination="form.pagination"
alignWhole="center"
row-key="id"
showOverflowTooltip
@page-current-change="onCurrentChange"
>
<template #empty>
<el-empty :image-size="60" description="暂无数据" style="height: 430px">
<template #image>
<Empty />
</template>
</el-empty>
</template>
<template #operation="{ row }">
<el-button :icon="useRenderIcon('ri:search-line')" :title="`查看序号为${row.id}的详情`" circle plain size="small" />
</template>
</pure-table>
</template>
<style lang="scss">
.pure-table-filter {
.el-table-filter__list {
min-width: 80px;
padding: 0;
li {
line-height: 28px;
}
}
}
</style>
<style lang="scss" scoped>
:deep(.el-table) {
--el-table-border: none;
--el-table-border-color: transparent;
.el-empty__description {
margin: 0;
}
.el-scrollbar__bar {
display: none;
}
}
</style>

View File

@ -0,0 +1,104 @@
<script lang="ts" setup>
import { useDark, useECharts } from '@pureadmin/utils';
import { computed, nextTick, type PropType, ref, watch } from 'vue';
import { $t } from '@/plugins/i18n';
const props = defineProps({
analyseData: {
type: Array as PropType<Array<number>>,
default: () => [],
},
analyseXAxis: {
type: Array as PropType<Array<number>>,
default: () => [],
},
color: {
type: Array as PropType<Array<string>>,
default: () => [],
},
});
const { isDark } = useDark();
const theme = computed(() => (isDark.value ? 'dark' : 'light'));
const chartRef = ref();
const { setOptions } = useECharts(chartRef, {
theme,
});
watch(
() => props,
async () => {
await nextTick(); // DOM
setOptions({
container: '.bar-card',
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'none',
},
},
grid: {
top: '30px',
bottom: '30px',
left: '50px',
right: 0,
},
xAxis: [
{
type: 'category',
data: props.analyseXAxis,
axisLabel: {
fontSize: '0.875rem',
},
axisPointer: {
type: 'shadow',
},
},
],
yAxis: [
{
type: 'value',
axisLabel: {
padding: 0,
fontSize: '0.875rem',
},
splitLine: {
show: true, // 线
},
position: 'left',
name: $t('unitMoney'),
},
],
series: [
{
type: 'line',
itemStyle: {
color: props.color[0],
borderRadius: [10, 10, 0, 0],
},
data: props.analyseData,
},
{
type: 'bar',
barWidth: 15,
itemStyle: {
color: props.color[1],
borderRadius: [10, 10, 0, 0],
},
data: props.analyseData,
},
],
});
},
{
deep: true,
immediate: true,
},
);
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 365px" />
</template>

View File

@ -0,0 +1,64 @@
<script lang="ts" setup>
import { useDark, useECharts } from '@pureadmin/utils';
import { computed, nextTick, type PropType, ref, watch } from 'vue';
const props = defineProps({
categoryData: {
type: Array as PropType<Array<number>>,
default: () => [],
},
});
const { isDark } = useDark();
const theme = computed(() => (isDark.value ? 'dark' : 'light'));
const chartRef = ref();
const { setOptions } = useECharts(chartRef, {
theme,
});
watch(
() => props,
async () => {
await nextTick(); // DOM
setOptions({
tooltip: {
trigger: 'item',
},
legend: {
textStyle: {
color: '#606266',
fontSize: '0.875rem',
},
bottom: 0,
},
series: [
{
type: 'pie',
radius: [16, 130],
roseType: 'area',
center: ['50%', '43%'],
labelLine: { length: 3, length2: 6, smooth: true },
itemStyle: {
borderRadius: [5, 5, 5, 5],
},
label: {
position: 'outer',
alignTo: 'edge',
edgeDistance: 10,
},
data: props.categoryData,
},
],
});
},
{
deep: true,
immediate: true,
},
);
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 373px" />
</template>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" class="empty-icon" viewBox="0 0 1024 1024"><path d="M855.6 427.2H168.5c-12.7 0-24.4 6.9-30.6 18L4.4 684.7C1.5 689.9 0 695.8 0 701.8v287.1c0 19.4 15.7 35.1 35.1 35.1H989c19.4 0 35.1-15.7 35.1-35.1V701.8c0-6-1.5-11.8-4.4-17.1L886.2 445.2c-6.2-11.1-17.9-18-30.6-18M673.4 695.6c-16.5 0-30.8 11.5-34.3 27.7-12.7 58.5-64.8 102.3-127.2 102.3s-114.5-43.8-127.2-102.3c-3.5-16.1-17.8-27.7-34.3-27.7H119c-26.4 0-43.3-28-31.1-51.4l81.7-155.8c6.1-11.6 18-18.8 31.1-18.8h622.4c13 0 25 7.2 31.1 18.8l81.7 155.8c12.2 23.4-4.7 51.4-31.1 51.4zm146.5-486.1c-1-1.8-2.1-3.7-3.2-5.5-9.8-16.6-31.1-22.2-47.8-12.6L648.5 261c-17 9.8-22.7 31.6-12.6 48.4.9 1.4 1.7 2.9 2.5 4.4 9.5 17 31.2 22.8 48 13L807 257.3c16.7-9.7 22.4-31 12.9-47.8m-444.5 51.6L255 191.6c-16.7-9.6-38-4-47.8 12.6-1.1 1.8-2.1 3.6-3.2 5.5-9.5 16.8-3.8 38.1 12.9 47.8L337.3 327c16.9 9.7 38.6 4 48-13.1.8-1.5 1.7-2.9 2.5-4.4 10.2-16.8 4.5-38.6-12.4-48.4M512 239.3h2.5c19.5.3 35.5-15.5 35.5-35.1v-139c0-19.3-15.6-34.9-34.8-35.1h-6.4C489.6 30.3 474 46 474 65.2v139c0 19.5 15.9 35.4 35.5 35.1z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,169 @@
<script lang="ts" setup>
import { randomGradient, useDark } from '@pureadmin/utils';
import { markRaw, type PropType, ref } from 'vue';
import { useRenderFlicker } from '@/components/Flicker';
import ReCol from '@/components/MyCol';
import CharLine from '@/components/Analyse/char-line.vue';
import CharPie from '@/components/Analyse/char-pie.vue';
import { $t } from '@/plugins/i18n';
import { currentMouth, currentWeek, currentYear } from '@/enums/dateEnums';
import AnalyseTable from '@/components/Analyse/analyse-table.vue';
interface Title {
analyse: string;
category: string;
table: string;
top: string;
}
const props = defineProps({
title: { type: Object as PropType<Title> },
financialType: { type: Number as PropType<any> },
color: {
type: Array as PropType<Array<string>>,
},
date: {
type: Array as PropType<Array<number>>,
default: () => [],
},
analyseXAxis: {
type: Array as PropType<Array<number>>,
default: () => [],
},
analyseData: {
type: Array as PropType<Array<number>>,
default: () => [],
},
categoryData: {
type: Array as PropType<Array<number>>,
default: () => [],
},
topData: {
type: Array as PropType<Array<number>>,
default: () => [],
},
});
const emits = defineEmits(['emitDateRange']);
const { isDark } = useDark();
//
const dateRange = ref(props.date);
//
const shortcuts = [
{ text: $t('thisWeek'), value: currentWeek },
{ text: $t('thisMonth'), value: currentMouth },
{ text: $t('thisYear'), value: currentYear },
];
/** 选择查询日期 */
const onChangeDateRange = () => {
const value = dateRange.value;
emits('emitDateRange', value);
};
</script>
<template>
<div>
<el-row :gutter="24" justify="space-around">
<re-col v-motion :enter="{ opacity: 1, y: 0, transition: { delay: 400 } }" :initial="{ opacity: 0, y: 100 }" :value="18" :xs="24" class="mb-[18px]">
<el-card class="bar-card" shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">{{ title.analyse }}</span>
<span>
<el-date-picker
v-model="dateRange"
:end-placeholder="$t('endDate')"
:shortcuts="shortcuts"
:start-placeholder="$t('startDate')"
class="!w-[230px]"
clearable
type="daterange"
value-format="YYYY-MM-DD"
@change="onChangeDateRange"
/>
</span>
</div>
<div class="flex justify-between items-start mt-3">
<char-line :analyse-data="analyseData" :analyse-x-axis="analyseXAxis" :color="color" />
</div>
</el-card>
</re-col>
<re-col v-motion :enter="{ opacity: 1, y: 0, transition: { delay: 400 } }" :initial="{ opacity: 0, y: 100 }" :value="6" :xs="24" class="mb-[18px]">
<el-card class="bar-card" shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">{{ title.category }}</span>
</div>
<div class="flex justify-between items-start mt-3">
<char-pie :category-data="categoryData" />
</div>
</el-card>
</re-col>
<re-col v-motion :enter="{ opacity: 1, y: 0, transition: { delay: 560 } }" :initial="{ opacity: 0, y: 100 }" :value="18" :xs="24" class="mb-[18px]">
<el-card class="h-[580px]" shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">{{ title.table }}</span>
</div>
<analyse-table :endDate="dateRange[1]" :financialType="financialType" :startDate="dateRange[0]" class="mt-3" />
</el-card>
</re-col>
<re-col v-motion :enter="{ opacity: 1, y: 0, transition: { delay: 560 } }" :initial="{ opacity: 0, y: 100 }" :value="6" :xs="24" class="mb-[18px]">
<el-card shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">{{ title.top }}</span>
</div>
<el-scrollbar class="mt-3 h-[505px]" max-height="505">
<el-timeline>
<el-timeline-item
v-for="(item, index) in topData"
:key="index"
:icon="markRaw(useRenderFlicker({ background: randomGradient({ randomizeHue: true }) }))"
:timestamp="item.date"
center
placement="top"
>
<p class="text-text_color_regular text-sm">
{{ `${item.name} - ${item.amount}` }}
</p>
</el-timeline-item>
</el-timeline>
</el-scrollbar>
</el-card>
</re-col>
</el-row>
</div>
</template>
<style lang="scss" scoped>
:deep(.el-card) {
--el-card-border-color: none;
/* 解决概率进度条宽度 */
.el-progress--line {
width: 85%;
}
/* 解决概率进度条字体大小 */
.el-progress-bar__innerText {
font-size: 15px;
}
/* 隐藏 el-scrollbar 滚动条 */
.el-scrollbar__bar {
display: none;
}
/* el-timeline 每一项上下、左右边距 */
.el-timeline-item {
margin: 0 6px;
}
}
.main-content {
margin: 20px 20px 0 !important;
}
</style>

View File

@ -0,0 +1,39 @@
.point {
width: var(--point-width);
height: var(--point-height);
background: var(--point-background);
position: relative;
border-radius: var(--point-border-radius);
}
.point-flicker:after {
background: var(--point-background);
}
.point-flicker:before,
.point-flicker:after {
content: "";
width: 100%;
height: 100%;
top: 0;
left: 0;
position: absolute;
border-radius: var(--point-border-radius);
animation: flicker 1.2s ease-out infinite;
}
@keyframes flicker {
0% {
transform: scale(0.5);
opacity: 1;
}
30% {
opacity: 1;
}
100% {
transform: scale(var(--point-scale));
opacity: 0;
}
}

View File

@ -0,0 +1,44 @@
import "./index.css";
import { type Component, h, defineComponent } from "vue";
export interface attrsType {
width?: string;
height?: string;
borderRadius?: number | string;
background?: string;
scale?: number | string;
}
/**
*
* @param width string
* @param height string
* @param borderRadius number | string 050%
* @param background string
* @param scale number | string 2
* @returns Component
*/
export function useRenderFlicker(attrs?: attrsType): Component {
return defineComponent({
name: "ReFlicker",
render() {
return h(
"div",
{
class: "point point-flicker",
style: {
"--point-width": attrs?.width ?? "12px",
"--point-height": attrs?.height ?? "12px",
"--point-background":
attrs?.background ?? "var(--el-color-primary)",
"--point-border-radius": attrs?.borderRadius ?? "50%",
"--point-scale": attrs?.scale ?? "2"
}
},
{
default: () => []
}
);
}
});
}

6
src/enums/dateEnums.ts Normal file
View File

@ -0,0 +1,6 @@
import dayjs from 'dayjs';
export const currentWeek = [dayjs().startOf('week').add(1, 'day'), dayjs().endOf('week').add(1, 'day')];
export const currentMouth = [dayjs().startOf('month'), dayjs().endOf('month').add(1, 'day')];
export const currentYear = [dayjs().startOf('year'), dayjs().endOf('year')];
export const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];

View File

@ -8,8 +8,7 @@ const Copyright = getConfig('Copyright');
<template>
<footer class="layout-footer text-[rgba(0,0,0,0.6)] dark:text-[rgba(220,220,242,0.8)]">
{{ Copyright }}
<a class="hover:text-primary" href="https://github.com/pure-admin" target="_blank"> &nbsp;{{ TITLE }} </a>
<a href="https://beian.miit.gov.cn/" target="_blank">&nbsp;&nbsp;苏ICP备2023044078号-2</a>
<a class="hover:text-primary" href="/" target="_blank"> &nbsp;{{ TITLE }} </a>
</footer>
</template>

View File

@ -10,7 +10,7 @@ export default [
rank: 1,
},
children: [
// 账单查询
// 账单概览
{
path: '/financial-user/bill',
name: 'BillUser',
@ -20,7 +20,28 @@ export default [
title: 'billManagement',
},
},
// 用户分类
// 支出分析
{
path: '/financial-user/expend',
name: 'ExpendUser',
component: () => import('@/views/financial-user/account-bill/expend/index.vue'),
meta: {
icon: 'icon-park-outline:expenses',
title: 'expendAnalyse',
},
},
// 收入分析
{
path: '/financial-user/income',
name: 'IncomeUser',
component: () => import('@/views/financial-user/account-bill/income/index.vue'),
meta: {
icon: 'icon-park-outline:income',
title: 'incomeAnalyse',
},
},
// 账单分类管理
{
path: '/financial-user/category',
name: 'CategoryUser',

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
import { fetchAddBill, fetchDeleteBill, fetchGetBillList, fetchUpdateBill } from '@/api/v1/financial/bill';
import { fetchAddBill, fetchDeleteBill, fetchGetBillList, fetchUpdateBill } from '@/api/v1/financial/admin/bill';
import { pageSizes } from '@/enums/baseConstant';
import { storeMessage } from '@/utils/message';
import { storePagination } from '@/store/useStorePagination';

View File

@ -1,5 +1,10 @@
import { defineStore } from 'pinia';
import { fetchAddBudgetCategory, fetchDeleteBudgetCategory, fetchGetBudgetCategoryList, fetchUpdateBudgetCategory } from '@/api/v1/financial/budgetCategory';
import {
fetchAddBudgetCategory,
fetchDeleteBudgetCategory,
fetchGetBudgetCategoryList,
fetchUpdateBudgetCategory,
} from '@/api/v1/financial/admin/budgetCategory';
import { pageSizes } from '@/enums/baseConstant';
import { storeMessage } from '@/utils/message';
import { storePagination } from '@/store/useStorePagination';

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
import { fetchAddCategory, fetchDeleteCategory, fetchGetCategoryAllList, fetchGetCategoryList, fetchUpdateCategory } from '@/api/v1/financial/category';
import { fetchAddCategory, fetchDeleteCategory, fetchGetCategoryAllList, fetchGetCategoryList, fetchUpdateCategory } from '@/api/v1/financial/admin/category';
import { pageSizes } from '@/enums/baseConstant';
import { storeMessage } from '@/utils/message';
import { storePagination } from '@/store/useStorePagination';

View File

@ -4,7 +4,7 @@ import {
fetchDeleteDebtRepaymentPlan,
fetchGetDebtRepaymentPlanList,
fetchUpdateDebtRepaymentPlan,
} from '@/api/v1/financial/debtRepaymentPlan';
} from '@/api/v1/financial/admin/debtRepaymentPlan';
import { pageSizes } from '@/enums/baseConstant';
import { storeMessage } from '@/utils/message';
import { storePagination } from '@/store/useStorePagination';

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
import { fetchAddDebtTracking, fetchDeleteDebtTracking, fetchGetDebtTrackingList, fetchUpdateDebtTracking } from '@/api/v1/financial/debtTracking';
import { fetchAddDebtTracking, fetchDeleteDebtTracking, fetchGetDebtTrackingList, fetchUpdateDebtTracking } from '@/api/v1/financial/admin/debtTracking';
import { pageSizes } from '@/enums/baseConstant';
import { storeMessage } from '@/utils/message';
import { storePagination } from '@/store/useStorePagination';

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
import { fetchAddSavingGoal, fetchDeleteSavingGoal, fetchGetSavingGoalList, fetchUpdateSavingGoal } from '@/api/v1/financial/savingGoal';
import { fetchAddSavingGoal, fetchDeleteSavingGoal, fetchGetSavingGoalList, fetchUpdateSavingGoal } from '@/api/v1/financial/admin/savingGoal';
import { pageSizes } from '@/enums/baseConstant';
import { storeMessage } from '@/utils/message';
import { storePagination } from '@/store/useStorePagination';

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
import { fetchAddUserBill, fetchDeleteUserBill, fetchGetUserBillList, fetchUpdateUserBill } from '@/api/v1/financialUser/billUser';
import { fetchAddUserBill, fetchDeleteUserBill, fetchGetUserBillList, fetchUpdateUserBill } from '@/api/v1/financial/user/billUser';
import { pageSizes } from '@/enums/baseConstant';
import { storeMessage } from '@/utils/message';
import { storePagination } from '@/store/useStorePagination';
@ -13,6 +13,8 @@ export const useBillUserStore = defineStore('billUserStore', {
return {
// 账单信息列表
datalist: [],
// 收入和支出
expendWithIncomeList: [],
// 查询表单
form: {
// 类型1 - 收入,-1 - 支出

View File

@ -7,7 +7,7 @@ import {
fetchDeleteUserBudgetCategory,
fetchGetUserBudgetCategoryList,
fetchUpdateUserBudgetCategory,
} from '@/api/v1/financialUser/budgetCategoryUser';
} from '@/api/v1/financial/user/budgetCategoryUser';
import { getDefaultDateRange } from '@/utils/date';
/**

View File

@ -5,7 +5,7 @@ import {
fetchGetCategoryUserAllList,
fetchGetCategoryUserList,
fetchUpdateCategoryUser,
} from '@/api/v1/financialUser/categoryUser';
} from '@/api/v1/financial/user/categoryUser';
import { pageSizes } from '@/enums/baseConstant';
import { storeMessage } from '@/utils/message';
import { storePagination } from '@/store/useStorePagination';

View File

@ -7,7 +7,7 @@ import {
fetchDeleteUserDebtRepaymentPlan,
fetchGetUserDebtRepaymentPlanList,
fetchUpdateUserDebtRepaymentPlan,
} from '@/api/v1/financialUser/debtRepaymentPlanUser';
} from '@/api/v1/financial/user/debtRepaymentPlanUser';
/**
* Store

View File

@ -7,7 +7,7 @@ import {
fetchDeleteUserDebtTracking,
fetchGetUserDebtTrackingList,
fetchUpdateUserDebtTracking,
} from '@/api/v1/financialUser/debtTrackingUser';
} from '@/api/v1/financial/user/debtTrackingUser';
/**
* Store

View File

@ -7,7 +7,7 @@ import {
fetchDeleteUserSavingGoal,
fetchGetUserSavingGoalList,
fetchUpdateUserSavingGoal,
} from '@/api/v1/financialUser/savingGoalUser';
} from '@/api/v1/financial/user/savingGoalUser';
import { getDefaultDateRange } from '@/utils/date';
/**

View File

@ -4,7 +4,7 @@ import { columns } from '@/views/configuration/emailTemplate/utils/columns';
import PureTableBar from '@/components/TableBar/src/bar';
import AddFill from '@iconify-icons/ri/add-circle-line';
import PureTable from '@pureadmin/table';
import { onAdd, onDelete, onDeleteBatch, onSearch, onUpdate, selectRows } from '@/views/configuration/emailTemplate/utils/hooks';
import { onAdd, onDelete, onDeleteBatch, onSearch, onUpdate, selectRows, viewTemplate } from '@/views/configuration/emailTemplate/utils/hooks';
import Delete from '@iconify-icons/ep/delete';
import EditPen from '@iconify-icons/ep/edit-pen';
import Refresh from '@iconify-icons/ep/refresh';
@ -14,6 +14,7 @@ import { useEmailTemplateStore } from '@/store/configuration/emailTemplate.ts';
import { useRenderIcon } from '@/components/CommonIcon/src/hooks';
import { auth } from '@/views/configuration/emailTemplate/utils/auth';
import { hasAuth } from '@/router/utils';
import View from '@iconify-icons/ep/view';
const tableRef = ref();
const formRef = ref();
@ -122,7 +123,12 @@ onMounted(() => {
</template>
<template #operation="{ row }">
<el-button v-if="hasAuth(auth.update)" :icon="useRenderIcon(EditPen)" :size="size" class="reset-margin" link type="primary" @click="onUpdate(row)"> {{ $t('modify') }} </el-button>
<el-button :icon="useRenderIcon(View)" :size="size" class="reset-margin" link type="primary" @click="viewTemplate(row.body)">
{{ $t('view') }}
</el-button>
<el-button v-if="hasAuth(auth.update)" :icon="useRenderIcon(EditPen)" :size="size" class="reset-margin" link type="primary" @click="onUpdate(row)">
{{ $t('modify') }}
</el-button>
<el-popconfirm v-if="hasAuth(auth.deleted)" :title="`${$t('delete')} ${row.templateName}?`" @confirm="onDelete(row)">
<template #reference>
<el-button :icon="useRenderIcon(Delete)" :size="size" class="reset-margin" link type="primary">

View File

@ -133,3 +133,14 @@ export const onDeleteBatch = async () => {
},
});
};
/** 查看模板 */
export const viewTemplate = (template: string) => {
addDialog({
title: $t('view'),
draggable: true,
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => <div v-html={template} />,
});
};

View File

@ -0,0 +1,88 @@
<script lang="ts" setup>
import Analyse from '@/components/Analyse/index.vue';
import { onMounted, reactive, ref } from 'vue';
import { dayjs } from '@/views/welcome/utils/utils';
import { currentMouth, days } from '@/enums/dateEnums';
import { fetchGetExpendOrIncome } from '@/api/v1/financial/user/billUser';
import { $t } from '@/plugins/i18n';
const title = {
analyse: $t('expenditureAnalysis'),
category: $t('paymentLedger'),
table: $t('dataStatistics'),
top: $t('rankingList'),
};
//
const analyseXAxis = ref([]);
const analyseData = ref([]);
//
const categoryList = ref([]);
//
const topList = ref([]);
const form = reactive({
type: -1,
startDate: dayjs(currentMouth[0]).format('YYYY-MM-DD'),
endDate: dayjs(currentMouth[1]).format('YYYY-MM-DD'),
});
/** 查询账单支出 */
const onSearch = async () => {
//
form.startDate = dayjs(form.startDate).format('YYYY-MM-DD');
form.endDate = dayjs(form.endDate).format('YYYY-MM-DD');
//
const result = await fetchGetExpendOrIncome(form);
if (result.code !== 200) return;
//
analyseData.value = [];
analyseXAxis.value = [];
result.data.list.forEach(item => {
const transactionDate = item.transactionDate;
const date = `${dayjs(transactionDate).format('MM-DD')} ${days[dayjs(transactionDate).day()]}`;
analyseData.value.push(item.amount);
analyseXAxis.value.push(date);
});
//
categoryList.value = result.data.categoryAmounts.map(categoryAmount => ({
value: categoryAmount.amount,
name: categoryAmount.categoryName,
}));
//
topList.value = result.data.list.map(item => {
const transactionDate = item.transactionDate;
const date = `${dayjs(transactionDate).format('YYYY-MM-DD')} ${days[dayjs(transactionDate).day()]}`;
return { date, amount: item.amount, name: item.categoryName };
});
};
/** 选择查询日期 */
const onChangeDateRange = async dateRange => {
form.startDate = dateRange[0];
form.endDate = dateRange[1];
await onSearch();
};
onMounted(() => {
onSearch();
});
</script>
<template>
<Analyse
:analyse-data="analyseData"
:analyseXAxis="analyseXAxis"
:category-data="categoryList"
:color="['#41b6ff', '#F56C6C']"
:date="[form.startDate, form.endDate]"
:financialType="form.type"
:title="title"
:top-data="topList"
@emit-date-range="onChangeDateRange"
/>
</template>

View File

@ -0,0 +1,89 @@
<script lang="ts" setup>
import Analyse from '@/components/Analyse/index.vue';
import { onMounted, reactive, ref } from 'vue';
import { dayjs } from '@/views/welcome/utils/utils';
import { currentMouth, days } from '@/enums/dateEnums';
import { fetchGetExpendOrIncome } from '@/api/v1/financial/user/billUser';
import { $t } from '@/plugins/i18n';
const title = {
analyse: $t('revenueAnalysis'),
category: $t('incomeBrackets'),
table: $t('dataStatistics'),
top: $t('rankingList'),
};
//
const analyseXAxis = ref([]);
const analyseData = ref([]);
//
const categoryList = ref([]);
//
const topList = ref([]);
const form = reactive({
type: 1,
startDate: dayjs(currentMouth[0]).format('YYYY-MM-DD'),
endDate: dayjs(currentMouth[1]).format('YYYY-MM-DD'),
});
/** 查询账单收入 */
const onSearch = async () => {
//
form.startDate = dayjs(form.startDate).format('YYYY-MM-DD');
form.endDate = dayjs(form.endDate).format('YYYY-MM-DD');
//
const result = await fetchGetExpendOrIncome(form);
if (result.code !== 200) return;
//
analyseData.value = [];
analyseXAxis.value = [];
result.data.list.forEach(item => {
const transactionDate = item.transactionDate;
const date = `${dayjs(transactionDate).format('MM-DD')} ${days[dayjs(transactionDate).day()]}`;
analyseData.value.push(item.amount);
analyseXAxis.value.push(date);
});
//
categoryList.value = result.data.categoryAmounts.map(categoryAmount => ({
value: categoryAmount.amount,
name: categoryAmount.categoryName,
}));
//
topList.value = result.data.list.map(item => {
const transactionDate = item.transactionDate;
const date = `${dayjs(transactionDate).format('YYYY-MM-DD')} ${days[dayjs(transactionDate).day()]}`;
return { date, amount: item.amount, name: item.categoryName };
});
};
/** 选择查询日期 */
const onChangeDateRange = async dateRange => {
form.startDate = dateRange[0];
form.endDate = dateRange[1];
await onSearch();
};
onMounted(() => {
onSearch();
});
</script>
<template>
<Analyse
:analyse-data="analyseData"
:analyseXAxis="analyseXAxis"
:category-data="categoryList"
:color="['#41b6ff', '#67C23A']"
:date="[form.startDate, form.endDate]"
:financialType="form.type"
:title="title"
:top-data="topList"
@emit-date-range="onChangeDateRange"
/>
</template>

View File

@ -6,7 +6,7 @@ import { FormProps } from '@/views/financial-user/budget-saving/budget-category/
import { $t } from '@/plugins/i18n';
import { useAdminUserStore } from '@/store/system/adminUser';
import { budget } from '@/enums/bill/budget';
import { fetchGetAllUserParentList } from '@/api/v1/financialUser/budgetCategoryUser';
import { fetchGetAllUserParentList } from '@/api/v1/financial/user/budgetCategoryUser';
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({

View File

@ -6,7 +6,7 @@ import { FormProps } from '@/views/financial/budget-saving/budget-category/utils
import { $t } from '@/plugins/i18n';
import LoadingSvg from '@/assets/svg/loading.svg';
import { useAdminUserStore } from '@/store/system/adminUser';
import { fetchGetAllParentList } from '@/api/v1/financial/budgetCategory';
import { fetchGetAllParentList } from '@/api/v1/financial/admin/budgetCategory';
import { budget } from '@/enums/bill/budget';
const props = withDefaults(defineProps<FormProps>(), {

View File

@ -14,7 +14,6 @@ const props = defineProps({
});
const { isDark } = useDark();
const theme = computed(() => (isDark.value ? 'dark' : 'light'));
const chartRef = ref();

View File

@ -0,0 +1,108 @@
<script setup lang="ts">
import { useDark, useECharts } from "@pureadmin/utils";
import { type PropType, ref, computed, watch, nextTick } from "vue";
const props = defineProps({
requireData: {
type: Array as PropType<Array<number>>,
default: () => []
},
questionData: {
type: Array as PropType<Array<number>>,
default: () => []
}
});
const { isDark } = useDark();
const theme = computed(() => (isDark.value ? "dark" : "light"));
const chartRef = ref();
const { setOptions } = useECharts(chartRef, {
theme
});
watch(
() => props,
async () => {
await nextTick(); // DOM
setOptions({
container: ".bar-card",
color: ["#41b6ff", "#e85f33"],
tooltip: {
trigger: "axis",
axisPointer: {
type: "none"
}
},
grid: {
top: "20px",
left: "50px",
right: 0
},
legend: {
data: ["需求人数", "提问数量"],
textStyle: {
color: "#606266",
fontSize: "0.875rem"
},
bottom: 0
},
xAxis: [
{
type: "category",
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
axisLabel: {
fontSize: "0.875rem"
},
axisPointer: {
type: "shadow"
}
}
],
yAxis: [
{
type: "value",
axisLabel: {
fontSize: "0.875rem"
},
splitLine: {
show: false // 线
}
// name: ": "
}
],
series: [
{
name: "需求人数",
type: "bar",
barWidth: 10,
itemStyle: {
color: "#41b6ff",
borderRadius: [10, 10, 0, 0]
},
data: props.requireData
},
{
name: "提问数量",
type: "bar",
barWidth: 10,
itemStyle: {
color: "#e86033ce",
borderRadius: [10, 10, 0, 0]
},
data: props.questionData
}
]
});
},
{
deep: true,
immediate: true
}
);
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 365px" />
</template>

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import { type PropType, ref, computed } from "vue";
import { useDark, useECharts } from "@pureadmin/utils";
const props = defineProps({
data: {
type: Array as PropType<Array<number>>,
default: () => []
},
color: {
type: String,
default: "#41b6ff"
}
});
const { isDark } = useDark();
const theme = computed(() => (isDark.value ? "dark" : "light"));
const chartRef = ref();
const { setOptions } = useECharts(chartRef, {
theme,
renderer: "svg"
});
setOptions({
container: ".line-card",
xAxis: {
type: "category",
show: false,
data: props.data
},
grid: {
top: "15px",
bottom: 0,
left: 0,
right: 0
},
yAxis: {
show: false,
type: "value"
},
series: [
{
data: props.data,
type: "line",
symbol: "none",
smooth: true,
color: props.color,
lineStyle: {
shadowOffsetY: 3,
shadowBlur: 7,
shadowColor: props.color
}
}
]
});
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 60px" />
</template>

View File

@ -0,0 +1,73 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { useDark, useECharts } from "@pureadmin/utils";
const { isDark } = useDark();
const theme = computed(() => (isDark.value ? "dark" : "light"));
const chartRef = ref();
const { setOptions } = useECharts(chartRef, {
theme,
renderer: "svg"
});
setOptions({
container: ".line-card",
title: {
text: "100%",
left: "47%",
top: "30%",
textAlign: "center",
textStyle: {
fontSize: "16",
fontWeight: 600
}
},
polar: {
radius: ["100%", "90%"],
center: ["50%", "50%"]
},
angleAxis: {
max: 100,
show: false
},
radiusAxis: {
type: "category",
show: true,
axisLabel: {
show: false
},
axisLine: {
show: false
},
axisTick: {
show: false
}
},
series: [
{
type: "bar",
roundCap: true,
barWidth: 2,
showBackground: true,
backgroundStyle: {
color: "#dfe7ef"
},
data: [100],
coordinateSystem: "polar",
color: "#7846e5",
itemStyle: {
shadowBlur: 2,
shadowColor: "#7846e5",
shadowOffsetX: 0,
shadowOffsetY: 0
}
}
]
});
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 60px" />
</template>

View File

@ -0,0 +1,3 @@
export { default as ChartBar } from "./ChartBar.vue";
export { default as ChartLine } from "./ChartLine.vue";
export { default as ChartRound } from "./ChartRound.vue";

View File

@ -1,23 +0,0 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { decode } from 'js-base64';
import { MdPreview } from 'md-editor-v3';
import 'md-editor-v3/lib/preview.css';
const content = ref();
/** 获取文档信息 */
const onSearch = async () => {
const response = await fetch('http://129.211.31.58:3000/api/v1/repos/auth/auth-server-java/contents/ReadMe.md');
const json = await response.json();
content.value = decode(json.content);
};
onMounted(() => {
onSearch();
});
</script>
<template>
<MdPreview id="server-read-me" :modelValue="content" />
</template>

View File

@ -0,0 +1,99 @@
import { tableData } from '../../utils/data';
import { delay } from '@pureadmin/utils';
import { onMounted, reactive, ref } from 'vue';
import type { PaginationProps } from '@pureadmin/table';
import ThumbUp from '@iconify-icons/ri/thumb-up-line';
import Hearts from '@iconify-icons/ri/hearts-line';
import Empty from './empty.svg?component';
export function useColumns() {
const dataList = ref([]);
const loading = ref(true);
const columns: TableColumnList = [
{
sortable: true,
label: '序号',
prop: 'id',
},
{
sortable: true,
label: '需求人数',
prop: 'requiredNumber',
filterMultiple: false,
filterClassName: 'pure-table-filter',
filters: [
{ text: '≥16000', value: 'more' },
{ text: '<16000', value: 'less' },
],
filterMethod: (value, { requiredNumber }) => {
return value === 'more' ? requiredNumber >= 16000 : requiredNumber < 16000;
},
},
{
sortable: true,
label: '提问数量',
prop: 'questionNumber',
},
{
sortable: true,
label: '解决数量',
prop: 'resolveNumber',
},
{
sortable: true,
label: '用户满意度',
minWidth: 100,
prop: 'satisfaction',
cellRenderer: ({ row }) => (
<div class='flex justify-center w-full'>
<span class='flex items-center w-[60px]'>
<span class='ml-auto mr-2'>{row.satisfaction}%</span>
<iconifyIconOffline icon={row.satisfaction > 98 ? Hearts : ThumbUp} color='#e85f33' />
</span>
</div>
),
},
{
sortable: true,
label: '统计日期',
prop: 'date',
},
{
label: '操作',
fixed: 'right',
slot: 'operation',
},
];
/** 分页配置 */
const pagination = reactive<PaginationProps>({
pageSize: 10,
currentPage: 1,
layout: 'prev, pager, next',
total: 0,
align: 'center',
});
function onCurrentChange(page: number) {
console.log('onCurrentChange', page);
loading.value = true;
delay(300).then(() => {
loading.value = false;
});
}
onMounted(() => {
dataList.value = tableData;
pagination.total = dataList.value.length;
loading.value = false;
});
return {
Empty,
loading,
columns,
dataList,
pagination,
onCurrentChange,
};
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" class="empty-icon" viewBox="0 0 1024 1024"><path d="M855.6 427.2H168.5c-12.7 0-24.4 6.9-30.6 18L4.4 684.7C1.5 689.9 0 695.8 0 701.8v287.1c0 19.4 15.7 35.1 35.1 35.1H989c19.4 0 35.1-15.7 35.1-35.1V701.8c0-6-1.5-11.8-4.4-17.1L886.2 445.2c-6.2-11.1-17.9-18-30.6-18M673.4 695.6c-16.5 0-30.8 11.5-34.3 27.7-12.7 58.5-64.8 102.3-127.2 102.3s-114.5-43.8-127.2-102.3c-3.5-16.1-17.8-27.7-34.3-27.7H119c-26.4 0-43.3-28-31.1-51.4l81.7-155.8c6.1-11.6 18-18.8 31.1-18.8h622.4c13 0 25 7.2 31.1 18.8l81.7 155.8c12.2 23.4-4.7 51.4-31.1 51.4zm146.5-486.1c-1-1.8-2.1-3.7-3.2-5.5-9.8-16.6-31.1-22.2-47.8-12.6L648.5 261c-17 9.8-22.7 31.6-12.6 48.4.9 1.4 1.7 2.9 2.5 4.4 9.5 17 31.2 22.8 48 13L807 257.3c16.7-9.7 22.4-31 12.9-47.8m-444.5 51.6L255 191.6c-16.7-9.6-38-4-47.8 12.6-1.1 1.8-2.1 3.6-3.2 5.5-9.5 16.8-3.8 38.1 12.9 47.8L337.3 327c16.9 9.7 38.6 4 48-13.1.8-1.5 1.7-2.9 2.5-4.4 10.2-16.8 4.5-38.6-12.4-48.4M512 239.3h2.5c19.5.3 35.5-15.5 35.5-35.1v-139c0-19.3-15.6-34.9-34.8-35.1h-6.4C489.6 30.3 474 46 474 65.2v139c0 19.5 15.9 35.4 35.5 35.1z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,60 @@
<script lang="ts" setup>
import { useColumns } from './columns';
import { useRenderIcon } from '@/components/CommonIcon/src/hooks';
import PureTable from '@pureadmin/table';
const { loading, columns, dataList, pagination, Empty, onCurrentChange } = useColumns();
</script>
<template>
<pure-table
:columns="columns"
:data="dataList.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize)"
:loading="loading"
:loading-config="{ background: 'transparent' }"
:pagination="pagination"
alignWhole="center"
row-key="id"
showOverflowTooltip
@page-current-change="onCurrentChange"
>
<template #empty>
<el-empty :image-size="60" description="暂无数据">
<template #image>
<Empty />
</template>
</el-empty>
</template>
<template #operation="{ row }">
<el-button :icon="useRenderIcon('ri:search-line')" :title="`查看序号为${row.id}的详情`" circle plain size="small" />
</template>
</pure-table>
</template>
<style lang="scss">
.pure-table-filter {
.el-table-filter__list {
min-width: 80px;
padding: 0;
li {
line-height: 28px;
}
}
}
</style>
<style lang="scss" scoped>
:deep(.el-table) {
--el-table-border: none;
--el-table-border-color: transparent;
.el-empty__description {
margin: 0;
}
.el-scrollbar__bar {
display: none;
}
}
</style>

View File

@ -1,23 +0,0 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { decode } from 'js-base64';
import { MdPreview } from 'md-editor-v3';
import 'md-editor-v3/lib/preview.css';
const content = ref();
/** 获取文档信息 */
const onSearch = async () => {
const response = await fetch('http://129.211.31.58:3000/api/v1/repos/auth/auth-web/contents/ReadMe.md');
const json = await response.json();
content.value = decode(json.content);
};
onMounted(() => {
onSearch();
});
</script>
<template>
<MdPreview id="web-read-me" :modelValue="content" />
</template>

View File

@ -1,30 +1,29 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { markRaw, ref } from 'vue';
import ReCol from '@/components/MyCol';
import { useDark } from './utils/utils';
import WelcomeTable from './components/table/index.vue';
import { ReNormalCountTo } from '@/components/CountTo';
import ChartLine from '@/views/welcome/components/ChartLine.vue';
import ChartRound from '@/views/welcome/components/ChartRound.vue';
import { chartData } from './utils/data';
import { getServerCommitList, getWebCommitList, serverCommitList, webCommitList } from '@/views/welcome/utils/hooks';
import WebReadMe from '@/views/welcome/components/web-read-me.vue';
import { TabsPaneContext } from 'element-plus';
import ServerReadMe from '@/views/welcome/components/server-read-me.vue';
import { useRenderFlicker } from '@/components/Flicker';
import { ChartBar, ChartLine, ChartRound } from './components/charts';
import Segmented, { type OptionsType } from '@/components/Segmented';
import { barChartData, chartData, latestNewsData, progressData } from './utils/data';
defineOptions({
name: 'Welcome',
});
const { isDark } = useDark();
// tab
const activeName = ref('web');
// tab
const onTabClick = (tab: TabsPaneContext, _: Event) => {
activeName.value = tab.paneName;
};
onMounted(() => {
getWebCommitList();
getServerCommitList();
});
let curWeek = ref(1); // 01
const optionsBasis: Array<OptionsType> = [
{
label: '上周',
},
{
label: '本周',
},
];
</script>
<template>
@ -62,64 +61,64 @@ onMounted(() => {
</el-card>
</re-col>
<el-row :gutter="24" class="w-[100%] justify-around">
<re-col v-motion :enter="{ opacity: 1, y: 0, transition: { delay: 560 } }" :initial="{ opacity: 0, y: 100 }" :lg="16" :sm="24" :xl="18" :xs="24" class="mb-[18px]">
<el-card class="h-[1178px] overflow-y-auto" shadow="never">
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="onTabClick">
<el-tab-pane label="前端文档" name="web">
<web-read-me class="mt-3 h-[100%]" />
</el-tab-pane>
<el-tab-pane label="后端文档" name="server">
<server-read-me class="mt-3 h-[100%]" />
</el-tab-pane>
</el-tabs>
</el-card>
</re-col>
<re-col v-motion :enter="{ opacity: 1, y: 0, transition: { delay: 400 } }" :initial="{ opacity: 0, y: 100 }" :value="18" :xs="24" class="mb-[18px]">
<el-card class="bar-card" shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">分析概览</span>
<Segmented v-model="curWeek" :options="optionsBasis" />
</div>
<div class="flex justify-between items-start mt-3">
<ChartBar :questionData="barChartData[curWeek].questionData" :requireData="barChartData[curWeek].requireData" />
</div>
</el-card>
</re-col>
<re-col v-motion :enter="{ opacity: 1, y: 0, transition: { delay: 640 } }" :initial="{ opacity: 0, y: 100 }" :lg="8" :sm="24" :xl="6" :xs="24">
<el-card class="mb-[18px]" shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">前端git近期20次更改</span>
</div>
<el-scrollbar class="mt-3" max-height="504">
<el-timeline>
<el-timeline-item v-for="(item, index) in webCommitList" :key="index" :timestamp="item.date" center placement="top">
<p class="text-text_color_regular text-sm">
<el-link :href="item.html_url" :title="item.message" :underline="false" target="_blank">
{{ `提交信息:${item.message},提交用户:` }}
</el-link>
<el-link :href="item.url" :title="item.username" :underline="false" target="_blank">
<el-avatar :size="16" :src="item.avatar_url" class="align-middle" />
{{ ` ${item.username}` }}
</el-link>
</p>
</el-timeline-item>
</el-timeline>
</el-scrollbar>
</el-card>
<re-col v-motion :enter="{ opacity: 1, y: 0, transition: { delay: 480 } }" :initial="{ opacity: 0, y: 100 }" :value="6" :xs="24" class="mb-[18px]">
<el-card shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">解决概率</span>
</div>
<div v-for="(item, index) in progressData" :key="index" :class="['flex', 'justify-between', 'items-start', index === 0 ? 'mt-8' : 'mt-[2.15rem]']">
<el-progress :color="item.color" :duration="item.duration" :percentage="item.percentage" :stroke-width="21" :text-inside="true" striped striped-flow />
<span class="text-nowrap ml-2 text-text_color_regular text-sm">
{{ item.week }}
</span>
</div>
</el-card>
</re-col>
<el-card shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">后端git近期20次更改</span>
</div>
<el-scrollbar class="mt-3" max-height="504">
<el-timeline>
<el-timeline-item v-for="(item, index) in serverCommitList" :key="index" :timestamp="item.date" center placement="top">
<p class="text-text_color_regular text-sm">
<el-link :href="item.html_url" :title="item.message" :underline="false" target="_blank">
{{ `提交信息:${item.message},提交用户:` }}
</el-link>
<el-link :href="item.url" :title="item.username" :underline="false" target="_blank">
<el-avatar :size="16" :src="item.avatar_url" class="align-middle" />
{{ ` ${item.username}` }}
</el-link>
</p>
</el-timeline-item>
</el-timeline>
</el-scrollbar>
</el-card>
</re-col>
</el-row>
<re-col v-motion :enter="{ opacity: 1, y: 0, transition: { delay: 560 } }" :initial="{ opacity: 0, y: 100 }" :value="18" :xs="24" class="mb-[18px]">
<el-card class="h-[580px]" shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">数据统计</span>
</div>
<WelcomeTable class="mt-3" />
</el-card>
</re-col>
<re-col v-motion :enter="{ opacity: 1, y: 0, transition: { delay: 640 } }" :initial="{ opacity: 0, y: 100 }" :value="6" :xs="24" class="mb-[18px]">
<el-card shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">最新动态</span>
</div>
<el-scrollbar class="mt-3" max-height="504">
<el-timeline>
<el-timeline-item
v-for="(item, index) in latestNewsData"
:key="index"
:icon="markRaw(useRenderFlicker({ background: randomGradient({ randomizeHue: true }) }))"
:timestamp="item.date"
center
placement="top"
>
<p class="text-text_color_regular text-sm">
{{ `新增 ${item.requiredNumber} 条问题,${item.resolveNumber} 条已解决` }}
</p>
</el-timeline-item>
</el-timeline>
</el-scrollbar>
</el-card>
</re-col>
</el-row>
</div>
</template>

View File

@ -1,8 +1,11 @@
import { cloneDeep, dayjs, getRandomIntBetween } from './utils';
import GroupLine from '@iconify-icons/ri/group-line';
import Question from '@iconify-icons/ri/question-answer-line';
import CheckLine from '@iconify-icons/ri/chat-check-line';
import Smile from '@iconify-icons/ri/star-smile-line';
const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
/** 需求人数、提问数量、解决数量、用户满意度 */
const chartData = [
{
@ -47,4 +50,83 @@ const chartData = [
},
];
export { chartData };
/** 分析概览 */
const barChartData = [
{
requireData: [2101, 5288, 4239, 4962, 6752, 5208, 7450],
questionData: [2216, 1148, 1255, 1788, 4821, 1973, 4379],
},
{
requireData: [2101, 3280, 4400, 4962, 5752, 6889, 7600],
questionData: [2116, 3148, 3255, 3788, 4821, 4970, 5390],
},
];
/** 解决概率 */
const progressData = [
{
week: '周一',
percentage: 85,
duration: 110,
color: '#41b6ff',
},
{
week: '周二',
percentage: 86,
duration: 105,
color: '#41b6ff',
},
{
week: '周三',
percentage: 88,
duration: 100,
color: '#41b6ff',
},
{
week: '周四',
percentage: 89,
duration: 95,
color: '#41b6ff',
},
{
week: '周五',
percentage: 94,
duration: 90,
color: '#26ce83',
},
{
week: '周六',
percentage: 96,
duration: 85,
color: '#26ce83',
},
{
week: '周日',
percentage: 100,
duration: 80,
color: '#26ce83',
},
].reverse();
/** 数据统计 */
const tableData = Array.from({ length: 30 }).map((_, index) => {
return {
id: index + 1,
requiredNumber: getRandomIntBetween(13500, 19999),
questionNumber: getRandomIntBetween(12600, 16999),
resolveNumber: getRandomIntBetween(13500, 17999),
satisfaction: getRandomIntBetween(95, 100),
date: dayjs().subtract(index, 'day').format('YYYY-MM-DD'),
};
});
/** 最新动态 */
const latestNewsData = cloneDeep(tableData)
.slice(0, 26)
.map((item, index) => {
return Object.assign(item, {
date: `${dayjs().subtract(index, 'day').format('YYYY-MM-DD')} ${days[dayjs().subtract(index, 'day').day()]}`,
});
});
export { chartData, barChartData, progressData, tableData, latestNewsData };

View File

@ -1,34 +0,0 @@
import { ref } from 'vue';
import dayjs from 'dayjs';
// 前端代码提交记录
export const webCommitList = ref([]);
// 后端代码提交记录
export const serverCommitList = ref([]);
/** 获取web代码提交记录 */
export const getWebCommitList = async () => {
const response = await fetch('http://129.211.31.58:3000/api/v1/repos/auth/auth-web/commits?page=1&limit=20');
const json = await response.json();
webCommitList.value = json.map(item => ({
date: dayjs(item?.commit?.committer?.date).format('YYYY-MM-DD HH:mm:ss'),
url: item?.committer?.html_url,
username: item?.committer?.username,
avatar_url: item?.committer?.avatar_url,
message: item?.commit?.message,
html_url: item?.html_url,
}));
};
/** 获取后端代码提交记录 */
export const getServerCommitList = async () => {
const response = await fetch('http://129.211.31.58:3000/api/v1/repos/auth/auth-server-java/commits?page=1&limit=20');
const json = await response.json();
serverCommitList.value = json.map(item => ({
date: dayjs(item?.commit?.committer?.date).format('YYYY-MM-DD HH:mm:ss'),
url: item?.committer?.html_url,
username: item?.committer?.username,
avatar_url: item?.committer?.avatar_url,
message: item?.commit?.message,
html_url: item?.html_url,
}));
};