mirror of
https://github.com/KwiTsukasa/kt-template-online-web.git
synced 2026-05-27 16:35:47 +08:00
feat: migrate web component list UI
This commit is contained in:
parent
f439349933
commit
1aaafb75a3
@ -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/"
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
4420
pnpm-lock.yaml
4420
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
49
src/App.vue
49
src/App.vue
@ -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">
|
||||
<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
43
src/api/component.ts
Normal 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
14
src/api/dict.ts
Normal 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
25
src/api/request.ts
Normal 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;
|
||||
@ -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
36
src/hooks/useTheme.ts
Normal 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,
|
||||
});
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
<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="flex items-center gap-5 justify-center">
|
||||
<IconShareInternal size="20" @click="openTab(item.id)" />
|
||||
<Popconfirm content="确定要删除吗?" @before-ok="handleRemove(item.id)">
|
||||
<IconDelete size="20" />
|
||||
<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="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 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>
|
||||
<CardMeta>
|
||||
<template #description>
|
||||
<Descriptions :data="item.desc" layout="inline-vertical" :column="3" @click="chartClick(item.id)" />
|
||||
</template>
|
||||
</CardMeta>
|
||||
</Card>
|
||||
</CardGrid>
|
||||
<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>
|
||||
|
||||
@ -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>
|
||||
<div class="filter-panel">
|
||||
<RadioGroup
|
||||
type="button"
|
||||
v-model="type"
|
||||
v-model:value="type"
|
||||
@change="getComponentTypeList"
|
||||
size="large"
|
||||
class="w-[fit-content] flex flex-wrap"
|
||||
size="default"
|
||||
option-type="button"
|
||||
button-style="solid"
|
||||
class="filter-row"
|
||||
>
|
||||
<Radio v-for="item in typeList" :value="item.value" :key="item.value">{{ item.label }}</Radio>
|
||||
<RadioButton v-for="item in typeList" :value="item.value" :key="item.value">{{ item.label }}</RadioButton>
|
||||
</RadioGroup>
|
||||
|
||||
<RadioGroup
|
||||
type="button"
|
||||
v-model="componentType"
|
||||
v-model:value="componentType"
|
||||
@change="handleChange"
|
||||
size="large"
|
||||
class="w-[fit-content] flex flex-wrap"
|
||||
size="default"
|
||||
option-type="button"
|
||||
button-style="solid"
|
||||
class="filter-row"
|
||||
>
|
||||
<Radio v-for="item in componentTypeList" :value="item.value" :key="item.value">
|
||||
<RadioButton v-for="item in componentTypeList" :value="item.value" :key="item.value">
|
||||
{{ item.label }}
|
||||
</Radio>
|
||||
</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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node",
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": [
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
|
||||
@ -9,6 +9,7 @@ export default ({ mode }) => {
|
||||
return defineConfig({
|
||||
plugins: [vue(), UnoCSS()],
|
||||
server: {
|
||||
port: 48088,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: VITE_APP_PROXY,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user