新增前端vue

This commit is contained in:
2025-11-26 18:17:22 +08:00
parent 3683b86db7
commit 3ac807e5d9
38 changed files with 5121 additions and 544 deletions

View File

@@ -0,0 +1,22 @@
- 官方网站:<https://jeesite.com>
- 使用文档:<https://jeesite.com/docs>
- 后端代码:<https://gitee.com/thinkgem/jeesite5>
- 前端代码:<https://gitee.com/thinkgem/jeesite-vue>
------
<div align="center">
如果你喜欢 JeeSite请给她一个 ⭐️ Star您的支持将是我们前行的动力。
</div>
------
- 问题反馈:<https://gitee.com/thinkgem/jeesite-vue/issues> [【新手必读】](https://gitee.com/thinkgem/jeesite5/issues/I18ARR)
- 需求收集:<https://gitee.com/thinkgem/jeesite-vue/issues/new>
- QQ 群:`127515876``209330483``223507718``709534275``730390092``1373527``183903863(外包)`
- 微信群:添加客服微信 <http://s.jeesite.com> 邀请您进群
- 关注微信公众号,了解最新动态:
<p style="padding-left:40px">
<img alt="JeeSite微信公众号" src="https://jeesite.com/assets/images/mp.png" width="220" height="220">
</p>

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author gaoxq
*/
import { defHttp } from '@jeesite/core/utils/http/axios';
import { useGlobSetting } from '@jeesite/core/hooks/setting';
import { BasicModel, Page } from '@jeesite/core/api/model/baseModel';
const { adminPath } = useGlobSetting();
export interface BizQuickLogin extends BasicModel<BizQuickLogin> {
createTime?: string; // 创建时间
systemName: string; // 系统名称
homepageUrl: string; // 首页地址
iconClass: string; // 图标类名
iconColor: string; // 图标颜色
sortOrder: number; // 排序序号
bgColor: string; // 图标背景色
maskColor: string; // 悬浮遮罩色
isEnabled: number; // 是否启用
updateTime?: string; // 更新时间
ftenantId?: string; // 租户id
fflowId?: string; // 流程id
fflowTaskId?: string; // 流程任务主键
fflowState?: number; // 流程任务状态
}
export const bizQuickLoginList = (params?: BizQuickLogin | any) =>
defHttp.get<BizQuickLogin>({ url: adminPath + '/biz/quickLogin/list', params });
export const bizQuickLoginListData = (params?: BizQuickLogin | any) =>
defHttp.post<Page<BizQuickLogin>>({ url: adminPath + '/biz/quickLogin/listData', params });
export const bizQuickLoginForm = (params?: BizQuickLogin | any) =>
defHttp.get<BizQuickLogin>({ url: adminPath + '/biz/quickLogin/form', params });
export const bizQuickLoginSave = (params?: any, data?: BizQuickLogin | any) =>
defHttp.postJson<BizQuickLogin>({ url: adminPath + '/biz/quickLogin/save', params, data });
export const bizQuickLoginDelete = (params?: BizQuickLogin | any) =>
defHttp.get<BizQuickLogin>({ url: adminPath + '/biz/quickLogin/delete', params });

View File

@@ -0,0 +1,30 @@
{
"name": "@jeesite/biz",
"version": "5.14.0",
"private": true,
"type": "module",
"scripts": {
"type:check": "vue-tsc --noEmit --skipLibCheck",
"uninstall": "rimraf node_modules",
"update": "ncu -u"
},
"dependencies": {
"qs": "6.14.0"
},
"devDependencies": {
"@types/qs": "6.9.18"
},
"homepage": "https://jeesite.com",
"repository": {
"type": "git",
"url": "https://gitee.com/thinkgem/jeesite-vue.git"
},
"bugs": {
"url": "https://gitee.com/thinkgem/jeesite-vue/issues"
},
"author": {
"name": "ThinkGem",
"email": "thinkgem@163.com",
"url": "https://gitee.com/thinkgem"
}
}

View File

@@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@jeesite/biz/*": ["./*"]
}
},
"include": [
"./**/*.ts",
"./**/*.tsx",
"./**/*.vue"
],
"exclude": [
"node_modules",
"vite.config.ts",
"_lib",
"dist"
]
}

