引入tailwindCss

This commit is contained in:
2024-07-01 15:23:00 +08:00
parent 6e5ac35913
commit 77a08b3f0f
35 changed files with 3003 additions and 1530 deletions

View File

@@ -14,6 +14,7 @@
"@amap/amap-jsapi-loader": "^1.0.1",
"@ant-design/colors": "^6.0.0",
"@ant-design/icons-vue": "^6.1.0",
"@antv/g2": "^5.1.22",
"@bytemd/plugin-gfm": "^1.17.2",
"@bytemd/plugin-highlight": "^1.17.4",
"@bytemd/plugin-highlight-ssr": "^1.20.2",

View File

@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { Cooperate, CooperateParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询商务合作
*/
export async function pageCooperate(params: CooperateParam) {
const res = await request.get<ApiResult<PageResult<Cooperate>>>(
MODULES_API_URL + '/booking/cooperate/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询商务合作列表
*/
export async function listCooperate(params?: CooperateParam) {
const res = await request.get<ApiResult<Cooperate[]>>(
MODULES_API_URL + '/booking/cooperate',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加商务合作
*/
export async function addCooperate(data: Cooperate) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/booking/cooperate',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改商务合作
*/
export async function updateCooperate(data: Cooperate) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/booking/cooperate',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除商务合作
*/
export async function removeCooperate(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/booking/cooperate/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除商务合作
*/
export async function removeBatchCooperate(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/booking/cooperate/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询商务合作
*/
export async function getCooperate(id: number) {
const res = await request.get<ApiResult<Cooperate>>(
MODULES_API_URL + '/booking/cooperate/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,33 @@
import type { PageParam } from '@/api';
/**
* 商务合作
*/
export interface Cooperate {
// ID
cooperateId?: number;
// 部门名称
name?: string;
// 咨询电话
phone?: string;
// 图片
image?: string;
// 备注
comments?: string;
// 状态
status?: number;
// 排序号
sortNumber?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
}
/**
* 商务合作搜索条件
*/
export interface CooperateParam extends PageParam {
cooperateId?: number;
keywords?: string;
}

View File

@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CooperateLog, CooperateLogParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询商务合作留言记录
*/
export async function pageCooperateLog(params: CooperateLogParam) {
const res = await request.get<ApiResult<PageResult<CooperateLog>>>(
MODULES_API_URL + '/booking/cooperate-log/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询商务合作留言记录列表
*/
export async function listCooperateLog(params?: CooperateLogParam) {
const res = await request.get<ApiResult<CooperateLog[]>>(
MODULES_API_URL + '/booking/cooperate-log',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加商务合作留言记录
*/
export async function addCooperateLog(data: CooperateLog) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/booking/cooperate-log',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改商务合作留言记录
*/
export async function updateCooperateLog(data: CooperateLog) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/booking/cooperate-log',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除商务合作留言记录
*/
export async function removeCooperateLog(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/booking/cooperate-log/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除商务合作留言记录
*/
export async function removeBatchCooperateLog(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/booking/cooperate-log/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询商务合作留言记录
*/
export async function getCooperateLog(id: number) {
const res = await request.get<ApiResult<CooperateLog>>(
MODULES_API_URL + '/booking/cooperate-log/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,35 @@
import type { PageParam } from '@/api';
/**
* 商务合作留言记录
*/
export interface CooperateLog {
// ID
logId?: number;
// 关联ID
cooperateId?: number;
// 部门名称
name?: string;
// 咨询电话
phone?: string;
// 图片
image?: string;
// 备注
comments?: string;
// 状态
status?: number;
// 排序号
sortNumber?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
}
/**
* 商务合作留言记录搜索条件
*/
export interface CooperateLogParam extends PageParam {
logId?: number;
keywords?: string;
}

View File

@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { Match, MatchParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询比赛信息表
*/
export async function pageMatch(params: MatchParam) {
const res = await request.get<ApiResult<PageResult<Match>>>(
MODULES_API_URL + '/booking/match/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询比赛信息表列表
*/
export async function listMatch(params?: MatchParam) {
const res = await request.get<ApiResult<Match[]>>(
MODULES_API_URL + '/booking/match',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加比赛信息表
*/
export async function addMatch(data: Match) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/booking/match',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改比赛信息表
*/
export async function updateMatch(data: Match) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/booking/match',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除比赛信息表
*/
export async function removeMatch(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/booking/match/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除比赛信息表
*/
export async function removeBatchMatch(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/booking/match/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询比赛信息表
*/
export async function getMatch(id: number) {
const res = await request.get<ApiResult<Match>>(
MODULES_API_URL + '/booking/match/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,73 @@
import type { PageParam } from '@/api';
/**
* 比赛信息表
*/
export interface Match {
// 赛事ID
matchId?: number;
// 标题
title?: string;
// 比赛类型 0常规比赛
type?: number;
// 活动开始时间
matchStartTime?: number;
// 活动结束时间
matchEndTime?: number;
// 报名时间
bmStartTime?: number;
// 报名时间
bmEndTime?: number;
// 文章分类ID
categoryId?: number;
// 封面图
image?: string;
// 虚拟阅读量(仅用作展示)
virtualViews?: number;
// 实际阅读量
actualViews?: number;
// 文章附件
files?: string;
// 视频地址
video?: string;
// 退费规则
refundRule?: string;
// 活动介绍
content?: string;
// 经度
longitude?: string;
// 纬度
latitude?: string;
// 比赛活动地点
address?: string;
// 报名费用
price?: string;
// 已报名人数
users?: number;
// 报名人数限制
maxUsers?: number;
// 用户ID
userId?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0未开始, 1进行中2已结束
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 比赛信息表搜索条件
*/
export interface MatchParam extends PageParam {
matchId?: number;
keywords?: string;
}

View File

@@ -0,0 +1,106 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { MatchOrder, MatchOrderParam } from './model';
import { MODULES_API_URL } from '@/config/setting';
/**
* 分页查询比赛报名记录表
*/
export async function pageMatchOrder(params: MatchOrderParam) {
const res = await request.get<ApiResult<PageResult<MatchOrder>>>(
MODULES_API_URL + '/booking/match-order/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询比赛报名记录表列表
*/
export async function listMatchOrder(params?: MatchOrderParam) {
const res = await request.get<ApiResult<MatchOrder[]>>(
MODULES_API_URL + '/booking/match-order',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加比赛报名记录表
*/
export async function addMatchOrder(data: MatchOrder) {
const res = await request.post<ApiResult<unknown>>(
MODULES_API_URL + '/booking/match-order',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改比赛报名记录表
*/
export async function updateMatchOrder(data: MatchOrder) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/booking/match-order',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除比赛报名记录表
*/
export async function removeMatchOrder(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/booking/match-order/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除比赛报名记录表
*/
export async function removeBatchMatchOrder(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
MODULES_API_URL + '/booking/match-order/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询比赛报名记录表
*/
export async function getMatchOrder(id: number) {
const res = await request.get<ApiResult<MatchOrder>>(
MODULES_API_URL + '/booking/match-order/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,47 @@
import type { PageParam } from '@/api';
/**
* 比赛报名记录表
*/
export interface MatchOrder {
// 赛事ID
matchOrderId?: number;
// 赛事ID
matchId?: number;
// 场次
session?: number;
// 比赛活动地点
address?: string;
// 姓名
name?: number;
// 性别
sex?: number;
// 身份证号码
idCardNo?: string;
// 手机号码
phone?: string;
// 用户ID
userId?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0未开始, 1进行中2已结束
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 注册时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 比赛报名记录表搜索条件
*/
export interface MatchOrderParam extends PageParam {
matchOrderId?: number;
keywords?: string;
}

View File

@@ -0,0 +1,185 @@
{
"code": 0,
"data": [
{
"name": "软妹子",
"value": 23
},
{
"name": "汪星人",
"value": 23
},
{
"name": "长腿欧巴",
"value": 23
},
{
"name": "萝莉",
"value": 22
},
{
"name": "辣~",
"value": 22
},
{
"name": "K歌",
"value": 22
},
{
"name": "大长腿",
"value": 21
},
{
"name": "川妹子",
"value": 21
},
{
"name": "女神",
"value": 21
},
{
"name": "米粉",
"value": 20
},
{
"name": "专注设计",
"value": 20
},
{
"name": "逛街",
"value": 20
},
{
"name": "黑长直",
"value": 20
},
{
"name": "海纳百川",
"value": 19
},
{
"name": "萌萌哒",
"value": 19
},
{
"name": "坚持",
"value": 19
},
{
"name": "话唠",
"value": 19
},
{
"name": "果粉",
"value": 18
},
{
"name": "喵星人",
"value": 18
},
{
"name": "花粉",
"value": 18
},
{
"name": "衬衫控",
"value": 18
},
{
"name": "宅男",
"value": 17
},
{
"name": "小清新",
"value": 17
},
{
"name": "眼镜男",
"value": 17
},
{
"name": "琼瑶",
"value": 17
},
{
"name": "穷游党",
"value": 16
},
{
"name": "铲屎官",
"value": 16
},
{
"name": "正太",
"value": 16
},
{
"name": "中二病",
"value": 16
},
{
"name": "夜猫子",
"value": 15
},
{
"name": "逗比",
"value": 15
},
{
"name": "腹黑",
"value": 15
},
{
"name": "吃鸡",
"value": 15
},
{
"name": "为了联盟",
"value": 14
},
{
"name": "背包客",
"value": 14
},
{
"name": "民谣",
"value": 14
},
{
"name": "为了部落",
"value": 14
},
{
"name": "懒癌患者",
"value": 13
},
{
"name": "追剧",
"value": 13
},
{
"name": "IT民工",
"value": 13
},
{
"name": "CNB成员",
"value": 13
},
{
"name": "选择困难",
"value": 12
},
{
"name": "锤粉",
"value": 12
},
{
"name": "欧皇",
"value": 12
},
{
"name": "仙气十足",
"value": 12
}
]
}

View File

@@ -0,0 +1,73 @@
{
"code": 0,
"data": [
{
"date": "2020-06-12",
"value": 7
},
{
"date": "2020-06-13",
"value": 5
},
{
"date": "2020-06-14",
"value": 4
},
{
"date": "2020-06-15",
"value": 2
},
{
"date": "2020-06-16",
"value": 4
},
{
"date": "2020-06-17",
"value": 7
},
{
"date": "2020-06-18",
"value": 5
},
{
"date": "2020-06-19",
"value": 6
},
{
"date": "2020-06-20",
"value": 5
},
{
"date": "2020-06-21",
"value": 9
},
{
"date": "2020-06-22",
"value": 6
},
{
"date": "2020-06-23",
"value": 3
},
{
"date": "2020-06-24",
"value": 1
},
{
"date": "2020-06-25",
"value": 5
},
{
"date": "2020-06-26",
"value": 3
},
{
"date": "2020-06-27",
"value": 6
},
{
"date": "2020-06-18",
"value": 5
}
]
}

View File

@@ -0,0 +1,105 @@
{
"code": 0,
"data": {
"list1": [
{
"month": "1月",
"value": 816
},
{
"month": "2月",
"value": 542
},
{
"month": "3月",
"value": 914
},
{
"month": "4月",
"value": 781
},
{
"month": "5月",
"value": 355
},
{
"month": "6月",
"value": 796
},
{
"month": "7月",
"value": 714
},
{
"month": "8月",
"value": 1195
},
{
"month": "9月",
"value": 1055
},
{
"month": "10月",
"value": 271
},
{
"month": "11月",
"value": 384
},
{
"month": "12月",
"value": 1098
}
],
"list2": [
{
"month": "1月",
"value": 1098
},
{
"month": "2月",
"value": 384
},
{
"month": "3月",
"value": 271
},
{
"month": "4月",
"value": 1055
},
{
"month": "5月",
"value": 1195
},
{
"month": "6月",
"value": 714
},
{
"month": "7月",
"value": 796
},
{
"month": "8月",
"value": 355
},
{
"month": "9月",
"value": 781
},
{
"month": "10月",
"value": 914
},
{
"month": "11月",
"value": 542
},
{
"month": "12月",
"value": 816
}
]
}
}

View File

@@ -0,0 +1,60 @@
{
"code": 0,
"data": [
{
"time": "16:00",
"visits": 15,
"views": 45
},
{
"time": "16:05",
"visits": 39,
"views": 169
},
{
"time": "16:10",
"visits": 152,
"views": 400
},
{
"time": "16:15",
"visits": 94,
"views": 285
},
{
"time": "16:20",
"visits": 102,
"views": 316
},
{
"time": "16:25",
"visits": 86,
"views": 148
},
{
"time": "16:30",
"visits": 39,
"views": 150
},
{
"time": "16:35",
"visits": 38,
"views": 234
},
{
"time": "16:40",
"visits": 95,
"views": 158
},
{
"time": "16:45",
"visits": 30,
"views": 100
},
{
"time": "16:50",
"visits": 86,
"views": 266
}
]
}

View File

@@ -1,13 +1,13 @@
import request from '@/utils/request';
import type { ApiResult } from '@/api';
import type { PayNumData, SaleroomResult, VisitData, CloudData } from './model';
import type { PayNumData, VisitData, CloudData } from './model';
/**
* 获取支付笔数数据
*/
export async function getPayNumList() {
const res = await request.get<ApiResult<PayNumData[]>>(
'https://www.gxwebsoft.com/20200610/analysis-pay-num.json'
'./20200610/analysis-pay-num.json'
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
@@ -19,9 +19,10 @@ export async function getPayNumList() {
* 获取销售量数据
*/
export async function getSaleroomList() {
const res = await request.get<ApiResult<SaleroomResult>>(
'https://www.gxwebsoft.com/20200610/analysis-saleroom.json'
);
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
const res = await readFile('./20200610/analysis-saleroom.json');
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
@@ -34,7 +35,7 @@ export async function getSaleroomList() {
*/
export async function getVisitHourList() {
const res = await request.get<ApiResult<VisitData[]>>(
'https://www.gxwebsoft.com/20200610/analysis-visits.json'
'./20200610/analysis-visits.json'
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
@@ -47,7 +48,7 @@ export async function getVisitHourList() {
*/
export async function getWordCloudList() {
const res = await request.get<ApiResult<CloudData[]>>(
'https://www.gxwebsoft.com/20200610/analysis-hot-search.json'
'./20200610/analysis-hot-search.json'
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;

View File

@@ -22,7 +22,7 @@ export async function getChinaMapData() {
*/
export async function getUserCountList() {
const res = await request.get<ApiResult<UserCount[]>>(
'https://www.gxwebsoft.com/20200610/monitor-user-count.json'
'https://cdn.eleadmin.com/20200610/monitor-user-count.json'
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
@@ -35,7 +35,7 @@ export async function getUserCountList() {
*/
export async function getBrowserCountList() {
const res = await request.get<ApiResult<BrowserCount[]>>(
'https://www.gxwebsoft.com/20200610/monitor-browser-count.json'
'https://cdn.eleadmin.com/20200610/monitor-browser-count.json'
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;

View File

@@ -1,138 +0,0 @@
<!-- 最新动态 -->
<template>
<a-card :title="title" :bordered="false" :body-style="{ padding: '6px 0' }">
<template #extra>
<more-icon @remove="onRemove" @edit="onEdit" />
</template>
<div
style="height: 346px; padding: 22px 20px 0 20px"
class="ele-scrollbar-hover"
>
<a-timeline>
<a-timeline-item
v-for="item in activities"
:key="item.id"
:color="item.color"
>
<em>{{ item.time }}</em>
<em>{{ item.title }}</em>
</a-timeline-item>
</a-timeline>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import MoreIcon from './more-icon.vue';
defineProps<{
title?: string;
}>();
const emit = defineEmits<{
(e: 'remove'): void;
(e: 'edit'): void;
}>();
interface Activitie {
id: number;
title: string;
time: string;
color?: string;
}
// 最新动态数据
const activities = ref<Activitie[]>([]);
/* 查询最新动态 */
const queryActivities = () => {
activities.value = [
{
id: 1,
title: 'SunSmile 解决了bug 登录提示操作失败',
time: '20:30',
color: 'gray'
},
{
id: 2,
title: 'Jasmine 解决了bug 按钮颜色与设计不符',
time: '19:30',
color: 'gray'
},
{
id: 3,
title: '项目经理 指派了任务 解决项目一的bug',
time: '18:30'
},
{
id: 4,
title: '项目经理 指派了任务 解决项目二的bug',
time: '17:30'
},
{
id: 5,
title: '项目经理 指派了任务 解决项目三的bug',
time: '16:30'
},
{
id: 6,
title: '项目经理 指派了任务 解决项目四的bug',
time: '15:30',
color: 'gray'
},
{
id: 7,
title: '项目经理 指派了任务 解决项目五的bug',
time: '14:30',
color: 'gray'
},
{
id: 8,
title: '项目经理 指派了任务 解决项目六的bug',
time: '12:30',
color: 'gray'
},
{
id: 9,
title: '项目经理 指派了任务 解决项目七的bug',
time: '11:30'
},
{
id: 10,
title: '项目经理 指派了任务 解决项目八的bug',
time: '10:30',
color: 'gray'
},
{
id: 11,
title: '项目经理 指派了任务 解决项目九的bug',
time: '09:30',
color: 'green'
},
{
id: 12,
title: '项目经理 指派了任务 解决项目十的bug',
time: '08:30',
color: 'red'
}
];
};
const onRemove = () => {
emit('remove');
};
const onEdit = () => {
emit('edit');
};
queryActivities();
</script>
<style lang="less" scoped>
.ele-scrollbar-hover
:deep(.ant-timeline-item-last > .ant-timeline-item-content) {
min-height: auto;
}
</style>

View File

@@ -1,82 +0,0 @@
<template>
<a-card :title="title" :bordered="false" :body-style="{ padding: '2px' }">
<template #extra
><a @click="openUrl('/oa/app/index')" class="ele-text-placeholder"
>更多<RightOutlined /></a
></template>
<a-list :size="`small`" :split="false" :data-source="list">
<template #renderItem="{ item }">
<a-list-item>
<div class="app-box">
<a-image
:height="45"
:width="45"
:preview="false"
:src="item.appIcon"
fallback="https://file.wsdns.cn/20230218/550e610d43334dd2a7f66d5b20bd58eb.svg"
/>
<div class="app-info">
<a
class="ele-text-heading"
@click="openNew('/oa/app/detail/' + item.appId)"
>
{{ item.appName }}
</a>
<span class="ele-text-placeholder">
{{ item.appCode }}
</span>
</div>
</div>
</a-list-item>
</template>
</a-list>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { Article } from '@/api/cms/article/model';
import { openUrl } from '@/utils/common';
import { pageApp } from '@/api/oa/app';
import { RightOutlined } from '@ant-design/icons-vue';
const props = defineProps<{
title: string;
}>();
const list = ref<Article[]>([]);
/**
* 加载数据
*/
const reload = () => {
const { title } = props;
// 加载文章列表
pageApp({
limit: 5,
status: 0,
appStatus: '开发中'
}).then((data) => {
if (data?.list) {
list.value = data.list;
}
});
};
reload();
</script>
<script lang="ts">
export default {
name: 'DashboardArticleList'
};
</script>
<style lang="less" scoped>
.app-box {
display: flex;
.app-info {
display: flex;
margin-left: 5px;
flex-direction: column;
}
}
</style>

View File

@@ -1,56 +0,0 @@
<template>
<a-card :title="title" :bordered="false" :body-style="{ padding: '2px' }">
<template #extra
><a
@click="openNew('/cms/category/' + categoryId)"
class="ele-text-placeholder"
>更多<RightOutlined /></a
></template>
<a-list :size="`small`" :split="false" :data-source="list">
<template #renderItem="{ item }">
<a-list-item>
<a
class="ele-text-secondary"
@click="openUrl('/cms/article/' + item.articleId)"
>
{{ item.title }}
</a>
</a-list-item>
</template>
</a-list>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { pageArticle } from '@/api/cms/article';
import { Article } from '@/api/cms/article/model';
import { openNew, openUrl } from '@/utils/common';
import { RightOutlined } from '@ant-design/icons-vue';
const list = ref<Article[]>([]);
const props = defineProps<{
title: string;
categoryId: number;
}>();
/**
* 加载数据
*/
const reload = () => {
const { categoryId } = props;
// 加载文章列表
pageArticle({ categoryId, limit: 6 }).then((data) => {
if (data?.list) {
list.value = data.list;
}
});
};
reload();
</script>
<script lang="ts">
export default {
name: 'DashboardArticleList'
};
</script>

View File

@@ -1,80 +0,0 @@
<template>
<a-card :bordered="false" title="小组成员">
<template #extra>
<a-tooltip>
<template #title>邀请加入</template>
<UserAddOutlined @click="onShowQrcode" :style="{ fontSize: '18px' }" />
</a-tooltip>
</template>
<a-list
class="demo-loadmore-list"
item-layout="horizontal"
:data-source="list"
>
<template #renderItem="{ item }">
<a-list-item>
<template #actions>
<a-popover>
<template #content> 待处理 </template>
<a class="ele-text-danger">{{ item.pending }}</a>
</a-popover>
<a-popover>
<template #content> 本月已处理 </template>
<a class="ele-text-secondary">{{ item.month }}</a>
</a-popover>
</template>
<a-skeleton avatar :title="false" :loading="!!item.loading" active>
<a-list-item-meta>
<template #title>
{{ item.nickname }}
</template>
<template #avatar>
<a-avatar :src="item.avatar" />
</template>
</a-list-item-meta>
</a-skeleton>
</a-list-item>
</template>
</a-list>
<!-- 工单二维码 -->
<Qrcode v-model:visible="showQrcode" />
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { pageTaskCount } from '@/api/oa/task-count';
import { TaskCount } from '@/api/oa/task-count/model';
import { UserAddOutlined } from '@ant-design/icons-vue';
import Qrcode from './qrcode.vue';
const showQrcode = ref(false);
const list = ref<TaskCount[]>([]);
pageTaskCount({ limit: 10, roleCode: 'commander' }).then((res) => {
if (res) {
list.value = res?.list;
}
});
const onShowQrcode = () => {
showQrcode.value = true;
};
</script>
<style lang="less" scoped>
.monitor-evaluate-text {
width: 90px;
flex-shrink: 0;
white-space: nowrap;
opacity: 0.8;
& > .anticon {
font-size: 12px;
margin: 0 6px 0 8px;
}
}
/deep/.ant-list-item {
padding: 7px 0;
}
</style>

View File

@@ -1,70 +0,0 @@
<!-- 本月目标 -->
<template>
<a-card :title="title" :bordered="false">
<template #extra>
<more-icon @remove="onRemove" @edit="onEdit" />
</template>
<div class="workplace-goal-group">
<a-progress
:width="180"
:percent="80"
type="dashboard"
:stroke-width="4"
:show-info="false"
/>
<div class="workplace-goal-content">
<ele-tag color="blue" size="large" shape="circle">
<trophy-outlined />
</ele-tag>
<div class="workplace-goal-num">285</div>
</div>
<div class="workplace-goal-text">恭喜, 本月目标已达标!</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { TrophyOutlined } from '@ant-design/icons-vue';
import MoreIcon from './more-icon.vue';
defineProps<{
title?: string;
}>();
const emit = defineEmits<{
(e: 'remove'): void;
(e: 'edit'): void;
}>();
const onRemove = () => {
emit('remove');
};
const onEdit = () => {
emit('edit');
};
</script>
<style lang="less" scoped>
.workplace-goal-group {
height: 310px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
.workplace-goal-content {
position: absolute;
top: 50%;
left: 50%;
width: 180px;
margin: -50px 0 0 -90px;
text-align: center;
}
.workplace-goal-num {
font-size: 40px;
}
}
</style>

View File

@@ -1,70 +0,0 @@
<!-- 本月目标 -->
<template>
<a-card :title="title" :bordered="false">
<template #extra>
<more-icon @remove="onRemove" @edit="onEdit" />
</template>
<div class="workplace-goal-group">
<a-progress
:width="180"
:percent="80"
type="dashboard"
:stroke-width="4"
:show-info="false"
/>
<div class="workplace-goal-content">
<ele-tag color="blue" size="large" shape="circle">
<trophy-outlined />
</ele-tag>
<div class="workplace-goal-num">285</div>
</div>
<div class="workplace-goal-text">恭喜, 本月目标已达标!</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { TrophyOutlined } from '@ant-design/icons-vue';
import MoreIcon from './more-icon.vue';
defineProps<{
title?: string;
}>();
const emit = defineEmits<{
(e: 'remove'): void;
(e: 'edit'): void;
}>();
const onRemove = () => {
emit('remove');
};
const onEdit = () => {
emit('edit');
};
</script>
<style lang="less" scoped>
.workplace-goal-group {
height: 310px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
.workplace-goal-content {
position: absolute;
top: 50%;
left: 50%;
width: 180px;
margin: -50px 0 0 -90px;
text-align: center;
}
.workplace-goal-num {
font-size: 40px;
}
}
</style>

View File

@@ -0,0 +1,72 @@
<template>
<a-card :bordered="false" title="热门搜索">
<v-chart
ref="hotSearchChartRef"
:option="hotSearchChartOption"
style="height: 330px"
/>
</a-card>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue/es';
import { use } from 'echarts/core';
import type { EChartsCoreOption } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { LineChart, BarChart } from 'echarts/charts';
import { GridComponent, TooltipComponent } from 'echarts/components';
import VChart from 'vue-echarts';
import 'echarts-wordcloud';
import { wordCloudColor } from 'ele-admin-pro/es';
import { getWordCloudList } from '@/api/dashboard/analysis';
import useEcharts from '@/utils/use-echarts';
use([CanvasRenderer, LineChart, BarChart, GridComponent, TooltipComponent]);
//
const hotSearchChartRef = ref<InstanceType<typeof VChart> | null>(null);
useEcharts([hotSearchChartRef]);
// 词云图表配置
const hotSearchChartOption: EChartsCoreOption = reactive({});
/* 获取词云数据 */
const getWordCloudData = () => {
getWordCloudList()
.then((data) => {
Object.assign(hotSearchChartOption, {
tooltip: {
show: true,
confine: true,
borderWidth: 1
},
series: [
{
type: 'wordCloud',
width: '100%',
height: '100%',
sizeRange: [12, 24],
gridSize: 6,
textStyle: {
color: wordCloudColor
},
emphasis: {
textStyle: {
shadowBlur: 8,
shadowColor: 'rgba(0, 0, 0, .15)'
}
},
data: data
}
]
});
})
.catch((e) => {
message.error(e.message);
});
};
getWordCloudData();
</script>

View File

@@ -1,178 +0,0 @@
<!-- 快捷方式 -->
<template>
<a-row :gutter="16" ref="wrapRef">
<a-col v-for="item in data" :key="item.url" :lg="3" :md="6" :sm="9" :xs="8">
<a-card :bordered="false" hoverable :body-style="{ padding: 0 }">
<div class="app-link-block" @click="navTo(item)">
<component
:is="item.icon"
class="app-link-icon"
:style="{ color: item.color }"
/>
<div class="app-link-title">{{ item.title }}</div>
</div>
</a-card>
</a-col>
</a-row>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import SortableJs from 'sortablejs';
import type { Row as ARow } from 'ant-design-vue';
import { openUrl } from '@/utils/common';
import { getOriginDomain } from '@/utils/domain';
const CACHE_KEY = 'workplace-links';
// 当前开发环境
const env = import.meta.env.MODE;
interface LinkItem {
icon: string;
title: string;
url: string;
color?: string;
}
// 默认顺序
const DEFAULT: LinkItem[] = [
{
icon: 'settingOutlined',
title: '系统设置',
url: '/system/setting'
},
{
icon: 'AntDesignOutlined',
title: '项目管理',
url: '/oa/app/index'
},
{
icon: 'TeamOutlined',
title: '用户管理',
url: '/system/user'
},
{
icon: 'ShoppingOutlined',
title: '产品管理',
url: '/product/index'
},
{
icon: 'FileSearchOutlined',
title: '文章管理',
url: '/cms/article'
},
{
icon: 'ChromeOutlined',
title: '网址导航',
url: '/oa/link'
},
{
icon: 'AppstoreAddOutlined',
title: '扩展插件',
url: '/system/plug'
},
{
icon: 'DesktopOutlined',
title: '应用主页',
url: `http://${localStorage.getItem('TenantId')}.${localStorage.getItem(
'domain'
)}`
}
];
// 获取缓存的顺序
const cache = (() => {
const str = localStorage.getItem(CACHE_KEY);
try {
return str ? JSON.parse(str) : null;
} catch (e) {
return null;
}
})();
const data = ref<LinkItem[]>([...(cache ?? DEFAULT)]);
const wrapRef = ref<InstanceType<typeof ARow> | null>(null);
let sortableIns: SortableJs | null = null;
/* 重置布局 */
const reset = () => {
data.value = [...DEFAULT];
cacheData();
};
/* 缓存布局 */
const cacheData = () => {
localStorage.setItem(CACHE_KEY, JSON.stringify(data.value));
};
const navTo = (item) => {
if (item.icon == 'DesktopOutlined') {
if (env == 'development') {
return openUrl(getOriginDomain());
}
return openUrl(`http://www.${domain.value}`);
}
openUrl(item.url);
};
onMounted(() => {
const isTouchDevice = 'ontouchstart' in document.documentElement;
if (isTouchDevice) {
return;
}
sortableIns = new SortableJs(wrapRef.value?.$el, {
animation: 300,
onUpdate: ({ oldIndex, newIndex }) => {
if (typeof oldIndex === 'number' && typeof newIndex === 'number') {
const temp = [...data.value];
temp.splice(newIndex, 0, temp.splice(oldIndex, 1)[0]);
data.value = temp;
cacheData();
}
},
setData: () => {}
});
});
onBeforeUnmount(() => {
if (sortableIns) {
sortableIns.destroy();
}
});
defineExpose({ reset });
</script>
<script lang="ts">
import * as icons from './link-icons';
import { getSiteInfo } from '@/api/layout';
import { ref } from 'vue';
const tenantId = ref<number>();
const domain = ref<string>();
getSiteInfo().then((data) => {
tenantId.value = data.tenantId;
domain.value = data.domain;
});
export default {
components: icons
};
</script>
<style lang="less" scoped>
.app-link-block {
padding: 12px;
text-align: center;
display: block;
color: inherit;
.app-link-icon {
color: #666666;
font-size: 30px;
margin: 6px 0 10px 0;
}
}
</style>

View File

@@ -1,12 +0,0 @@
export {
UserOutlined,
TeamOutlined,
FileSearchOutlined,
ChromeOutlined,
ShoppingOutlined,
LaptopOutlined,
AppstoreAddOutlined,
DesktopOutlined,
AntDesignOutlined,
SettingOutlined
} from '@ant-design/icons-vue';

View File

@@ -1,54 +0,0 @@
<template>
<a-card :title="linkType" :bordered="false" :body-style="{ padding: '2px' }">
<template #extra
><a @click="openNew('/oa/link')" class="ele-text-placeholder"
>更多<RightOutlined /></a
></template>
<a-list :size="`small`" :split="false" :data-source="list">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #avatar>
<a-avatar :src="item.icon" />
</template>
<template #title>
<a @click="openUrl(item.url)">{{ item.name }}</a>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { openNew, openUrl } from '@/utils/common';
import { pageLink } from '@/api/oa/link';
import { RightOutlined } from '@ant-design/icons-vue';
const list = ref<any[]>([]);
const props = defineProps<{
linkType: string;
}>();
/**
* 加载数据
*/
const reload = () => {
const { linkType } = props;
// 加载文章列表
pageLink({ linkType, limit: 5 }).then((data) => {
if (data?.list) {
list.value = data.list;
}
});
};
reload();
</script>
<script lang="ts">
export default {
name: 'DashboardArticleList'
};
</script>

View File

@@ -1,38 +0,0 @@
<template>
<a-dropdown placement="bottomRight">
<more-outlined class="ele-text-secondary" style="font-size: 18px" />
<template #overlay>
<a-menu :selectable="false" @click="onClick">
<a-menu-item key="edit">
<div class="ele-cell">
<edit-outlined />
<div class="ele-cell-content">编辑</div>
</div>
</a-menu-item>
<a-menu-item key="remove">
<div class="ele-cell ele-text-danger">
<delete-outlined />
<div class="ele-cell-content">删除</div>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<script lang="ts" setup>
import {
MoreOutlined,
EditOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
const emit = defineEmits<{
(e: 'edit'): void;
(e: 'remove'): void;
}>();
const onClick = ({ key }) => {
emit(key);
};
</script>

View File

@@ -1,121 +0,0 @@
<!-- 用户信息 -->
<template>
<a-card :bordered="false" :body-style="{ padding: '20px' }">
<div class="ele-cell workplace-user-card">
<div class="ele-cell-content ele-cell">
<a-avatar :size="68" :src="loginUser.avatar">
<template v-if="!loginUser.avatar" #icon>
<user-outlined />
</template>
</a-avatar>
<div class="ele-cell-content">
<h4 class="ele-elip">
早安, {{ loginUser.nickname }}, 开始您一天的工作吧!
</h4>
<div class="ele-elip ele-text-secondary">
<cloud-outlined />
<em>{{ elip[Math.floor(Math.random() * elip.length)] }}</em>
<!-- <em>今日多云转阴18 - 22出门记得穿外套哦~</em>-->
</div>
</div>
</div>
<div class="workplace-count-group">
<!-- <div class="workplace-count-item">-->
<!-- <div class="workplace-count-header">-->
<!-- <ele-tag color="blue" shape="circle" size="small">-->
<!-- <appstore-filled />-->
<!-- </ele-tag>-->
<!-- <span class="workplace-count-name">项目数</span>-->
<!-- </div>-->
<!-- <h2>0</h2>-->
<!-- </div>-->
<!-- <div class="workplace-count-item">-->
<!-- <div class="workplace-count-header">-->
<!-- <ele-tag color="orange" shape="circle" size="small">-->
<!-- <check-square-outlined />-->
<!-- </ele-tag>-->
<!-- <span class="workplace-count-name">待办项</span>-->
<!-- </div>-->
<!-- <h2>6 / 24</h2>-->
<!-- </div>-->
<!-- <div class="workplace-count-item">-->
<!-- <div class="workplace-count-header">-->
<!-- <ele-tag color="green" shape="circle" size="small">-->
<!-- <bell-filled />-->
<!-- </ele-tag>-->
<!-- <span class="workplace-count-name">消息</span>-->
<!-- </div>-->
<!-- <h2>0</h2>-->
<!-- </div>-->
</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import {
UserOutlined,
CloudOutlined,
AppstoreFilled,
CheckSquareOutlined,
BellFilled
} from '@ant-design/icons-vue';
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
// 当前登录用户信息
const loginUser = computed(() => userStore.info ?? {});
const elip = ref<string[]>([
'小事成就大事,细节成就完美~',
'心态决定命运,自信走向成功',
'人生能有几回博,今日不博何时博',
'成功需要成本,时间也是一种成本,对时间的珍惜就是对成本的节约',
'有志者自有千方百计,无志者只感千难万难',
'积一时之跬步,臻千里之遥程'
]);
</script>
<style lang="less" scoped>
.workplace-user-card {
.ele-cell-content {
overflow: hidden;
}
h4 {
margin-bottom: 6px;
}
}
.workplace-count-group {
white-space: nowrap;
text-align: right;
flex-shrink: 0;
}
.workplace-count-item {
display: inline-block;
margin: 0 4px 0 24px;
}
.workplace-count-name {
margin-left: 8px;
}
@media screen and (max-width: 992px) {
.workplace-count-item {
margin: 0 2px 0 12px;
}
}
@media screen and (max-width: 768px) {
.workplace-user-card {
display: block;
}
.workplace-count-group {
margin-top: 8px;
}
}
</style>

View File

@@ -1,66 +0,0 @@
<template>
<ele-modal
:width="400"
:visible="visible"
:title="`邀请新成员`"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
:footer="null"
@ok="save"
>
<div
class="qrcode-list"
style="display: flex; justify-content: space-around"
>
<div>
<img :src="qrcode" width="240" height="240" />
<div
style="
display: flex;
justify-content: center;
font-size: 26px;
padding-top: 20px;
"
>使用微信扫一扫</div
>
</div>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { User } from '@/api/system/user/model';
import { reactive, ref } from 'vue';
import { taskJoinQRCode } from '@/api/oa/task';
defineProps<{
// 弹窗是否打开
visible: boolean;
}>();
const emit = defineEmits<{
(e: 'done', user: User): void;
(e: 'update:visible', visible: boolean): void;
}>();
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const qrcode = ref('');
// 用户信息
const form = reactive<User>({
userId: undefined,
nickname: undefined
});
const save = () => {
emit('done', form);
};
taskJoinQRCode({}).then((text) => {
qrcode.value = String(text);
});
</script>
<style scoped lang="less"></style>

View File

@@ -0,0 +1,247 @@
<template>
<a-card :bordered="false" :body-style="{ padding: 0 }">
<a-tabs
size="large"
v-model:activeKey="saleSearch.type"
class="monitor-card-tabs"
@change="onSaleTypeChange"
>
<a-tab-pane tab="销售额" key="saleroom" />
<template #rightExtra>
<a-space
size="middle"
:class="[
'analysis-tabs-extra',
{ 'hidden-lg-and-down': styleResponsive }
]"
>
<a-radio-group v-model:value="saleSearch.dateType">
<a-radio-button value="1">今天</a-radio-button>
<a-radio-button value="2">本周</a-radio-button>
<a-radio-button value="3">本月</a-radio-button>
<a-radio-button value="4">本年</a-radio-button>
</a-radio-group>
<div style="width: 300px">
<a-range-picker
value-format="YYYY-MM-DD"
v-model:value="saleSearch.datetime"
/>
</div>
</a-space>
</template>
</a-tabs>
<div style="padding-bottom: 10px">
<a-row :gutter="16">
<a-col
v-bind="
styleResponsive ? { lg: 17, md: 15, sm: 24, xs: 24 } : { span: 17 }
"
>
<div v-if="saleSearch.type === 'saleroom'" class="demo-monitor-title">
销售量趋势
</div>
<div v-else class="demo-monitor-title">访问量趋势</div>
<v-chart
ref="saleChartRef"
:option="saleChartOption"
style="height: 320px"
/>
</a-col>
<a-col
v-bind="
styleResponsive ? { lg: 7, md: 9, sm: 24, xs: 24 } : { span: 7 }
"
>
<div v-if="saleSearch.type === 'saleroom'">
<div class="demo-monitor-title">门店销售额排名</div>
<div
v-for="(item, index) in saleroomRankData"
:key="index"
class="demo-monitor-rank-item ele-cell"
>
<ele-tag
shape="circle"
:color="index < 3 ? '#314659' : ''"
style="border: none"
>
{{ index + 1 }}
</ele-tag>
<div class="ele-cell-content ele-elip">{{ item.name }}</div>
<div class="ele-text-secondary">{{ item.value }}</div>
</div>
</div>
<div v-else>
<div class="demo-monitor-title">门店访问量排名</div>
<div
v-for="(item, index) in visitsRankData"
:key="index"
class="demo-monitor-rank-item ele-cell"
>
<ele-tag
shape="circle"
:color="index < 3 ? '#314659' : ''"
style="border: none"
>
{{ index + 1 }}
</ele-tag>
<div class="ele-cell-content ele-elip">{{ item.name }}</div>
<div class="ele-text-secondary">{{ item.value }}</div>
</div>
</div>
</a-col>
</a-row>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue/es';
import { use } from 'echarts/core';
import type { EChartsCoreOption } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { BarChart } from 'echarts/charts';
import { GridComponent, TooltipComponent } from 'echarts/components';
import VChart from 'vue-echarts';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import { getSaleroomList } from '@/api/dashboard/analysis';
import type { SaleroomData } from '@/api/dashboard/analysis/model';
import useEcharts from '@/utils/use-echarts';
use([CanvasRenderer, BarChart, GridComponent, TooltipComponent]);
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
//
const saleChartRef = ref<InstanceType<typeof VChart> | null>(null);
useEcharts([saleChartRef]);
// 销售额柱状图配置
const saleChartOption: EChartsCoreOption = reactive({});
// 门店销售排名数据
const saleroomRankData = ref([
{ name: '工专路 1 号店', value: '323,234' },
{ name: '工专路 2 号店', value: '323,234' },
{ name: '工专路 3 号店', value: '323,234' },
{ name: '工专路 4 号店', value: '323,234' },
{ name: '工专路 5 号店', value: '323,234' },
{ name: '工专路 6 号店', value: '323,234' },
{ name: '工专路 7 号店', value: '323,234' }
]);
// 门店访问排名数据
const visitsRankData = ref([
{ name: '工专路 1 号店', value: '323,234' },
{ name: '工专路 2 号店', value: '323,234' },
{ name: '工专路 3 号店', value: '323,234' },
{ name: '工专路 4 号店', value: '323,234' },
{ name: '工专路 5 号店', value: '323,234' },
{ name: '工专路 6 号店', value: '323,234' },
{ name: '工专路 7 号店', value: '323,234' }
]);
// 销售量趋势数据
const saleroomData1 = ref<SaleroomData[]>([]);
// 访问量趋势数据
const saleroomData2 = ref<SaleroomData[]>([]);
interface SaleSearchType {
type: string;
dateType: string;
datetime: [string, string];
}
// 销售量搜索参数
const saleSearch = reactive<SaleSearchType>({
type: 'saleroom',
dateType: '1',
datetime: ['2022-01-08', '2022-02-12']
});
/* 获取销售量数据 */
const getSaleroomData = () => {
getSaleroomList()
.then((data) => {
saleroomData1.value = data.list1;
saleroomData2.value = data.list2;
onSaleTypeChange();
})
.catch((e) => {
message.error(e.message);
});
};
/* 销售量tab选择改变事件 */
const onSaleTypeChange = () => {
if (saleSearch.type === 'saleroom') {
Object.assign(saleChartOption, {
tooltip: {
trigger: 'axis'
},
xAxis: [
{
type: 'category',
data: saleroomData1.value.map((d) => d.month)
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
type: 'bar',
data: saleroomData1.value.map((d) => d.value)
}
]
});
} else {
Object.assign(saleChartOption, {
tooltip: {
trigger: 'axis'
},
xAxis: [
{
type: 'category',
data: saleroomData2.value.map((d) => d.month)
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
type: 'bar',
data: saleroomData2.value.map((d) => d.value)
}
]
});
}
};
getSaleroomData();
</script>
<style lang="less" scoped>
.monitor-card-tabs :deep(.ant-tabs-nav) {
padding: 0 16px;
}
.demo-monitor-title {
padding: 6px 20px;
}
.demo-monitor-rank-item {
padding: 0 20px;
margin-top: 18px;
}
</style>

View File

@@ -0,0 +1,246 @@
<!-- 统计卡片 -->
<template>
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { lg: 6, md: 12, sm: 24, xs: 24 } : { span: 6 }"
>
<a-card class="analysis-chart-card" :bordered="false">
<div class="ele-text-secondary ele-cell">
<div class="ele-cell-content">总销售额</div>
<a-tooltip title="指标说明">
<question-circle-outlined />
</a-tooltip>
</div>
<h1 class="analysis-chart-card-num">¥ 126,560</h1>
<div class="analysis-chart-card-content" style="padding-top: 16px">
<a-space size="middle">
<span class="analysis-trend-text">
周同比12% <caret-up-outlined class="ele-text-danger" />
</span>
<span class="analysis-trend-text">
日同比11% <caret-down-outlined class="ele-text-success" />
</span>
</a-space>
</div>
<a-divider />
<div>日销售额 12,423</div>
</a-card>
</a-col>
<a-col
v-bind="styleResponsive ? { lg: 6, md: 12, sm: 24, xs: 24 } : { span: 6 }"
>
<a-card class="analysis-chart-card" :bordered="false">
<div class="ele-text-secondary ele-cell">
<div class="ele-cell-content">访问量</div>
<ele-tag color="red"></ele-tag>
</div>
<h1 class="analysis-chart-card-num">8,846</h1>
<v-chart
ref="visitChartRef"
:option="visitChartOption"
style="height: 40px"
/>
<a-divider />
<div>日访问量 1,234</div>
</a-card>
</a-col>
<a-col
v-bind="styleResponsive ? { lg: 6, md: 12, sm: 24, xs: 24 } : { span: 6 }"
>
<a-card class="analysis-chart-card" :bordered="false">
<div class="ele-text-secondary ele-cell">
<div class="ele-cell-content">支付笔数</div>
<ele-tag color="blue"></ele-tag>
</div>
<h1 class="analysis-chart-card-num">6,560</h1>
<v-chart
ref="payNumChartRef"
:option="payNumChartOption"
style="height: 40px"
/>
<a-divider />
<div>转化率 60%</div>
</a-card>
</a-col>
<a-col
v-bind="styleResponsive ? { lg: 6, md: 12, sm: 24, xs: 24 } : { span: 6 }"
>
<a-card class="analysis-chart-card" :bordered="false">
<div class="ele-text-secondary ele-cell">
<div class="ele-cell-content">活动运营效果</div>
<ele-tag color="green"></ele-tag>
</div>
<h1 class="analysis-chart-card-num">78%</h1>
<div class="analysis-chart-card-content" style="padding-top: 16px">
<a-progress
:percent="78"
:show-info="false"
stroke-color="#13c2c2"
status="active"
/>
</div>
<a-divider />
<a-space size="middle">
<span class="analysis-trend-text">
周同比12% <caret-up-outlined class="ele-text-danger" />
</span>
<span class="analysis-trend-text">
日同比11% <caret-down-outlined class="ele-text-success" />
</span>
</a-space>
</a-card>
</a-col>
</a-row>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue/es';
import {
QuestionCircleOutlined,
CaretUpOutlined,
CaretDownOutlined
} from '@ant-design/icons-vue';
import { use } from 'echarts/core';
import type { EChartsCoreOption } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { LineChart, BarChart } from 'echarts/charts';
import { GridComponent, TooltipComponent } from 'echarts/components';
import VChart from 'vue-echarts';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import { getPayNumList } from '@/api/dashboard/analysis';
import useEcharts from '@/utils/use-echarts';
use([CanvasRenderer, LineChart, BarChart, GridComponent, TooltipComponent]);
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
//
const visitChartRef = ref<InstanceType<typeof VChart> | null>(null);
const payNumChartRef = ref<InstanceType<typeof VChart> | null>(null);
useEcharts([visitChartRef, payNumChartRef]);
// 访问量折线图配置
const visitChartOption: EChartsCoreOption = reactive({});
// 支付笔数柱状图配置
const payNumChartOption: EChartsCoreOption = reactive({});
/* 获取支付笔数数据 */
const getPayNumData = () => {
getPayNumList()
.then((data) => {
Object.assign(visitChartOption, {
color: '#975fe5',
tooltip: {
trigger: 'axis',
formatter:
'<i class="ele-chart-dot" style="background: #975fe5;"></i>{b0}: {c0}'
},
grid: {
top: 10,
bottom: 0,
left: 0,
right: 0
},
xAxis: [
{
show: false,
type: 'category',
boundaryGap: false,
data: data.map((d) => d.date)
}
],
yAxis: [
{
show: false,
type: 'value',
splitLine: {
show: false
}
}
],
series: [
{
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: {
opacity: 0.5
},
data: data.map((d) => d.value)
}
]
});
Object.assign(payNumChartOption, {
tooltip: {
trigger: 'axis',
formatter:
'<i class="ele-chart-dot" style="background: #5b8ff9;"></i>{b0}: {c0}'
},
grid: {
top: 10,
bottom: 0,
left: 0,
right: 0
},
xAxis: [
{
show: false,
type: 'category',
data: data.map((d) => d.date)
}
],
yAxis: [
{
show: false,
type: 'value',
splitLine: {
show: false
}
}
],
series: [
{
type: 'bar',
data: data.map((d) => d.value)
}
]
});
})
.catch((e) => {
message.error(e.message);
});
};
getPayNumData();
</script>
<style lang="less" scoped>
.analysis-chart-card {
:deep(.ant-card-body) {
padding: 16px 22px 12px 22px;
}
:deep(.ant-divider) {
margin: 12px 0;
}
}
.analysis-chart-card-num {
font-size: 30px;
}
.analysis-chart-card-content {
height: 40px;
}
.analysis-trend-text {
white-space: nowrap;
}
</style>

View File

@@ -1,112 +0,0 @@
<template>
<a-card :title="title" :bordered="false" :body-style="{ padding: '2px' }">
<template #extra
><a @click="openUrl('/oa/task')" class="ele-text-placeholder"
>更多<RightOutlined /></a
></template>
<a-list :size="`small`" :split="false" :data-source="list">
<template #renderItem="{ item }">
<a-list-item>
<div class="app-box">
<div class="app-info">
<a
class="ele-text-secondary"
@click="openNew('/oa/task/detail/' + item.taskId)"
>
<a-typography-paragraph
ellipsis
:content="`【${item.taskType}】${item.name}`"
/>
</a>
</div>
<a class="ele-text-placeholder">
<a-tag v-if="item.progress === TOBEARRANGED" color="red"
>待安排</a-tag
>
<a-tag v-if="item.progress === PENDING" color="orange"
>待处理</a-tag
>
<a-tag v-if="item.progress === PROCESSING" color="purple"
>处理中</a-tag
>
<a-tag v-if="item.progress === TOBECONFIRMED" color="cyan"
>待评价</a-tag
>
<a-tag v-if="item.progress === COMPLETED" color="green"
>已完成</a-tag
>
<a-tag v-if="item.progress === CLOSED">已关闭</a-tag>
<div class="ele-text-danger" v-if="item.overdueDays">
已逾期{{ item.overdueDays }}
</div>
</a>
</div>
</a-list-item>
</template>
</a-list>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { openUrl } from '@/utils/common';
import { Task } from '@/api/oa/task/model';
import { pageTask } from '@/api/oa/task';
import { useUserStore } from '@/store/modules/user';
import {
CLOSED,
COMPLETED,
PENDING,
PROCESSING,
TOBEARRANGED,
TOBECONFIRMED
} from '@/api/oa/task/model/progress';
import { RightOutlined } from '@ant-design/icons-vue';
const props = defineProps<{
title: string;
}>();
const list = ref<Task[]>([]);
/**
* 加载数据
*/
const reload = () => {
const { title } = props;
const userStore = useUserStore();
const where = {
userId: undefined,
commander: userStore.info?.userId,
limit: 6,
status: 0
};
// 加载列表
pageTask(where).then((data) => {
if (data?.list) {
list.value = data.list;
}
});
};
reload();
</script>
<script lang="ts">
export default {
name: 'DashboardArticleList'
};
</script>
<style lang="less" scoped>
.app-box {
display: flex;
width: 100%;
justify-content: space-between;
overflow: hidden;
.app-info {
display: flex;
margin-left: 5px;
flex-direction: column;
width: 400px;
}
}
</style>

View File

@@ -1,84 +0,0 @@
<!-- 小组成员 -->
<template>
<a-card :title="title" :bordered="false" :body-style="{ padding: '2px 0px' }">
<template #extra>
<more-icon @remove="onRemove" @edit="onEdit" />
</template>
<div
v-for="(item, index) in userList"
:key="index"
class="ele-cell user-list-item"
>
<div style="flex-shrink: 0">
<a-avatar :size="46" :src="item.avatar" />
</div>
<div class="ele-cell-content">
<span class="ele-cell-title ele-elip">{{ item.nickname }}</span>
<div class="ele-cell-desc ele-elip">{{ item.phone }}</div>
</div>
<div style="flex-shrink: 0">
<a-tag :color="['green', 'red'][item.status]">
{{ ['在线', '离线'][item.status] }}
</a-tag>
</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import MoreIcon from './more-icon.vue';
import { pageUsers } from '@/api/system/user';
import type { User } from '@/api/system/user/model';
defineProps<{
title?: string;
}>();
const emit = defineEmits<{
(e: 'remove'): void;
(e: 'edit'): void;
}>();
// 小组成员数据
const userList = ref<User[]>([]);
/* 查询小组成员 */
const queryUserList = () => {
pageUsers({ parentId: 11, limit: 5 }).then((data: any) => {
userList.value = data.list;
});
};
const onRemove = () => {
emit('remove');
};
const onEdit = () => {
emit('edit');
};
queryUserList();
</script>
<style lang="less" scoped>
.user-list-item {
padding: 12px 18px;
& + .user-list-item {
border-top: 1px solid hsla(0, 0%, 60%, 0.15);
}
.ele-cell-content {
overflow: hidden;
}
.ele-cell-desc {
margin-top: 0;
}
.ant-tag {
margin: 0;
}
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<a-card
:bordered="false"
title="最近1小时访问情况"
:body-style="{ padding: '16px 6px 0 0' }"
>
<v-chart
ref="visitHourChartRef"
:option="visitHourChartOption"
style="height: 362px"
/>
</a-card>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue/es';
import { use } from 'echarts/core';
import type { EChartsCoreOption } from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import { LineChart } from 'echarts/charts';
import {
GridComponent,
TooltipComponent,
LegendComponent
} from 'echarts/components';
import VChart from 'vue-echarts';
import { getVisitHourList } from '@/api/dashboard/analysis';
import useEcharts from '@/utils/use-echarts';
use([
CanvasRenderer,
LineChart,
GridComponent,
TooltipComponent,
LegendComponent
]);
//
const visitHourChartRef = ref<InstanceType<typeof VChart> | null>(null);
useEcharts([visitHourChartRef]);
// 最近 1 小时访问情况折线图配置
const visitHourChartOption: EChartsCoreOption = reactive({});
/* 获取最近 1 小时访问情况数据 */
const getVisitHourData = () => {
getVisitHourList()
.then((data) => {
Object.assign(visitHourChartOption, {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['浏览量', '访问量'],
right: 20
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: data.map((d) => d.time)
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: '浏览量',
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: {
opacity: 0.5
},
data: data.map((d) => d.views)
},
{
name: '访问量',
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: {
opacity: 0.5
},
data: data.map((d) => d.visits)
}
]
});
})
.catch((e) => {
message.error(e.message);
});
};
getVisitHourData();
</script>

View File

@@ -1,359 +1,25 @@
<template>
<div class="ele-body ele-body-card">
<a-card :bordered="false">
<template #title>
<SoundOutlined class="ele-text-danger" />
<span class="gg-title ele-text-heading">公告</span>
<a @click="openNew('/cms/article/374')" class="ele-text-heading"
>系统内测中如何获取体验账号</a
>
</template>
<template #extra>
<a @click="openNew('/cms/category/92')" class="ele-text-placeholder"
>更多<RightOutlined
/></a>
</template>
</a-card>
<!-- <profile-card />-->
<LinkCard />
<a-row :gutter="16" ref="wrapRef">
<a-col :md="9">
<App title="我的项目" />
</a-col>
<a-col :md="9">
<Task title="我的工单" :categoryId="92" />
</a-col>
<a-col :md="6">
<Article title="通知公告" :categoryId="92" />
</a-col>
<a-col :md="6">
<Article title="公司动态" :categoryId="37" />
</a-col>
<a-col :md="6">
<Article title="经验分享" :categoryId="93" />
</a-col>
<a-col :md="6">
<Article title="API文档" :categoryId="90" />
</a-col>
<a-col :md="6">
<Link linkType="实用工具" />
</a-col>
<!-- <a-col :md="12">-->
<!-- <a-card title="商品管理" :bordered="false" />-->
<!-- </a-col>-->
</a-row>
<!-- <a-row :gutter="16" ref="wrapRef">-->
<!-- <a-col-->
<!-- v-for="(item, index) in data"-->
<!-- :key="item.name"-->
<!-- :lg="item.lg"-->
<!-- :md="item.md"-->
<!-- :sm="item.sm"-->
<!-- :xs="item.xs"-->
<!-- >-->
<!-- <component-->
<!-- :is="item.name"-->
<!-- :title="item.title"-->
<!-- @remove="onRemove(index)"-->
<!-- @edit="onEdit(index)"-->
<!-- />-->
<!-- </a-col>-->
<!-- </a-row>-->
<a-card :bordered="false" :body-style="{ padding: 0 }">
<div class="ele-cell" style="line-height: 42px">
<div
class="ele-cell-content ele-text-primary workplace-bottom-btn"
@click="add"
>
<PlusCircleOutlined /> 添加视图
</div>
<a-divider type="vertical" />
<div
class="ele-cell-content ele-text-primary workplace-bottom-btn"
@click="reset"
>
<UndoOutlined /> 重置布局
</div>
</div>
</a-card>
<ele-modal
:width="680"
v-model:visible="visible"
title="未添加的视图"
:footer="null"
>
<a-row :gutter="16">
<a-col
v-for="item in notAddedData"
:key="item.name"
:md="8"
:sm="12"
:xs="24"
>
<div
class="workplace-card-item ele-border-split"
@click="addView(item)"
>
<div class="workplace-card-header ele-border-split">
{{ item.title }}
</div>
<div class="workplace-card-body ele-text-placeholder">
<plus-circle-outlined />
</div>
</div>
</a-col>
</a-row>
<a-empty v-if="!notAddedData.length" description="已添加所有视图" />
</ele-modal>
<statistics-card />
<sale-card />
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import SortableJs from 'sortablejs';
import type { Row as ARow } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import Article from './components/article-list.vue';
import Link from './components/link-list.vue';
import App from './components/app-list.vue';
import Task from './components/task-card.vue';
import {
PlusCircleOutlined,
SoundOutlined,
RightOutlined
} from '@ant-design/icons-vue';
import { openNew } from '@/utils/common';
const CACHE_KEY = 'workplace-layout';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import StatisticsCard from './components/statistics-card.vue';
import SaleCard from './components/sale-card.vue';
import VisitHour from './components/visit-hour.vue';
import HotSearch from './components/hot-search.vue';
interface ViewItem {
name: string;
title: string;
lg: number;
md: number;
sm: number;
xs: number;
}
// 默认布局
const DEFAULT: ViewItem[] = [
{
name: 'link',
title: '网址导航',
lg: 24,
md: 24,
sm: 24,
xs: 24
},
{
name: 'task-card',
title: '我的工单',
lg: 18,
md: 24,
sm: 24,
xs: 24
},
// {
// name: 'project-card',
// title: '项目管理',
// lg: 16,
// md: 24,
// sm: 24,
// xs: 24
// },
{
name: 'user-list',
title: '小组成员',
lg: 6,
md: 24,
sm: 24,
xs: 24
}
// {
// name: 'activities-card',
// title: '最新动态',
// lg: 6,
// md: 24,
// sm: 24,
// xs: 24
// },
// {
// name: 'goal-card',
// title: '本月目标',
// lg: 8,
// md: 24,
// sm: 24,
// xs: 24
// },
// {
// name: 'docs',
// title: '知识库',
// lg: 8,
// md: 24,
// sm: 24,
// xs: 24
// }
];
// 获取缓存的顺序
const cache = (() => {
const str = localStorage.getItem(CACHE_KEY);
try {
return str ? JSON.parse(str) : null;
} catch (e) {
return null;
}
})();
const data = ref<ViewItem[]>([...(cache ?? DEFAULT)]);
const visible = ref(false);
const wrapRef = ref<InstanceType<typeof ARow> | null>(null);
let sortableIns: SortableJs | null = null;
// 未添加的数据
const notAddedData = computed(() => {
return DEFAULT.filter((d) => !data.value.some((t) => t.name === d.name));
});
/* 添加 */
const add = () => {
visible.value = true;
};
/* 重置布局 */
const reset = () => {
data.value = [...DEFAULT];
cacheData();
message.success('已重置');
};
/* 缓存布局 */
const cacheData = () => {
localStorage.setItem(CACHE_KEY, JSON.stringify(data.value));
};
/* 删除视图 */
const onRemove = (index: number) => {
data.value = data.value.filter((_d, i) => i !== index);
cacheData();
};
/* 编辑视图 */
const onEdit = (index: number) => {
data.value.map((d) => {
if (d.name == 'user-list') {
}
});
// message.info('点击了编辑');
};
/* 添加视图 */
const addView = (item) => {
data.value.push(item);
cacheData();
message.success('已添加');
};
onMounted(() => {
const isTouchDevice = 'ontouchstart' in document.documentElement;
if (isTouchDevice) {
return;
}
sortableIns = new SortableJs(wrapRef.value?.$el, {
handle: '.ant-card-head',
animation: 300,
onUpdate: ({ oldIndex, newIndex }) => {
if (typeof oldIndex === 'number' && typeof newIndex === 'number') {
const temp = [...data.value];
temp.splice(newIndex, 0, temp.splice(oldIndex, 1)[0]);
data.value = temp;
cacheData();
}
},
setData: () => {}
});
});
onBeforeUnmount(() => {
if (sortableIns) {
sortableIns.destroy();
}
});
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
</script>
<script lang="ts">
import ActivitiesCard from './components/activities-card.vue';
import TaskCard from './components/task-card.vue';
import GoalCard from './components/goal-card.vue';
import UserList from './components/count-user.vue';
import Docs from './components/docs.vue';
import LinkCard from './components/link-card.vue';
import ProfileCard from './components/profile-card.vue';
export default {
name: 'DashboardWorkplace',
components: {
LinkCard,
UserList,
ActivitiesCard,
TaskCard,
GoalCard,
Docs,
ProfileCard
}
name: 'DashboardAnalysis'
};
</script>
<style lang="less" scoped>
.ele-body :deep(.ant-card-head) {
cursor: move;
position: relative;
}
.ele-body :deep(.ant-row > .ant-col.sortable-chosen > .ant-card) {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.2);
}
.workplace-bottom-btn {
text-align: center;
cursor: pointer;
transition: background-color 0.2s;
}
.workplace-bottom-btn:hover {
background: hsla(0, 0%, 60%, 0.05);
}
/* 添加弹窗 */
.workplace-card-item {
margin-bottom: 15px;
border-width: 1px;
border-style: solid;
border-radius: 4px;
position: relative;
cursor: pointer;
transition: box-shadow 0.2s, background-color 0.2s;
}
.workplace-card-item:hover {
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.1);
background: hsla(0, 0%, 60%, 0.05);
}
.workplace-card-item .workplace-card-header {
border-bottom-width: 1px;
border-bottom-style: solid;
padding: 8px;
}
.gg-title {
padding: 0 5px;
margin-right: 20px;
}
.workplace-card-body {
font-size: 26px;
padding: 24px 10px;
text-align: center;
}
</style>

1293
yarn.lock

File diff suppressed because it is too large Load Diff