mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-13 06:40:59 +00:00
feat(frontend): Phase 5b — port four shared components to Vue 3
CustomStatistic.vue and SettingListItem.vue are mechanical
Vue.component → SFC ports.
AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the
five sidebar icons (dashboard/user/setting/tool/logout) live in a
name→component map and render via <component :is>. The legacy
<a-drawer slot="handle"> hack is replaced with a sibling fixed-
position toggle button. Tab paths take basePath/requestUri as
props instead of pulling them from Go template scope.
TableSortable.vue: the biggest Vue 3 rewrite of this phase.
- $listeners is gone — replaced by inheritAttrs: false +
explicit attrs forwarding
- scopedSlots: this.$scopedSlots collapsed into Vue 3's unified
slots object — just iterate Object.keys(this.slots) and forward
- Vue 2 h(tag, { props, on, scopedSlots }, children) →
Vue 3 h(tag, { ...props, ...on }, slotsObject)
- 'a-table' string → resolveComponent('a-table') so app.use(Antd)
registration is honored
- inject: ['sortable'] (Options API) → inject('sortable', null)
(Composition API) inside the trigger child
- beforeDestroy → beforeUnmount
- customRow's return shape flattened (no nested props/on/class)
Two intentional skips, documented in the migration doc:
- aClientTable.html — slot fragments, not a component. Migrates
inline with inbounds.html (new Phase 5f).
- aPersianDatepicker.html — wraps a Persian-only third-party
lib; defer until settings.html lands.
Build verified with vite 8.0.11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import {
|
||||
DashboardOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
ToolOutlined,
|
||||
LogoutOutlined,
|
||||
CloseOutlined,
|
||||
MenuFoldOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
import { currentTheme } from '@/composables/useTheme.js';
|
||||
import ThemeSwitch from '@/components/ThemeSwitch.vue';
|
||||
|
||||
const SIDEBAR_COLLAPSED_KEY = 'isSidebarCollapsed';
|
||||
|
||||
const props = defineProps({
|
||||
// Path prefix (e.g. /custom-base/) the panel is served under. Defaults
|
||||
// to '' which means tab keys end up as '/panel/...'. Pages pass the
|
||||
// value the Go backend gave them (in production via a meta tag).
|
||||
basePath: { type: String, default: '' },
|
||||
// Current request URI so the matching menu item highlights.
|
||||
requestUri: { type: String, default: '' },
|
||||
});
|
||||
|
||||
// AD-Vue 4 dropped <a-icon :type="x"> in favor of explicit icon
|
||||
// imports — keep a small name-to-component map so tab definitions stay
|
||||
// declarative.
|
||||
const iconByName = {
|
||||
dashboard: DashboardOutlined,
|
||||
user: UserOutlined,
|
||||
setting: SettingOutlined,
|
||||
tool: ToolOutlined,
|
||||
logout: LogoutOutlined,
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ key: `${props.basePath}panel/`, icon: 'dashboard', title: 'Dashboard' },
|
||||
{ key: `${props.basePath}panel/inbounds`, icon: 'user', title: 'Inbounds' },
|
||||
{ key: `${props.basePath}panel/settings`, icon: 'setting', title: 'Settings' },
|
||||
{ key: `${props.basePath}panel/xray`, icon: 'tool', title: 'Xray' },
|
||||
{ key: `${props.basePath}logout/`, icon: 'logout', title: 'Logout' },
|
||||
];
|
||||
|
||||
const activeTab = ref([props.requestUri]);
|
||||
|
||||
const drawerOpen = ref(false);
|
||||
const collapsed = ref(JSON.parse(localStorage.getItem(SIDEBAR_COLLAPSED_KEY) || 'false'));
|
||||
|
||||
function openLink(key) {
|
||||
if (key.startsWith('http')) {
|
||||
window.open(key);
|
||||
} else {
|
||||
window.location.href = key;
|
||||
}
|
||||
}
|
||||
|
||||
function onCollapse(isCollapsed, type) {
|
||||
// Only persist explicit toggle clicks, not breakpoint-triggered collapses.
|
||||
if (type === 'clickTrigger') {
|
||||
localStorage.setItem(SIDEBAR_COLLAPSED_KEY, isCollapsed);
|
||||
collapsed.value = isCollapsed;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDrawer() {
|
||||
drawerOpen.value = !drawerOpen.value;
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
drawerOpen.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ant-sidebar">
|
||||
<a-layout-sider
|
||||
:theme="currentTheme"
|
||||
collapsible
|
||||
:collapsed="collapsed"
|
||||
breakpoint="md"
|
||||
@collapse="onCollapse"
|
||||
>
|
||||
<ThemeSwitch />
|
||||
<a-menu
|
||||
:theme="currentTheme"
|
||||
mode="inline"
|
||||
:selected-keys="activeTab"
|
||||
@click="({ key }) => openLink(key)"
|
||||
>
|
||||
<a-menu-item v-for="tab in tabs" :key="tab.key">
|
||||
<component :is="iconByName[tab.icon]" />
|
||||
<span>{{ tab.title }}</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
|
||||
<a-drawer
|
||||
placement="left"
|
||||
:closable="false"
|
||||
:open="drawerOpen"
|
||||
:wrap-class-name="currentTheme"
|
||||
:wrap-style="{ padding: 0 }"
|
||||
:style="{ height: '100%' }"
|
||||
@close="closeDrawer"
|
||||
>
|
||||
<ThemeSwitch />
|
||||
<a-menu
|
||||
:theme="currentTheme"
|
||||
mode="inline"
|
||||
:selected-keys="activeTab"
|
||||
@click="({ key }) => openLink(key)"
|
||||
>
|
||||
<a-menu-item v-for="tab in tabs" :key="tab.key">
|
||||
<component :is="iconByName[tab.icon]" />
|
||||
<span>{{ tab.title }}</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-drawer>
|
||||
|
||||
<button class="drawer-handle" type="button" @click="toggleDrawer">
|
||||
<CloseOutlined v-if="drawerOpen" />
|
||||
<MenuFoldOutlined v-else />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ant-sidebar > .ant-layout-sider {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.drawer-handle {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
z-index: 1100;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
border: none;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.drawer-handle {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user