View File

@@ -0,0 +1,158 @@
<!--
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author gaoxq
-->
<template>
<BasicDrawer
v-bind="$attrs"
:showFooter="true"
:okAuth="'biz:quickLogin:edit'"
@register="registerDrawer"
@ok="handleSubmit"
width="70%"
>
<template #title>
<Icon :icon="getTitle.icon" class="m-1 pr-1" />
<span> {{ getTitle.value }} </span>
</template>
<BasicForm @register="registerForm" />
</BasicDrawer>
</template>
<script lang="ts" setup name="ViewsBizQuickLoginForm">
import { ref, unref, computed } from 'vue';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
import { router } from '@jeesite/core/router';
import { Icon } from '@jeesite/core/components/Icon';
import { BasicForm, FormSchema, useForm } from '@jeesite/core/components/Form';
import { BasicDrawer, useDrawerInner } from '@jeesite/core/components/Drawer';
import { BizQuickLogin, bizQuickLoginSave, bizQuickLoginForm } from '@jeesite/biz/api/biz/quickLogin';
const emit = defineEmits(['success', 'register']);
const { t } = useI18n('biz.quickLogin');
const { showMessage } = useMessage();
const { meta } = unref(router.currentRoute);
const record = ref<BizQuickLogin>({} as BizQuickLogin);
const getTitle = computed(() => ({
icon: meta.icon || 'i-ant-design:book-outlined',
value: record.value.isNewRecord ? t('新增系统信息') : t('编辑系统信息'),
}));
const inputFormSchemas: FormSchema<BizQuickLogin>[] = [
{
label: t('系统名称'),
field: 'systemName',
component: 'Input',
componentProps: {
maxlength: 100,
},
required: true,
},
{
label: t('首页地址'),
field: 'homepageUrl',
component: 'Input',
componentProps: {
maxlength: 255,
},
required: true,
},
{
label: t('图标类名'),
field: 'iconClass',
component: 'Input',
componentProps: {
maxlength: 50,
},
required: true,
},
{
label: t('图标颜色'),
field: 'iconColor',
component: 'Input',
componentProps: {
maxlength: 20,
},
required: true,
},
{
label: t('排序序号'),
field: 'sortOrder',
component: 'Input',
componentProps: {
maxlength: 10,
},
required: true,
},
{
label: t('图标背景色'),
field: 'bgColor',
component: 'Input',
componentProps: {
maxlength: 52,
},
required: true,
},
{
label: t('悬浮遮罩色'),
field: 'maskColor',
component: 'Input',
componentProps: {
maxlength: 52,
},
required: true,
},
{
label: t('是否启用'),
field: 'isEnabled',
component: 'Select',
componentProps: {
dictType: '',
allowClear: true,
},
required: true,
},
];
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm<BizQuickLogin>({
labelWidth: 120,
schemas: inputFormSchemas,
baseColProps: { md: 24, lg: 12 },
});
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
setDrawerProps({ loading: true });
await resetFields();
const res = await bizQuickLoginForm(data);
record.value = (res.bizQuickLogin || {}) as BizQuickLogin;
record.value.__t = new Date().getTime();
await setFieldsValue(record.value);
setDrawerProps({ loading: false });
});
async function handleSubmit() {
try {
const data = await validate();
setDrawerProps({ confirmLoading: true });
const params: any = {
isNewRecord: record.value.isNewRecord,
id: record.value.id || data.id,
};
// console.log('submit', params, data, record);
const res = await bizQuickLoginSave(params, data);
showMessage(res.message);
setTimeout(closeDrawer);
emit('success', data);
} catch (error: any) {
if (error && error.errorFields) {
showMessage(error.message || t('common.validateError'));
}
console.log('error', error);
} finally {
setDrawerProps({ confirmLoading: false });
}
}
</script>

View File

