feat: migrate web component list UI

This commit is contained in:
sunlei 2026-05-13 18:16:57 +08:00
parent f439349933
commit 1aaafb75a3
19 changed files with 2999 additions and 2171 deletions

View File

@ -1,4 +1,4 @@
NODE_ENV = depelopment
VITE_APP_PLAY_GROUND = http://192.168.52.164:5173
VITE_APP_PROXY = "http://192.168.1.206:48085/"
NODE_ENV = development
VITE_APP_PLAY_GROUND = "http://192.168.0.49:5173"
VITE_APP_PROXY = "http://192.168.0.49:48085/"

View File

@ -5,7 +5,7 @@
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ShyTemplate - 模版库</title>
<title>KT-Template - 模版库</title>
<meta name="Keywords" content="echarts,Echarts,gallery,makeapie,make a pie,ppchart,PPChart" />
<meta name="description" content="让图表更简单。PPChart 提供 Echarts 收录、图表制作等服务" />
</head>

View File

@ -10,15 +10,16 @@
"deploy": "pnpm build && npx shy ftp deploy -f ./deploy"
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"ant-design-vue": "^4.2.6",
"axios": "^1.1.3",
"moment": "^2.29.4",
"dayjs": "^1.11.20",
"monaco-editor": "^0.34.1",
"pinia": "^2.0.23",
"vue": "^3.2.41",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@arco-design/web-vue": "^2.38.1",
"@types/node": "^18.11.9",
"@vitejs/plugin-vue": "^3.2.0",
"sass": "^1.56.0",

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +1,70 @@
<script setup lang="ts">
import { LayoutContent, Layout } from "@arco-design/web-vue";
import { ConfigProvider, Layout, LayoutContent } from "ant-design-vue";
import { onMounted } from "vue";
import Header from "@/modules/header/Header.vue";
import { useTheme } from "@/hooks/useTheme";
const { initTheme, themeConfig } = useTheme();
onMounted(() => {
initTheme();
});
</script>
<template>
<Layout class="!h-[calc(100%-60px)] !pt-60px">
<Header></Header>
<LayoutContent class="content">
<router-view></router-view>
</LayoutContent>
</Layout>
<ConfigProvider :theme="themeConfig">
<Layout class="app-layout">
<Header></Header>
<LayoutContent class="content">
<router-view></router-view>
</LayoutContent>
</Layout>
</ConfigProvider>
</template>
<style lang="scss">
html,
body {
height: 100%;
margin: 0;
--color-logo: rgb(85, 26, 139);
--color-logo-1: rgb(237, 221, 249);
--app-bg: #f5f5f5;
--app-surface: #ffffff;
--app-border: #f0f0f0;
--app-text: rgba(0, 0, 0, 0.88);
--app-muted: rgba(0, 0, 0, 0.45);
--app-cover-bg: linear-gradient(180deg, #fafafa 0%, #f5f7fb 100%);
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
color: var(--app-text);
height: 100%;
}
.content {
background-color: var(--color-bg-5);
height: calc(100% - 60px);
.app-layout {
height: 100vh;
padding-top: 56px;
background: var(--app-bg);
}
body[arco-theme="dark"] {
--color-logo: var(--color-text-1);
--color-logo-1: var(--color-bg-5);
.content {
background-color: var(--app-bg);
height: calc(100vh - 56px);
overflow: hidden;
}
body[data-theme="dark"] {
--app-bg: #141414;
--app-surface: #1f1f1f;
--app-border: #303030;
--app-text: rgba(255, 255, 255, 0.85);
--app-muted: rgba(255, 255, 255, 0.45);
--app-cover-bg: #262626;
--color-logo: var(--app-text);
--color-logo-1: var(--app-bg);
}
</style>

43
src/api/component.ts Normal file
View File

@ -0,0 +1,43 @@
import { get, post } from "./request";
export interface ComponentListParams {
pageSize: number;
pageNo: number;
type?: string;
componentType?: string;
name?: string;
}
export interface ComponentItem {
id: string;
createTime: string;
componentTypeMsg: string;
name: string;
typeMsg: string;
image: string;
}
export interface ComponentDetail {
id: string;
name: string;
type: string | number;
componentType: string | number;
template: string;
}
export interface ComponentListResult {
list: ComponentItem[];
total: number;
}
export const getComponentList = (params: ComponentListParams) => {
return get<ComponentListResult>("/component/list", { params });
};
export const getComponentDetail = (id: string) => {
return get<ComponentDetail>("/component/detail", { params: { id } });
};
export const removeComponent = (id: string) => {
return post<null>("/component/remove", undefined, { params: { id } });
};

14
src/api/dict.ts Normal file
View File

@ -0,0 +1,14 @@
import { get } from "./request";
export interface DictItem {
label: string;
value: string;
}
export const getComponentDictByType = (type: string) => {
return get<DictItem[]>("/dict/getComponentDictByType", { params: { type } });
};
export const getDictByKey = (dictKey: string) => {
return get<DictItem[]>("/dict/getDictByKey", { params: { dictKey } });
};

25
src/api/request.ts Normal file
View File

@ -0,0 +1,25 @@
import axios, { AxiosRequestConfig } from "axios";
import config from "@/config";
export interface ApiResponse<T = any> {
code: number;
data: T;
msg: string;
}
const request = axios.create({
baseURL: config.axiosBase,
timeout: 1000 * 30,
});
export const get = <T = any>(url: string, config?: AxiosRequestConfig) => {
return request.get<any, ApiResponse<T>>(url, config);
};
export const post = <T = any>(url: string, data?: any, config?: AxiosRequestConfig) => {
return request.post<any, ApiResponse<T>>(url, data, config);
};
request.interceptors.response.use((response) => response.data);
export default request;

View File

@ -1,5 +1,5 @@
<template>
<a href="/" class="logo"> ShyTemplate </a>
<a href="/" class="logo"> KT-Template </a>
</template>
<style lang="scss">

36
src/hooks/useTheme.ts Normal file
View File

@ -0,0 +1,36 @@
import { computed, ref } from "vue";
import { theme as antTheme } from "ant-design-vue";
type ThemeType = "light" | "dark";
const theme = ref<ThemeType>("light");
const applyDocumentTheme = (value: ThemeType) => {
if (value === "dark") {
document.body.setAttribute("data-theme", "dark");
} else {
document.body.removeAttribute("data-theme");
}
};
export const themeConfig = computed(() => ({
algorithm: theme.value === "dark" ? antTheme.darkAlgorithm : antTheme.defaultAlgorithm,
}));
export const setTheme = (value: ThemeType) => {
theme.value = value;
localStorage.setItem("theme", value);
applyDocumentTheme(value);
};
export const initTheme = () => {
const themeValue = (localStorage.getItem("theme") as ThemeType | null) || "light";
setTheme(themeValue);
};
export const useTheme = () => ({
theme,
themeConfig,
setTheme,
initTheme,
});

View File

@ -5,7 +5,6 @@ import "virtual:uno.css";
import App from "@/App.vue";
import { router } from "@/router.js";
import "@arco-design/web-vue/dist/arco.css";
import "ant-design-vue/dist/reset.css";
createApp(App).use(router).use(createPinia()).mount("#app");

View File

@ -1,17 +1,21 @@
<script setup lang="ts">
import { DescData, Message, Button, Empty } from "@arco-design/web-vue";
import { onBeforeUnmount, onMounted, reactive, ref } from "vue";
import axios from "axios";
import moment from "moment";
import { Button, Card, Descriptions, DescriptionsItem, Empty, message, Pagination, Popconfirm, Spin, Tooltip } from "ant-design-vue";
import { DeleteOutlined, ShareAltOutlined } from "@ant-design/icons-vue";
import { onBeforeUnmount, onMounted, reactive } from "vue";
import dayjs from "dayjs";
import Bus from "@/bus";
import { Spin, Card, CardGrid, CardMeta, Pagination, Descriptions, Popconfirm } from "@arco-design/web-vue";
import { IconShareInternal, IconDelete } from "@arco-design/web-vue/es/icon";
import config from "@/config";
import { getComponentDetail, getComponentList, removeComponent } from "@/api/component";
enum HttpStatus {
OK = 200,
}
interface DescData {
label: string;
value: string;
}
const chartData = reactive<any>({
chartList: [] as Array<{
title: string;
@ -28,7 +32,7 @@ const chartData = reactive<any>({
});
const convertTime = (timeStr: string) => {
return moment(timeStr).format("YYYY-MM-DD");
return dayjs(timeStr).format("YYYY-MM-DD");
};
const getData = () => {
@ -44,10 +48,9 @@ const getData = () => {
if (chartData.type) {
params.type = chartData.type;
}
axios
.get(`${config.axiosBase}/component/list`, { params })
getComponentList(params)
.then((res) => {
const { code, data, msg } = res.data;
const { code, data, msg } = res;
if (code === HttpStatus.OK) {
const { list, total } = data;
chartData.total = total;
@ -65,7 +68,7 @@ const getData = () => {
};
});
} else {
Message.error(msg || "服务器开小差了,请稍后再试...");
message.error(msg || "服务器开小差了,请稍后再试...");
}
})
.finally(() => {
@ -88,9 +91,9 @@ onBeforeUnmount(() => {
});
const chartClick = async (params: string) => {
const res = await axios.get(`${config.axiosBase}/component/detail?id=${params}`);
const { template, componentType, type, id, name } = res?.data?.data;
window.open(`${config.playground}/?id=${id}&name=${name}&componentType=${componentType}&type=${type}#${template}`);
const res = await getComponentDetail(params);
const { template, componentType, type, id, name } = res.data;
window.open(buildPlaygroundUrl({ id, name, componentType, type, template }));
};
const pageChange = (pageIndex: number) => {
@ -99,10 +102,10 @@ const pageChange = (pageIndex: number) => {
};
const openTab = async (params: string) => {
const res = await axios.get(`${config.axiosBase}/component/detail?id=${params}`);
const { template, componentType, type, id, name } = res?.data?.data;
const res = await getComponentDetail(params);
const { template, componentType, type, id, name } = res.data;
const url = `${config.playground}/?id=${id}&name=${name}&componentType=${componentType}&type=${type}#${template}`;
const url = buildPlaygroundUrl({ id, name, componentType, type, template });
const tempInput = document.createElement("textarea");
@ -123,70 +126,94 @@ const openTab = async (params: string) => {
//
tempInput.remove();
Message.success("分享链接已复制到剪贴板");
message.success("分享链接已复制到剪贴板");
};
const handleAdd = () => {
window.open(`${config.playground}?type=${chartData.type}&componentType=${chartData.componentType}`);
window.open(
buildPlaygroundUrl({
type: chartData.type,
componentType: chartData.componentType,
})
);
};
const buildPlaygroundUrl = (params: {
id?: string;
name?: string;
type?: string | number;
componentType?: string | number;
template?: string;
}) => {
const query = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (key === "template" || value === undefined || value === null || value === "") return;
query.set(key, String(value));
});
const search = query.toString() ? `?${query.toString()}` : "";
const hash = params.template ? `#${params.template}` : "";
return `${config.playground}/${search}${hash}`;
};
const handleRemove = async (id: string) => {
try {
const {
data: { code, msg },
} = await axios.post(`${config.axiosBase}/component/remove?id=${id}`);
if (code !== HttpStatus.OK) return Message.error(msg);
Message.success(msg);
const { code, msg } = await removeComponent(id);
if (code !== HttpStatus.OK) return message.error(msg);
message.success(msg);
getData();
} catch (error) {
return Message.error(error as any);
return message.error(error as any);
}
};
</script>
<template>
<div class="w-full !h-full flex flex-col gap-8px overflow-hidden">
<div class="ml-auto">
<div class="chart-list">
<div class="chart-list_toolbar">
<div class="chart-list_total"> {{ chartData.total }} 个模板</div>
<Button type="primary" @click="handleAdd"> 新增 </Button>
</div>
<Spin class="flex-1 overflow-auto" :loading="chartData.loading" tip="加载中,请稍后...">
<Card :bordered="false">
<CardGrid
v-for="(item, index) in chartData.chartList"
class="chart-card"
:key="index"
:style="{
width: `${config.isMobileApp ? 'calc(100% - 16px)' : 'calc(20% - 16px)'}`,
}"
>
<Card class="w-full p-10px box-border" :title="item.title || '-'" hoverable>
<template #extra>
<div class="flex items-center gap-5 justify-center">
<IconShareInternal size="20" @click="openTab(item.id)" />
<Popconfirm content="确定要删除吗?" @before-ok="handleRemove(item.id)">
<IconDelete size="20" />
</Popconfirm>
</div>
</template>
<template #cover>
<div class="h-175px w-full" @click="chartClick(item.id)">
<img :src="item.image" class="w-full h-full object-cover" v-if="item.image" />
<Empty v-else class="w-full h-full flex justify-center items-center flex-col" description="暂无图片" />
</div>
</template>
<CardMeta>
<template #description>
<Descriptions :data="item.desc" layout="inline-vertical" :column="3" @click="chartClick(item.id)" />
</template>
</CardMeta>
</Card>
</CardGrid>
</Card>
<Spin class="chart-list_body" :spinning="chartData.loading" tip="加载中,请稍后...">
<div class="chart-grid">
<Card v-for="(item, index) in chartData.chartList" class="chart-card" :key="index" hoverable>
<template #title>
<span class="chart-title" @click="chartClick(item.id)">{{ item.title || "-" }}</span>
</template>
<template #extra>
<div class="chart-actions">
<Tooltip title="复制分享链接">
<ShareAltOutlined class="action-icon" @click="openTab(item.id)" />
</Tooltip>
<Popconfirm title="确定要删除吗?" @confirm="handleRemove(item.id)">
<Tooltip title="删除">
<DeleteOutlined class="action-icon danger" />
</Tooltip>
</Popconfirm>
</div>
</template>
<template #cover>
<div class="chart-cover" @click="chartClick(item.id)">
<img :src="item.image" class="chart-image" v-if="item.image" />
<Empty v-else class="chart-empty" description="暂无图片" />
</div>
</template>
<Descriptions layout="vertical" :column="3" size="small" @click="chartClick(item.id)">
<DescriptionsItem v-for="desc in item.desc" :key="desc.label" :label="desc.label">
{{ desc.value }}
</DescriptionsItem>
</Descriptions>
</Card>
</div>
</Spin>
<div class="pagination mt-15px">
<div class="pagination">
<Pagination
:total="chartData.total"
show-total
:show-total="(total: number) => `共 ${total} 条`"
@change="pageChange"
:disabled="chartData.loading"
:page-size="20"
@ -196,33 +223,152 @@ const handleRemove = async (id: string) => {
</template>
<style lang="scss" scoped>
.chart-list {
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
width: 100%;
height: 100%;
min-height: 0;
padding: 16px;
box-sizing: border-box;
border: 1px solid var(--app-border);
border-radius: 8px;
background: var(--app-surface);
}
.chart-list_toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 2px;
}
.chart-list_total {
color: var(--app-muted);
font-size: 13px;
}
.chart-list_body {
flex: 1;
min-height: 0;
overflow: hidden;
}
:deep(.chart-list_body.ant-spin-nested-loading),
:deep(.chart-list_body .ant-spin-container) {
height: 100%;
min-height: 0;
}
:deep(.chart-list_body .ant-spin-container) {
overflow: auto;
padding: 2px;
}
.chart-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 16px;
align-content: start;
}
.pagination {
position: sticky;
bottom: 0;
z-index: 1;
display: flex;
justify-content: end;
}
:deep(.arco-card) {
background: transparent;
}
:deep(.arco-card-body) {
box-sizing: border-box;
margin: 0 !important;
padding: 0 2px 0 0 !important;
width: 100%;
gap: 16px;
}
.arco-card-grid {
box-shadow: unset !important;
}
::-webkit-scrollbar {
display: none;
flex-shrink: 0;
margin-top: auto;
padding: 12px 2px 0;
border-top: 1px solid var(--app-border);
background: var(--app-surface);
}
.chart-card {
background: var(--color-bg-2);
overflow: hidden;
cursor: pointer;
}
</style>
:deep(.chart-card .ant-card-head) {
min-height: 44px;
padding: 0 16px;
border-bottom-color: var(--app-border);
}
:deep(.chart-card .ant-card-body) {
padding: 14px 16px 16px;
}
.chart-title {
display: block;
overflow: hidden;
color: var(--app-text);
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.chart-actions {
display: flex;
align-items: center;
gap: 12px;
color: var(--app-muted);
}
.chart-cover {
display: flex;
align-items: center;
justify-content: center;
height: 160px;
border-bottom: 1px solid var(--app-border);
background: var(--app-cover-bg);
}
.chart-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.chart-empty {
width: 100%;
height: 100%;
}
:deep(.ant-descriptions-item-label) {
color: var(--app-muted);
font-size: 12px;
}
:deep(.ant-descriptions-item-content) {
color: var(--app-text);
font-size: 13px;
}
:deep(.ant-descriptions-row > th),
:deep(.ant-descriptions-row > td) {
padding-bottom: 0 !important;
}
.action-icon {
font-size: 20px;
transition: color 0.2s;
}
.action-icon:hover {
color: #1677ff;
}
.action-icon.danger:hover {
color: #ff4d4f;
}
@media (max-width: 768px) {
.chart-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -1,8 +1,7 @@
<script setup lang="ts">
import { RadioGroup, Radio } from "@arco-design/web-vue";
import axios from "axios";
import { RadioButton, RadioGroup } from "ant-design-vue";
import { onMounted, ref } from "vue";
import config from "@/config";
import { getComponentDictByType, getDictByKey, DictItem } from "@/api/dict";
const emits = defineEmits(["change"]);
@ -13,48 +12,79 @@ const handleChange = () => {
});
};
const typeList = ref<any>([]);
const typeList = ref<DictItem[]>([]);
const type = ref<string>("");
const componentTypeList = ref<any>([]);
const componentTypeList = ref<DictItem[]>([]);
const componentType = ref("");
const getComponentTypeList = async () => {
const list = (await axios.get(`${config.axiosBase}/dict/getComponentDictByType?type=${type.value}`)).data.data;
list.unshift({ label: "全部", value: "" });
componentTypeList.value = list;
const list = (await getComponentDictByType(type.value)).data;
componentTypeList.value = [{ label: "全部", value: "" }, ...list];
componentType.value = componentTypeList.value[0]?.value;
handleChange();
};
onMounted(async () => {
typeList.value = (await axios.get(`${config.axiosBase}/dict/getDictByKey?dictKey=COMPONENT_TYPE`)).data.data;
typeList.value = (await getDictByKey("COMPONENT_TYPE")).data;
type.value = typeList.value[0]?.value;
getComponentTypeList();
});
</script>
<template>
<RadioGroup
type="button"
v-model="type"
@change="getComponentTypeList"
size="large"
class="w-[fit-content] flex flex-wrap"
>
<Radio v-for="item in typeList" :value="item.value" :key="item.value">{{ item.label }}</Radio>
</RadioGroup>
<div class="filter-panel">
<RadioGroup
v-model:value="type"
@change="getComponentTypeList"
size="default"
option-type="button"
button-style="solid"
class="filter-row"
>
<RadioButton v-for="item in typeList" :value="item.value" :key="item.value">{{ item.label }}</RadioButton>
</RadioGroup>
<RadioGroup
type="button"
v-model="componentType"
@change="handleChange"
size="large"
class="w-[fit-content] flex flex-wrap"
>
<Radio v-for="item in componentTypeList" :value="item.value" :key="item.value">
{{ item.label }}
</Radio>
</RadioGroup>
<RadioGroup
v-model:value="componentType"
@change="handleChange"
size="default"
option-type="button"
button-style="solid"
class="filter-row"
>
<RadioButton v-for="item in componentTypeList" :value="item.value" :key="item.value">
{{ item.label }}
</RadioButton>
</RadioGroup>
</div>
</template>
<style lang="scss" scoped>
.filter-panel {
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 16px;
border: 1px solid var(--app-border);
border-radius: 8px;
background: var(--app-surface);
}
.filter-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
:deep(.ant-radio-button-wrapper) {
height: 30px;
line-height: 28px;
border-inline-start-width: 1px;
border-radius: 6px;
}
:deep(.ant-radio-button-wrapper::before) {
display: none;
}
</style>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useRoute } from "vue-router";
import { InputSearch } from "@arco-design/web-vue";
import { InputSearch } from "ant-design-vue";
import { computed, reactive, onBeforeUnmount, onMounted } from "vue";
import Theme from "@/modules/theme/Theme.vue";
@ -45,15 +45,13 @@ onBeforeUnmount(() => {
<div class="menu">
<InputSearch
class="search-input"
v-model="searchData.content"
v-model:value="searchData.content"
v-if="showSearch"
placeholder="输入关键词"
button-text="搜索"
search-button
enter-button="搜索"
@search="searchClick"
:loading="searchData.loading"
allow-clear
@press-enter="searchClick"
/>
</div>
<Theme class="theme"></Theme>
@ -69,24 +67,27 @@ onBeforeUnmount(() => {
left: 0;
right: 0;
z-index: 999;
border-bottom: 1px solid var(--color-border);
background-color: var(--color-bg-2);
border-bottom: 1px solid var(--app-border);
background-color: var(--app-surface);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.nav-bar_content {
position: relative;
display: flex;
width: 100%;
height: 60px;
min-height: 60px;
max-height: 60px;
height: 56px;
min-height: 56px;
max-height: 56px;
box-sizing: border-box;
z-index: 999;
padding: 0 24px;
}
.nav-bar_left {
width: 180px;
font-size: 30px;
width: 190px;
font-size: 26px;
line-height: 1;
}
.nav-bar_right {
@ -94,14 +95,25 @@ onBeforeUnmount(() => {
display: flex;
flex: 1;
align-items: center;
padding-right: 1rem;
gap: 16px;
.menu {
flex: 1;
.search-input {
width: 320px;
width: 360px;
max-width: 100%;
}
}
}
</style>
@media (max-width: 768px) {
.nav-bar_content {
padding: 0 12px;
}
.nav-bar_left {
width: 150px;
font-size: 22px;
}
}
</style>

View File

@ -1,43 +1,22 @@
<script setup lang="ts">
import { onMounted, ref, } from "vue";
import { IconSunFill, IconMoonFill } from "@arco-design/web-vue/es/icon";
type ThemeType = 'light' | 'dark'
const size = 24
const theme = ref("light");
const themeChange = (value: ThemeType) => {
theme.value = value;
localStorage.setItem("theme", value);
themeJudge(value);
};
import { BulbFilled, BulbOutlined } from "@ant-design/icons-vue";
import { useTheme } from "@/hooks/useTheme";
const themeJudge = (theme: ThemeType) => {
if (theme === "dark") {
document.body.setAttribute("arco-theme", "dark");
} else {
document.body.removeAttribute("arco-theme");
}
};
//
onMounted(() => {
//
const themeValue = localStorage.getItem("theme") as (ThemeType | null) || "light";
themeJudge(themeValue);
theme.value = themeValue;
});
const { theme, setTheme } = useTheme();
</script>
<template>
<IconMoonFill v-if="theme === 'dark'" class="moon theme-icon" :size="size" @click="() => themeChange('light')" />
<IconSunFill v-else class="sun theme-icon" :size="size" @click="() => themeChange('dark')" />
<BulbFilled v-if="theme === 'dark'" class="moon theme-icon" @click="() => setTheme('light')" />
<BulbOutlined v-else class="sun theme-icon" @click="() => setTheme('dark')" />
</template>
<style lang="scss" scoped>
.moon {
color: var(--color-text-1)
color: var(--app-text);
}
.theme-icon {
cursor: pointer
cursor: pointer;
font-size: 24px;
}
</style>
</style>

View File

@ -37,9 +37,9 @@ onBeforeUnmount(() => {
</script>
<template>
<div class="h-full w-full flex flex-col overflow-hidden gap-8px">
<div class="home-page">
<ChartTypeGroup @change="tabChange"></ChartTypeGroup>
<div class="flex-1 overflow-hidden">
<div class="home-list">
<ChartList />
</div>
</div>
@ -47,7 +47,28 @@ onBeforeUnmount(() => {
<style lang="scss">
.content {
padding: 20px 30px;
padding: 16px 24px 20px;
}
.home-page {
display: flex;
flex-direction: column;
gap: 14px;
width: 100%;
height: 100%;
overflow: hidden;
}
.home-list {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
}
@media (max-width: 768px) {
.content {
padding: 12px;
}
}
</style>

View File

@ -6,6 +6,7 @@
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"moduleResolution": "node",
"isolatedModules": true,
"esModuleInterop": true,
"lib": [

View File

@ -2,7 +2,6 @@
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]

View File

@ -9,6 +9,7 @@ export default ({ mode }) => {
return defineConfig({
plugins: [vue(), UnoCSS()],
server: {
port: 48088,
proxy: {
"/api": {
target: VITE_APP_PROXY,