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 NODE_ENV = development
VITE_APP_PLAY_GROUND = http://192.168.52.164:5173 VITE_APP_PLAY_GROUND = "http://192.168.0.49:5173"
VITE_APP_PROXY = "http://192.168.1.206:48085/" VITE_APP_PROXY = "http://192.168.0.49:48085/"

View File

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

View File

@ -10,15 +10,16 @@
"deploy": "pnpm build && npx shy ftp deploy -f ./deploy" "deploy": "pnpm build && npx shy ftp deploy -f ./deploy"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"ant-design-vue": "^4.2.6",
"axios": "^1.1.3", "axios": "^1.1.3",
"moment": "^2.29.4", "dayjs": "^1.11.20",
"monaco-editor": "^0.34.1", "monaco-editor": "^0.34.1",
"pinia": "^2.0.23", "pinia": "^2.0.23",
"vue": "^3.2.41", "vue": "^3.2.41",
"vue-router": "^4.1.6" "vue-router": "^4.1.6"
}, },
"devDependencies": { "devDependencies": {
"@arco-design/web-vue": "^2.38.1",
"@types/node": "^18.11.9", "@types/node": "^18.11.9",
"@vitejs/plugin-vue": "^3.2.0", "@vitejs/plugin-vue": "^3.2.0",
"sass": "^1.56.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"> <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 Header from "@/modules/header/Header.vue";
import { useTheme } from "@/hooks/useTheme";
const { initTheme, themeConfig } = useTheme();
onMounted(() => {
initTheme();
});
</script> </script>
<template> <template>
<Layout class="!h-[calc(100%-60px)] !pt-60px"> <ConfigProvider :theme="themeConfig">
<Header></Header> <Layout class="app-layout">
<LayoutContent class="content"> <Header></Header>
<router-view></router-view> <LayoutContent class="content">
</LayoutContent> <router-view></router-view>
</Layout> </LayoutContent>
</Layout>
</ConfigProvider>
</template> </template>
<style lang="scss"> <style lang="scss">
html, html,
body { body {
height: 100%; height: 100%;
margin: 0;
--color-logo: rgb(85, 26, 139); --color-logo: rgb(85, 26, 139);
--color-logo-1: rgb(237, 221, 249); --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 { #app {
font-family: Avenir, Helvetica, Arial, sans-serif; font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
color: #2c3e50; color: var(--app-text);
height: 100%; height: 100%;
} }
.content { .app-layout {
background-color: var(--color-bg-5); height: 100vh;
height: calc(100% - 60px); padding-top: 56px;
background: var(--app-bg);
} }
body[arco-theme="dark"] { .content {
--color-logo: var(--color-text-1); background-color: var(--app-bg);
--color-logo-1: var(--color-bg-5); 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> </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> <template>
<a href="/" class="logo"> ShyTemplate </a> <a href="/" class="logo"> KT-Template </a>
</template> </template>
<style lang="scss"> <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 App from "@/App.vue";
import { router } from "@/router.js"; 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"); createApp(App).use(router).use(createPinia()).mount("#app");

View File

@ -1,17 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import { DescData, Message, Button, Empty } from "@arco-design/web-vue"; import { Button, Card, Descriptions, DescriptionsItem, Empty, message, Pagination, Popconfirm, Spin, Tooltip } from "ant-design-vue";
import { onBeforeUnmount, onMounted, reactive, ref } from "vue"; import { DeleteOutlined, ShareAltOutlined } from "@ant-design/icons-vue";
import axios from "axios"; import { onBeforeUnmount, onMounted, reactive } from "vue";
import moment from "moment"; import dayjs from "dayjs";
import Bus from "@/bus"; 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 config from "@/config";
import { getComponentDetail, getComponentList, removeComponent } from "@/api/component";
enum HttpStatus { enum HttpStatus {
OK = 200, OK = 200,
} }
interface DescData {
label: string;
value: string;
}
const chartData = reactive<any>({ const chartData = reactive<any>({
chartList: [] as Array<{ chartList: [] as Array<{
title: string; title: string;
@ -28,7 +32,7 @@ const chartData = reactive<any>({
}); });
const convertTime = (timeStr: string) => { const convertTime = (timeStr: string) => {
return moment(timeStr).format("YYYY-MM-DD"); return dayjs(timeStr).format("YYYY-MM-DD");
}; };
const getData = () => { const getData = () => {
@ -44,10 +48,9 @@ const getData = () => {
if (chartData.type) { if (chartData.type) {
params.type = chartData.type; params.type = chartData.type;
} }
axios getComponentList(params)
.get(`${config.axiosBase}/component/list`, { params })
.then((res) => { .then((res) => {
const { code, data, msg } = res.data; const { code, data, msg } = res;
if (code === HttpStatus.OK) { if (code === HttpStatus.OK) {
const { list, total } = data; const { list, total } = data;
chartData.total = total; chartData.total = total;
@ -65,7 +68,7 @@ const getData = () => {
}; };
}); });
} else { } else {
Message.error(msg || "服务器开小差了,请稍后再试..."); message.error(msg || "服务器开小差了,请稍后再试...");
} }
}) })
.finally(() => { .finally(() => {
@ -88,9 +91,9 @@ onBeforeUnmount(() => {
}); });
const chartClick = async (params: string) => { const chartClick = async (params: string) => {
const res = await axios.get(`${config.axiosBase}/component/detail?id=${params}`); const res = await getComponentDetail(params);
const { template, componentType, type, id, name } = res?.data?.data; const { template, componentType, type, id, name } = res.data;
window.open(`${config.playground}/?id=${id}&name=${name}&componentType=${componentType}&type=${type}#${template}`); window.open(buildPlaygroundUrl({ id, name, componentType, type, template }));
}; };
const pageChange = (pageIndex: number) => { const pageChange = (pageIndex: number) => {
@ -99,10 +102,10 @@ const pageChange = (pageIndex: number) => {
}; };
const openTab = async (params: string) => { const openTab = async (params: string) => {
const res = await axios.get(`${config.axiosBase}/component/detail?id=${params}`); const res = await getComponentDetail(params);
const { template, componentType, type, id, name } = res?.data?.data; 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"); const tempInput = document.createElement("textarea");
@ -123,70 +126,94 @@ const openTab = async (params: string) => {
// //
tempInput.remove(); tempInput.remove();
Message.success("分享链接已复制到剪贴板"); message.success("分享链接已复制到剪贴板");
}; };
const handleAdd = () => { 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) => { const handleRemove = async (id: string) => {
try { try {
const { const { code, msg } = await removeComponent(id);
data: { code, msg }, if (code !== HttpStatus.OK) return message.error(msg);
} = await axios.post(`${config.axiosBase}/component/remove?id=${id}`); message.success(msg);
if (code !== HttpStatus.OK) return Message.error(msg);
Message.success(msg);
getData(); getData();
} catch (error) { } catch (error) {
return Message.error(error as any); return message.error(error as any);
} }
}; };
</script> </script>
<template> <template>
<div class="w-full !h-full flex flex-col gap-8px overflow-hidden"> <div class="chart-list">
<div class="ml-auto"> <div class="chart-list_toolbar">
<div class="chart-list_total"> {{ chartData.total }} 个模板</div>
<Button type="primary" @click="handleAdd"> 新增 </Button> <Button type="primary" @click="handleAdd"> 新增 </Button>
</div> </div>
<Spin class="flex-1 overflow-auto" :loading="chartData.loading" tip="加载中,请稍后...">
<Card :bordered="false"> <Spin class="chart-list_body" :spinning="chartData.loading" tip="加载中,请稍后...">
<CardGrid <div class="chart-grid">
v-for="(item, index) in chartData.chartList" <Card v-for="(item, index) in chartData.chartList" class="chart-card" :key="index" hoverable>
class="chart-card" <template #title>
:key="index" <span class="chart-title" @click="chartClick(item.id)">{{ item.title || "-" }}</span>
:style="{ </template>
width: `${config.isMobileApp ? 'calc(100% - 16px)' : 'calc(20% - 16px)'}`, <template #extra>
}" <div class="chart-actions">
> <Tooltip title="复制分享链接">
<Card class="w-full p-10px box-border" :title="item.title || '-'" hoverable> <ShareAltOutlined class="action-icon" @click="openTab(item.id)" />
<template #extra> </Tooltip>
<div class="flex items-center gap-5 justify-center"> <Popconfirm title="确定要删除吗?" @confirm="handleRemove(item.id)">
<IconShareInternal size="20" @click="openTab(item.id)" /> <Tooltip title="删除">
<Popconfirm content="确定要删除吗?" @before-ok="handleRemove(item.id)"> <DeleteOutlined class="action-icon danger" />
<IconDelete size="20" /> </Tooltip>
</Popconfirm> </Popconfirm>
</div> </div>
</template> </template>
<template #cover> <template #cover>
<div class="h-175px w-full" @click="chartClick(item.id)"> <div class="chart-cover" @click="chartClick(item.id)">
<img :src="item.image" class="w-full h-full object-cover" v-if="item.image" /> <img :src="item.image" class="chart-image" v-if="item.image" />
<Empty v-else class="w-full h-full flex justify-center items-center flex-col" description="暂无图片" /> <Empty v-else class="chart-empty" description="暂无图片" />
</div> </div>
</template> </template>
<CardMeta> <Descriptions layout="vertical" :column="3" size="small" @click="chartClick(item.id)">
<template #description> <DescriptionsItem v-for="desc in item.desc" :key="desc.label" :label="desc.label">
<Descriptions :data="item.desc" layout="inline-vertical" :column="3" @click="chartClick(item.id)" /> {{ desc.value }}
</template> </DescriptionsItem>
</CardMeta> </Descriptions>
</Card> </Card>
</CardGrid> </div>
</Card>
</Spin> </Spin>
<div class="pagination mt-15px">
<div class="pagination">
<Pagination <Pagination
:total="chartData.total" :total="chartData.total"
show-total :show-total="(total: number) => `共 ${total} 条`"
@change="pageChange" @change="pageChange"
:disabled="chartData.loading" :disabled="chartData.loading"
:page-size="20" :page-size="20"
@ -196,33 +223,152 @@ const handleRemove = async (id: string) => {
</template> </template>
<style lang="scss" scoped> <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 { .pagination {
position: sticky;
bottom: 0;
z-index: 1;
display: flex; display: flex;
justify-content: end; justify-content: end;
} flex-shrink: 0;
:deep(.arco-card) { margin-top: auto;
background: transparent; padding: 12px 2px 0;
} border-top: 1px solid var(--app-border);
background: var(--app-surface);
: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;
} }
.chart-card { .chart-card {
background: var(--color-bg-2); overflow: hidden;
cursor: pointer; 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"> <script setup lang="ts">
import { RadioGroup, Radio } from "@arco-design/web-vue"; import { RadioButton, RadioGroup } from "ant-design-vue";
import axios from "axios";
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import config from "@/config"; import { getComponentDictByType, getDictByKey, DictItem } from "@/api/dict";
const emits = defineEmits(["change"]); const emits = defineEmits(["change"]);
@ -13,48 +12,79 @@ const handleChange = () => {
}); });
}; };
const typeList = ref<any>([]); const typeList = ref<DictItem[]>([]);
const type = ref<string>(""); const type = ref<string>("");
const componentTypeList = ref<any>([]); const componentTypeList = ref<DictItem[]>([]);
const componentType = ref(""); const componentType = ref("");
const getComponentTypeList = async () => { const getComponentTypeList = async () => {
const list = (await axios.get(`${config.axiosBase}/dict/getComponentDictByType?type=${type.value}`)).data.data; const list = (await getComponentDictByType(type.value)).data;
list.unshift({ label: "全部", value: "" }); componentTypeList.value = [{ label: "全部", value: "" }, ...list];
componentTypeList.value = list;
componentType.value = componentTypeList.value[0]?.value; componentType.value = componentTypeList.value[0]?.value;
handleChange(); handleChange();
}; };
onMounted(async () => { 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; type.value = typeList.value[0]?.value;
getComponentTypeList(); getComponentTypeList();
}); });
</script> </script>
<template> <template>
<RadioGroup <div class="filter-panel">
type="button" <RadioGroup
v-model="type" v-model:value="type"
@change="getComponentTypeList" @change="getComponentTypeList"
size="large" size="default"
class="w-[fit-content] flex flex-wrap" option-type="button"
> button-style="solid"
<Radio v-for="item in typeList" :value="item.value" :key="item.value">{{ item.label }}</Radio> class="filter-row"
</RadioGroup> >
<RadioButton v-for="item in typeList" :value="item.value" :key="item.value">{{ item.label }}</RadioButton>
</RadioGroup>
<RadioGroup <RadioGroup
type="button" v-model:value="componentType"
v-model="componentType" @change="handleChange"
@change="handleChange" size="default"
size="large" option-type="button"
class="w-[fit-content] flex flex-wrap" button-style="solid"
> class="filter-row"
<Radio v-for="item in componentTypeList" :value="item.value" :key="item.value"> >
{{ item.label }} <RadioButton v-for="item in componentTypeList" :value="item.value" :key="item.value">
</Radio> {{ item.label }}
</RadioGroup> </RadioButton>
</RadioGroup>
</div>
</template> </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"> <script setup lang="ts">
import { useRoute } from "vue-router"; 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 { computed, reactive, onBeforeUnmount, onMounted } from "vue";
import Theme from "@/modules/theme/Theme.vue"; import Theme from "@/modules/theme/Theme.vue";
@ -45,15 +45,13 @@ onBeforeUnmount(() => {
<div class="menu"> <div class="menu">
<InputSearch <InputSearch
class="search-input" class="search-input"
v-model="searchData.content" v-model:value="searchData.content"
v-if="showSearch" v-if="showSearch"
placeholder="输入关键词" placeholder="输入关键词"
button-text="搜索" enter-button="搜索"
search-button
@search="searchClick" @search="searchClick"
:loading="searchData.loading" :loading="searchData.loading"
allow-clear allow-clear
@press-enter="searchClick"
/> />
</div> </div>
<Theme class="theme"></Theme> <Theme class="theme"></Theme>
@ -69,24 +67,27 @@ onBeforeUnmount(() => {
left: 0; left: 0;
right: 0; right: 0;
z-index: 999; z-index: 999;
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--app-border);
background-color: var(--color-bg-2); background-color: var(--app-surface);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
} }
.nav-bar_content { .nav-bar_content {
position: relative; position: relative;
display: flex; display: flex;
width: 100%; width: 100%;
height: 60px; height: 56px;
min-height: 60px; min-height: 56px;
max-height: 60px; max-height: 56px;
box-sizing: border-box; box-sizing: border-box;
z-index: 999; z-index: 999;
padding: 0 24px;
} }
.nav-bar_left { .nav-bar_left {
width: 180px; width: 190px;
font-size: 30px; font-size: 26px;
line-height: 1;
} }
.nav-bar_right { .nav-bar_right {
@ -94,14 +95,25 @@ onBeforeUnmount(() => {
display: flex; display: flex;
flex: 1; flex: 1;
align-items: center; align-items: center;
padding-right: 1rem; gap: 16px;
.menu { .menu {
flex: 1; flex: 1;
.search-input { .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"> <script setup lang="ts">
import { onMounted, ref, } from "vue"; import { BulbFilled, BulbOutlined } from "@ant-design/icons-vue";
import { IconSunFill, IconMoonFill } from "@arco-design/web-vue/es/icon"; import { useTheme } from "@/hooks/useTheme";
type ThemeType = 'light' | 'dark'
const size = 24
const theme = ref("light");
const themeChange = (value: ThemeType) => {
theme.value = value;
localStorage.setItem("theme", value);
themeJudge(value);
};
const themeJudge = (theme: ThemeType) => { const { theme, setTheme } = useTheme();
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;
});
</script> </script>
<template> <template>
<IconMoonFill v-if="theme === 'dark'" class="moon theme-icon" :size="size" @click="() => themeChange('light')" /> <BulbFilled v-if="theme === 'dark'" class="moon theme-icon" @click="() => setTheme('light')" />
<IconSunFill v-else class="sun theme-icon" :size="size" @click="() => themeChange('dark')" /> <BulbOutlined v-else class="sun theme-icon" @click="() => setTheme('dark')" />
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.moon { .moon {
color: var(--color-text-1) color: var(--app-text);
} }
.theme-icon { .theme-icon {
cursor: pointer cursor: pointer;
font-size: 24px;
} }
</style> </style>

View File

@ -37,9 +37,9 @@ onBeforeUnmount(() => {
</script> </script>
<template> <template>
<div class="h-full w-full flex flex-col overflow-hidden gap-8px"> <div class="home-page">
<ChartTypeGroup @change="tabChange"></ChartTypeGroup> <ChartTypeGroup @change="tabChange"></ChartTypeGroup>
<div class="flex-1 overflow-hidden"> <div class="home-list">
<ChartList /> <ChartList />
</div> </div>
</div> </div>
@ -47,7 +47,28 @@ onBeforeUnmount(() => {
<style lang="scss"> <style lang="scss">
.content { .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> </style>

View File

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

View File

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

View File

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