@@ -0,0 +1,222 @@
<!--
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author gaoxq
-->
<template>
<div>
<BasicTable @register="registerTable">
<template #tableTitle>
<Icon :icon="getTitle.icon" class="m-1 pr-1" />
<span> {{ getTitle.value }} </span>
</template>
<template #toolbar>
<a-button type="primary" @click="handleForm({})" v-auth="'biz:quickLogin:edit'">
<Icon icon="i-fluent:add-12-filled" /> {{ t('新增') }}
</a-button>
</template>
<template #firstColumn="{ record }">
<a @click="handleForm({ id: record.id })" :title="record.createTime">
{{ record.createTime }}
</a>
</template>
</BasicTable>
<InputForm @register="registerDrawer" @success="handleSuccess" />
</div>
</template>
<script lang="ts" setup name="ViewsBizQuickLoginList">
import { onMounted, ref, unref } from 'vue';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
import { router } from '@jeesite/core/router';
import { Icon } from '@jeesite/core/components/Icon';
import { BasicTable, BasicColumn, useTable } from '@jeesite/core/components/Table';
import { BizQuickLogin, bizQuickLoginList } from '@jeesite/biz/api/biz/quickLogin';
import { bizQuickLoginDelete, bizQuickLoginListData } from '@jeesite/biz/api/biz/quickLogin';
import { useDrawer } from '@jeesite/core/components/Drawer';
import { FormProps } from '@jeesite/core/components/Form';
import InputForm from './form.vue';
const { t } = useI18n('biz.quickLogin');
const { showMessage } = useMessage();
const { meta } = unref(router.currentRoute);
const record = ref<BizQuickLogin>({} as BizQuickLogin);
const getTitle = {
icon: meta.icon || 'i-ant-design:book-outlined',
value: meta.title || t('系统信息管理'),
};
const searchForm: FormProps<BizQuickLogin> = {
baseColProps: { md: 8, lg: 6 },
labelWidth: 90,
schemas: [
{
label: t('创建时间起'),
field: 'createTime_gte',
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD HH:mm',
showTime: { format: 'HH:mm' },
},
},
{
label: t('创建时间止'),
field: 'createTime_lte',
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD HH:mm',
showTime: { format: 'HH:mm' },
},
},
{
label: t('系统名称'),
field: 'systemName',
component: 'Input',
},
],
};
const tableColumns: BasicColumn<BizQuickLogin>[] = [
{
title: t('创建时间'),
dataIndex: 'createTime',
key: 'a.create_time',
sorter: true,
width: 230,
align: 'left',
slot: 'firstColumn',
},
{
title: t('系统名称'),
dataIndex: 'systemName',
key: 'a.system_name',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('首页地址'),
dataIndex: 'homepageUrl',
key: 'a.homepage_url',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('图标类名'),
dataIndex: 'iconClass',
key: 'a.icon_class',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('图标颜色'),
dataIndex: 'iconColor',
key: 'a.icon_color',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('排序序号'),
dataIndex: 'sortOrder',
key: 'a.sort_order',
sorter: true,
width: 130,
align: 'center',
},
{
title: t('图标背景色'),
dataIndex: 'bgColor',
key: 'a.bg_color',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('悬浮遮罩色'),
dataIndex: 'maskColor',
key: 'a.mask_color',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('是否启用'),
dataIndex: 'isEnabled',
key: 'a.is_enabled',
sorter: true,
width: 130,
align: 'center',
dictType: '',
},
{
title: t('更新时间'),
dataIndex: 'updateTime',
key: 'a.update_time',
sorter: true,
width: 130,
align: 'center',
},
];
const actionColumn: BasicColumn<BizQuickLogin> = {
width: 160,
actions: (record: BizQuickLogin) => [
{
icon: 'i-clarity:note-edit-line',
title: t('编辑系统信息'),
onClick: handleForm.bind(this, { id: record.id }),
auth: 'biz:quickLogin:edit',
},
{
icon: 'i-ant-design:delete-outlined',
color: 'error',
title: t('删除系统信息'),
popConfirm: {
title: t('是否确认删除系统信息'),
confirm: handleDelete.bind(this, record),
},
auth: 'biz:quickLogin:edit',
},
],
};
const [registerTable, { reload, getForm }] = useTable<BizQuickLogin>({
api: bizQuickLoginListData,
beforeFetch: (params) => {
return params;
},
columns: tableColumns,
actionColumn: actionColumn,
formConfig: searchForm,
showTableSetting: true,
useSearchForm: true,
canResize: true,
});
onMounted(async () => {
const res = await bizQuickLoginList();
record.value = (res.bizQuickLogin || {}) as BizQuickLogin;
await getForm().setFieldsValue(record.value);
});
const [registerDrawer, { openDrawer }] = useDrawer();
function handleForm(record: Recordable) {
openDrawer(true, record);
}
async function handleDelete(record: Recordable) {
const params = { id: record.id };
const res = await bizQuickLoginDelete(params);
showMessage(res.message);
await handleSuccess(record);
}
async function handleSuccess(record: Recordable) {
await reload({ record });
}
</script>

View File

@@ -0,0 +1,143 @@
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { BasicColumn, BasicTableProps, FormProps } from '@jeesite/core/components/Table';
import { bizQuickLoginListData } from '@jeesite/biz/api/biz/quickLogin';
const { t } = useI18n('biz.quickLogin');
const modalProps = {
title: t('系统信息选择'),
};
const searchForm: FormProps<BizQuickLogin> = {
baseColProps: { md: 8, lg: 6 },
labelWidth: 90,
schemas: [
{
label: t('创建时间起'),
field: 'createTime_gte',
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD HH:mm',
showTime: { format: 'HH:mm' },
},
},
{
label: t('创建时间止'),
field: 'createTime_lte',
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD HH:mm',
showTime: { format: 'HH:mm' },
},
},
{
label: t('系统名称'),
field: 'systemName',
component: 'Input',
},
],
};
const tableColumns: BasicColumn<BizQuickLogin>[] = [
{
title: t('创建时间'),
dataIndex: 'createTime',
key: 'a.create_time',
sorter: true,
width: 230,
align: 'left',
slot: 'firstColumn',
},
{
title: t('系统名称'),
dataIndex: 'systemName',
key: 'a.system_name',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('首页地址'),
dataIndex: 'homepageUrl',
key: 'a.homepage_url',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('图标类名'),
dataIndex: 'iconClass',
key: 'a.icon_class',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('图标颜色'),
dataIndex: 'iconColor',
key: 'a.icon_color',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('排序序号'),
dataIndex: 'sortOrder',
key: 'a.sort_order',
sorter: true,
width: 130,
align: 'center',
},
{
title: t('图标背景色'),
dataIndex: 'bgColor',
key: 'a.bg_color',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('悬浮遮罩色'),
dataIndex: 'maskColor',
key: 'a.mask_color',
sorter: true,
width: 130,
align: 'left',
},
{
title: t('是否启用'),
dataIndex: 'isEnabled',
key: 'a.is_enabled',
sorter: true,
width: 130,
align: 'center',
dictType: '',
},
{
title: t('更新时间'),
dataIndex: 'updateTime',
key: 'a.update_time',
sorter: true,
width: 130,
align: 'center',
},
];
const tableProps: BasicTableProps = {
api: bizQuickLoginListData,
beforeFetch: (params) => {
params['isAll'] = true;
return params;
},
columns: tableColumns,
formConfig: searchForm,
rowKey: 'id',
};
export default {
modalProps,
tableProps,
itemCode: 'id',
itemName: 'id',
isShowCode: false,
};

View File

@@ -29,13 +29,6 @@
<MenuItem value="accountCenter" :text="t('sys.account.center')" icon="i-ion:person-outline" />
<MenuItem value="modifyPwd" :text="t('sys.account.modifyPwd')" icon="i-ant-design:key-outlined" />
<MenuDivider />
<MenuItem
value="doc"
:text="t('layout.header.dropdownItemDoc')"
icon="i-ion:document-text-outline"
v-if="getShowDoc"
/>
<MenuDivider v-if="getShowDoc" />
<MenuItem
v-if="getUseLockPage"
value="lock"

View File

@@ -1,107 +0,0 @@
<template>
<div class="md:flex">
<template v-for="(item, index) in growCardList" :key="item.title">
<Card
size="small"
:loading="loading"
:title="item.title"
class="w-full cursor-pointer !mt-4 md:w-1/4 !md:mt-0"
:class="[index + 1 < 4 && '!md:mr-4']"
:canExpan="false"
@click="navPage(item.url)"
>
<template #extra>
<Tag :color="item.color">{{ item.action }}</Tag>
</template>
<div class="flex justify-between px-4 py-4">
<CountTo prefix="" :startVal="1" :endVal="item.value" class="text-2xl" />
<Icon :icon="item.icon" :size="40" />
</div>
<div class="flex justify-between p-2 px-4">
<span>点击我</span>
<CountTo prefix="共" :startVal="1" :endVal="item.total" />
</div>
</Card>
</template>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { CountTo } from '@jeesite/core/components/CountTo';
import { Icon } from '@jeesite/core/components/Icon';
import { Tag, Card } from 'ant-design-vue';
import { useGo } from '@jeesite/core/hooks/web/usePage';
const loading = ref(true);
const growCardList = ref<GrowCardItem[]>();
const go = useGo();
interface GrowCardItem {
icon: string;
title: string;
value: number;
total: number;
color: string;
action: string;
url: string;
}
onMounted(() => {
const list: GrowCardItem[] = [
{
title: '工作台',
icon: 'icons/visit-count.svg',
value: 1999,
total: 120000,
color: 'green',
action: '时',
url: '/desktop/workbench',
},
{
title: '关于我们',
icon: 'icons/total-sales.svg',
value: 2999,
total: 500000,
color: 'blue',
action: '日',
url: '/desktop/about',
},
{
title: '源码下载',
icon: 'icons/download-count.svg',
value: 3999,
total: 120000,
color: 'orange',
action: '周',
url: 'https://gitee.com/thinkgem/jeesite-vue',
},
{
title: '官方网站',
icon: 'icons/transaction.svg',
value: 9999,
total: 99999,
color: 'purple',
action: '月',
url: 'http://jeesite.com',
},
];
// 此处写后端 API 获取 list 数据
growCardList.value = list;
setTimeout(() => {
loading.value = false;
}, 500);
});
function navPage(url: string) {
if (!url || url === '') {
return;
}
if (url.indexOf('://') != -1) {
window.open(url);
} else {
go(url);
}
}
</script>

View File

@@ -0,0 +1,295 @@
<template>
<div class="quick-login-container">
<!-- 标题区域保留内置图标 -->
<div class="title-bar">
<LoginOutlined class="title-icon" />
<span class="title-text">应用系统</span>
</div>
<!-- 搜索框保留内置图标 -->
<a-input
v-model:value="searchKey"
placeholder="搜索应用系统..."
class="search-input"
size="middle"
>
<template #prefix>
<SearchOutlined class="search-icon" />
</template>
</a-input>
<!-- 应用卡片滚动容器 -->
<div class="app-scroll-container">
<div class="app-card-grid">
<!-- 应用卡片图标为图片地址 -->
<div
v-for="(app, index) in filteredAppList"
:key="index"
class="app-card"
@click="openAppUrl(app.url)"
>
<!-- 图片图标容器 -->
<div class="app-icon-wrapper" :style="{ backgroundColor: app.bgColor }">
<img
:src="app.icon"
:alt="app.name"
class="app-icon"
@error="handleImgError($event)"
/>
</div>
<!-- 应用名称 -->
<div class="app-name">{{ app.name }}</div>
<!-- 悬浮遮罩层 -->
<div class="app-hover-mask" :style="{ background: app.maskColor }">
<span class="hover-text">打开 {{ app.name }}</span>
</div>
</div>
<!-- 空状态提示保留内置图标 -->
<div v-if="filteredAppList.length === 0" class="empty-tip">
<SearchOutlined class="empty-icon" />
<p class="empty-text">未找到匹配的应用</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { Input } from 'ant-design-vue';
import { LoginOutlined, SearchOutlined } from '@ant-design/icons-vue';
// 声明应用列表TS类型已删除fallbackIcon字段
interface AppItem {
name: string;
icon: string; // 应用图标为图片地址
url: string;
bgColor: string; // 图标背景色
maskColor: string; // 悬浮遮罩色
}
const appList = ref<AppItem[]>([]);
// 搜索关键词
const searchKey = ref('');
// 过滤后的应用列表
const filteredAppList = computed(() => {
if (!searchKey.value) return appList.value;
return appList.value.filter(app =>
app.name.toLowerCase().includes(searchKey.value.toLowerCase())
);
});
// 打开应用URL新窗口
const openAppUrl = (url: string) => {
if (!url) return;
window.open(
url,
'_blank'
);
};
// 图片加载失败处理直接使用通用默认图标已删除app参数
const handleImgError = (e: Event) => {
const img = e.target as HTMLImageElement;
// 终极备用图标(通用默认)
img.src = 'http://crontab.club:30012/cApi/images/login-brand.png';
};
</script>
<style scoped>
/* 全局容器 */
.quick-login-container {
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%);
border-radius: 12px;
padding: 24px;
width: 360px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
}
/* 标题区域 */
.title-bar {
display: flex;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.title-icon {
font-size: 20px;
color: #1890ff;
margin-right: 8px;
}
.title-text {
font-size: 18px;
font-weight: 600;
color: #262626;
}
/* 搜索框 */
.search-input {
margin-bottom: 24px;
}
.search-input :deep(.ant-input) {
border-radius: 8px;
padding: 10px 16px;
border: 1px solid #e5e6eb;
transition: all 0.3s ease;
}
.search-input :deep(.ant-input:focus) {
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
border-color: #1890ff;
}
/* 搜索图标样式 */
.search-icon {
font-size: 16px;
color: #86909c;
margin-right: 8px;
}
/* 核心:滚动容器 */
.app-scroll-container {
max-height: 400px;
overflow-y: auto;
padding-right: 4px;
scroll-behavior: smooth;
}
/* 自定义滚动条 */
.app-scroll-container::-webkit-scrollbar {
width: 6px;
}
.app-scroll-container::-webkit-scrollbar-track {
background: #f1f3f5;
border-radius: 3px;
}
.app-scroll-container::-webkit-scrollbar-thumb {
background: #c9cdd4;
border-radius: 3px;
}
.app-scroll-container::-webkit-scrollbar-thumb:hover {
background: #86909c;
}
/* 卡片网格布局 */
.app-card-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
/* 应用卡片 */
.app-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 120px;
background: #ffffff;
border-radius: 10px;
cursor: pointer;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
/* 悬浮动效 */
.app-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
}
/* 图标容器 */
.app-icon-wrapper {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
transition: all 0.3s ease;
}
.app-card:hover .app-icon-wrapper {
transform: scale(1.1);
opacity: 0.9;
}
/* 图片图标样式 */
.app-icon {
width: 24px;
height: 24px;
object-fit: contain; /* 保持图片比例 */
}
/* 应用名称 */
.app-name {
font-size: 14px;
font-weight: 500;
color: #333333;
transition: all 0.3s ease;
}
.app-card:hover .app-name {
color: #1890ff; /* 悬浮时名称变色 */
}
/* 悬浮遮罩层 */
.app-hover-mask {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 0;
display: flex;
align-items: center;
justify-content: center;
transition: height 0.3s ease;
}
.app-card:hover .app-hover-mask {
height: 100%;
}
.hover-text {
color: #ffffff;
font-size: 12px;
opacity: 0;
transition: opacity 0.3s ease 0.1s;
}
.app-card:hover .hover-text {
opacity: 1;
}
/* 空状态样式 */
.empty-tip {
grid-column: span 2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 120px;
color: #86909c;
}
.empty-icon {
font-size: 28px;
margin-bottom: 8px;
}
.empty-text {
font-size: 14px;
margin: 0;
}
</style>

View File

@@ -1,51 +0,0 @@
<template>
<Card title="成交占比" :loading="loading">
<div ref="chartRef" class="h-75 w-full"></div>
</Card>
</template>
<script lang="ts" setup>
import { onMounted, Ref, ref } from 'vue';
import { Card } from 'ant-design-vue';
import { useECharts } from '@jeesite/core/hooks/web/useECharts';
import type { EChartsOption } from 'echarts';
const loading = ref(true);
const chartRef = ref<HTMLDivElement | null>(null);
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
onMounted(() => {
const options: EChartsOption = {
tooltip: {
trigger: 'item',
},
series: [
{
name: '访问来源',
type: 'pie',
radius: '80%',
center: ['50%', '50%'],
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
data: [
{ value: 500, name: '电子产品' },
{ value: 310, name: '服装' },
{ value: 274, name: '化妆品' },
{ value: 400, name: '家居' },
].sort(function (a, b) {
return a.value - b.value;
}),
roseType: 'radius',
animationType: 'scale',
animationEasing: 'exponentialInOut',
animationDelay: function () {
return Math.random() * 400;
},
},
],
};
// 此处写后端 API 获取 options 数据
setTimeout(() => {
setOptions(options);
loading.value = false;
}, 900);
});
</script>

View File

@@ -1,44 +0,0 @@
<template>
<Card
v-bind="$attrs"
:loading="loading"
:tab-list="tabListTitle"
:active-tab-key="activeKey"
@tab-change="onTabChange"
>
<p v-if="activeKey === 'tab1'">
<VisitAnalysis />
</p>
<p v-if="activeKey === 'tab2'">
<VisitAnalysisBar />
</p>
</Card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { Card } from 'ant-design-vue';
import VisitAnalysis from './VisitAnalysis.vue';
import VisitAnalysisBar from './VisitAnalysisBar.vue';
const loading = ref(true);
const activeKey = ref('tab1');
const tabListTitle = [
{
key: 'tab1',
tab: '流量趋势',
},
{
key: 'tab2',
tab: '访问量',
},
];
function onTabChange(key) {
activeKey.value = key;
}
setTimeout(() => {
loading.value = false;
}, 700);
</script>

View File

@@ -1,109 +0,0 @@
<template>
<Skeleton active :paragraph="{ rows: 5 }" :loading="loading">
<div ref="chartRef" class="h-70 w-full"></div>
</Skeleton>
</template>
<script lang="ts" setup>
import { onMounted, ref, Ref } from 'vue';
import { Skeleton } from 'ant-design-vue';
import { useECharts } from '@jeesite/core/hooks/web/useECharts';
import type { EChartsOption } from 'echarts';
const loading = ref(true);
const chartRef = ref<HTMLDivElement | null>(null);
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
onMounted(() => {
const options: EChartsOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
lineStyle: {
width: 1,
color: '#019680',
},
},
},
xAxis: {
type: 'category',
boundaryGap: false,
data: [
'6:00',
'7:00',
'8:00',
'9:00',
'10:00',
'11:00',
'12:00',
'13:00',
'14:00',
'15:00',
'16:00',
'17:00',
'18:00',
'19:00',
'20:00',
'21:00',
'22:00',
'23:00',
],
splitLine: {
show: true,
lineStyle: {
width: 1,
type: 'solid',
color: 'rgba(226,226,226,0.5)',
},
},
axisTick: {
show: false,
},
},
yAxis: [
{
type: 'value',
max: 80000,
splitNumber: 4,
axisTick: {
show: false,
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(255,255,255,0.2)', 'rgba(226,226,226,0.2)'],
},
},
},
],
grid: { left: '1%', right: '1%', top: '2 %', bottom: 0, containLabel: true },
series: [
{
smooth: true,
data: [
111, 222, 4000, 18000, 33333, 55555, 66666, 33333, 14000, 36000, 66666, 44444, 22222, 11111, 4000, 2000,
500, 333, 222, 111,
],
type: 'line',
areaStyle: {},
itemStyle: {
color: '#5ab1ef',
},
},
{
smooth: true,
data: [
33, 66, 88, 333, 3333, 5000, 18000, 3000, 1200, 13000, 22000, 11000, 2221, 1201, 390, 198, 60, 30, 22, 11,
],
type: 'line',
areaStyle: {},
itemStyle: {
color: '#019680',
},
},
],
};
// 此处写后端 API 获取 options 数据
setOptions(options);
loading.value = false;
});
</script>

View File

@@ -1,49 +0,0 @@
<template>
<Skeleton active :paragraph="{ rows: 5 }" :loading="loading">
<div ref="chartRef" class="h-70 w-full"></div>
</Skeleton>
</template>
<script lang="ts" setup>
import { onMounted, ref, Ref } from 'vue';
import { Skeleton } from 'ant-design-vue';
import { useECharts } from '@jeesite/core/hooks/web/useECharts';
import type { EChartsOption } from 'echarts';
const loading = ref(true);
const chartRef = ref<HTMLDivElement | null>(null);
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
onMounted(() => {
const options: EChartsOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
lineStyle: {
width: 1,
color: '#019680',
},
},
},
grid: { left: '1%', right: '1%', top: '2 %', bottom: 0, containLabel: true },
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
},
yAxis: {
type: 'value',
max: 8000,
splitNumber: 4,
},
series: [
{
data: [3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000, 3200, 4800],
type: 'bar',
barMaxWidth: 80,
},
],
};
// 此处写后端 API 获取 options 数据
setOptions(options);
loading.value = false;
});
</script>

View File

@@ -1,89 +0,0 @@
<template>
<Card title="转化率" :loading="loading">
<div ref="chartRef" class="h-75 w-full"></div>
</Card>
</template>
<script lang="ts" setup>
import { onMounted, Ref, ref } from 'vue';
import { Card } from 'ant-design-vue';
import { useECharts } from '@jeesite/core/hooks/web/useECharts';
import type { EChartsOption } from 'echarts';
const loading = ref(true);
const chartRef = ref<HTMLDivElement | null>(null);
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
onMounted(() => {
const options: EChartsOption = {
legend: {
bottom: 0,
data: ['访问', '购买'],
},
tooltip: {},
radar: {
radius: '60%',
splitNumber: 8,
indicator: [
{
name: '电脑',
max: 100,
},
{
name: '充电器',
max: 100,
},
{
name: '耳机',
max: 100,
},
{
name: '手机',
max: 100,
},
{
name: 'Ipad',
max: 100,
},
{
name: '耳机',
max: 100,
},
],
},
series: [
{
type: 'radar',
symbolSize: 0,
areaStyle: {
shadowBlur: 0,
shadowColor: 'rgba(0,0,0,.2)',
shadowOffsetX: 0,
shadowOffsetY: 10,
opacity: 1,
},
data: [
{
value: [90, 50, 86, 40, 50, 20],
name: '访问',
itemStyle: {
color: '#b6a2de',
},
},
{
value: [70, 75, 70, 76, 20, 85],
name: '购买',
itemStyle: {
color: '#5ab1ef',
},
},
],
},
],
};
// 此处写后端 API 获取 options 数据
setTimeout(() => {
setOptions(options);
loading.value = false;
}, 900);
});
</script>

View File

@@ -1,71 +0,0 @@
<template>
<Card title="访问来源" :loading="loading">
<div ref="chartRef" class="h-75 w-full"></div>
</Card>
</template>
<script lang="ts" setup>
import { onMounted, Ref, ref } from 'vue';
import { Card } from 'ant-design-vue';
import { useECharts } from '@jeesite/core/hooks/web/useECharts';
import type { EChartsOption } from 'echarts';
const loading = ref(true);
const chartRef = ref<HTMLDivElement | null>(null);
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
onMounted(() => {
const options: EChartsOption = {
tooltip: {
trigger: 'item',
},
legend: {
bottom: '1%',
left: 'center',
},
series: [
{
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
name: '访问来源',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: '12',
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: [
{ value: 1048, name: '搜索引擎' },
{ value: 735, name: '直接访问' },
{ value: 580, name: '邮件营销' },
{ value: 484, name: '联盟广告' },
],
animationType: 'scale',
animationEasing: 'exponentialInOut',
animationDelay: function () {
return Math.random() * 100;
},
},
],
};
// 此处写后端 API 获取 options 数据
setTimeout(() => {
setOptions(options);
loading.value = false;
}, 900);
});
</script>

View File

@@ -1,18 +1,8 @@
<template>
<div class="mb-4">
<GrowCard class="enter-y" />
<SiteAnalysis class="enter-y !my-4" />
<div class="enter-y md:flex">
<VisitRadar class="w-full md:w-1/3" />
<VisitSource class="w-full !my-4 md:w-1/3 !md:mx-4 !md:my-0" />
<SalesProductPie class="w-full md:w-1/3" />
</div>
<div class="mb-4">
<Home class="enter-y" />
</div>
</template>
<script lang="ts" setup name="Analysis">
import GrowCard from './components/GrowCard.vue';
import SiteAnalysis from './components/SiteAnalysis.vue';
import VisitSource from './components/VisitSource.vue';
import VisitRadar from './components/VisitRadar.vue';
import SalesProductPie from './components/SalesProductPie.vue';
import Home from './components/Home.vue';
</script>