admin panel beter ui
This commit is contained in:
@@ -98,3 +98,22 @@ body #app .ant-card {
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 25px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
// ── Full-height list pages ──────────────────────────────────────────
|
||||
// Main.vue stretches #content-area/.content-area__content to fill the
|
||||
// viewport below the navbar; the routed page's own root needs to pick that
|
||||
// up so a short table doesn't leave a gray gap under its card. Most list
|
||||
// pages render <div id="page-user-list"><a-card>...</a-card></div>, a few
|
||||
// (e.g. FacilityList.vue) render <a-card> directly — both are covered here.
|
||||
.content-area__content > * {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
#page-user-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#page-user-list > .ant-card,
|
||||
.content-area__content > .ant-card {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
<template>
|
||||
<!--
|
||||
Standard header for every list page: the section title plus, when the
|
||||
caller has one, an "add" button ABOVE the list — same rid the create
|
||||
route used to gate on, so permissions are not re-derived by hand.
|
||||
Modelled on the one place this already existed (ListUser.vue's add-user
|
||||
button) and generalised so every list page looks the same.
|
||||
Standard "add" button for every list page — same rid the create route
|
||||
used to gate on, so permissions are not re-derived by hand. Rendered
|
||||
inline inside XTable's toolbar (via its #add slot), aligned with the
|
||||
search bar, rather than in a row of its own above the table.
|
||||
-->
|
||||
<div class="list-page-header">
|
||||
<a-button v-if="!rid || hasPermission(rid)" type="primary" @click="$emit('add')">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
{{ addLabel }}
|
||||
</a-button>
|
||||
</div>
|
||||
<a-button v-if="!rid || hasPermission(rid)" type="primary" @click="$emit('add')">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
{{ addLabel }}
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -32,11 +29,3 @@ defineEmits<{ (e: 'add'): void }>();
|
||||
const users = useUsersStore();
|
||||
const hasPermission = (id: string) => Boolean(users.hasPermission(id));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-page-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,33 +13,55 @@
|
||||
-->
|
||||
<div class="x-table">
|
||||
<div class="x-table__toolbar">
|
||||
<a-input-search
|
||||
v-if="!model._hideSearch"
|
||||
v-model:value="query.search"
|
||||
placeholder="جستجو"
|
||||
allow-clear
|
||||
style="max-width: 260px"
|
||||
@search="onSearch"
|
||||
/>
|
||||
<div class="x-table__search-group">
|
||||
<a-input-search
|
||||
v-if="!model._hideSearch"
|
||||
v-model:value="query.search"
|
||||
placeholder="جستجو"
|
||||
allow-clear
|
||||
style="max-width: 260px"
|
||||
@search="onSearch"
|
||||
/>
|
||||
|
||||
<div class="x-table__actions">
|
||||
<a-button v-if="!model._noFilters" :type="activeFilter ? 'primary' : 'default'" @click="toggleVisibleFilter">
|
||||
فیلتر
|
||||
<a-badge v-if="query.filters.length" :count="query.filters.length" />
|
||||
</a-button>
|
||||
<div class="x-table__actions">
|
||||
<a-badge v-if="!model._noFilters" :count="query.filters.length">
|
||||
<a-button type="text" :class="{ 'x-table__action--active': activeFilter }" @click="toggleVisibleFilter">
|
||||
فیلتر
|
||||
<template #icon><FilterOutlined /></template>
|
||||
</a-button>
|
||||
</a-badge>
|
||||
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button>ستونها</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item v-for="col in cols" :key="col.key">
|
||||
<a-checkbox v-model:checked="col.show">{{ col.title }}</a-checkbox>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-tooltip title="چاپ">
|
||||
<a-button type="text" shape="circle">
|
||||
<template #icon><PrinterOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<a-button class="refresh-btn" :loading="loading" @click="fetch()">بارگذاری مجدد</a-button>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-tooltip title="ستونها">
|
||||
<a-button type="text" shape="circle">
|
||||
<template #icon><SettingOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item v-for="col in cols" :key="col.key">
|
||||
<a-checkbox v-model:checked="col.show">{{ col.title }}</a-checkbox>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
<a-tooltip title="بارگذاری مجدد">
|
||||
<a-button class="refresh-btn" type="text" shape="circle" :loading="loading" @click="fetch()">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.add" class="x-table__add">
|
||||
<slot name="add" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -115,6 +137,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { FilterOutlined, PrinterOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons-vue';
|
||||
import api from '../../api/client';
|
||||
import type { XTbl } from './model';
|
||||
|
||||
@@ -336,11 +359,24 @@ defineExpose({ fetch });
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.x-table__search-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.x-table__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.x-table__action--active {
|
||||
color: #1677ff;
|
||||
}
|
||||
.x-table__add {
|
||||
display: flex;
|
||||
}
|
||||
.x-table__filters {
|
||||
background: #fafafa;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<!--
|
||||
Recursive renderer for one navMenuItems.ts node, used by VerticalNavMenu.vue.
|
||||
Extracted so a submenu can itself contain a submenu (e.g. "پیکربندی" nesting
|
||||
"کیف پول" nesting "لیست کیف پولهای شارژ شده") — ant-design-vue's a-sub-menu
|
||||
nests natively, the fixed two-level template just didn't recurse into it.
|
||||
-->
|
||||
<a-sub-menu v-if="node.submenu" :key="menuKey">
|
||||
<template #icon><component :is="resolveIcon(node.icon)" v-if="node.icon" /></template>
|
||||
<template #title>{{ node.i18n || node.name }}</template>
|
||||
<NavMenuNode
|
||||
v-for="(child, k) in visibleChildren(node.submenu)"
|
||||
:key="child.url || `${menuKey}-${k}`"
|
||||
:node="child"
|
||||
:menu-key="child.url || `${menuKey}-${k}`"
|
||||
/>
|
||||
</a-sub-menu>
|
||||
|
||||
<a-menu-item v-else :key="menuKey">
|
||||
<template #icon><component :is="resolveIcon(node.icon)" v-if="node.icon" /></template>
|
||||
{{ node.i18n || node.name }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUsersStore } from '../../stores/users';
|
||||
import { resolveIcon } from '../../components/icons';
|
||||
import type { NavMenuNode as NavMenuNodeType } from '../navMenuItems';
|
||||
|
||||
defineProps<{ node: NavMenuNodeType; menuKey: string }>();
|
||||
|
||||
const users = useUsersStore();
|
||||
|
||||
// Same visibility rule as VerticalNavMenu.vue: no permissionId = always visible.
|
||||
function canSee(node: NavMenuNodeType): boolean {
|
||||
if (!node.permissionId) return true;
|
||||
return Boolean(users.hasPermission(node.permissionId));
|
||||
}
|
||||
|
||||
function visibleChildren(nodes?: NavMenuNodeType[]): NavMenuNodeType[] {
|
||||
return (nodes ?? []).filter(canSee);
|
||||
}
|
||||
|
||||
defineOptions({ name: 'NavMenuNode' });
|
||||
</script>
|
||||
@@ -13,6 +13,14 @@
|
||||
<MenuFoldOutlined v-else />
|
||||
</a-button>
|
||||
<span class="page-title">{{ pageTitle }}</span>
|
||||
<a-breadcrumb v-if="breadcrumb" class="breadcrumb">
|
||||
<a-breadcrumb-item v-for="(crumb, i) in breadcrumb" :key="i">
|
||||
<router-link v-if="crumb.url && !crumb.active" :to="crumb.url">
|
||||
{{ crumb.title }}
|
||||
</router-link>
|
||||
<span v-else>{{ crumb.title }}</span>
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
</div>
|
||||
|
||||
<div class="nav-right">
|
||||
@@ -43,6 +51,12 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons-vue';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
|
||||
interface Crumb {
|
||||
title: string;
|
||||
url?: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
defineProps<{ collapsed?: boolean }>();
|
||||
defineEmits<{ (e: 'toggle-sidebar'): void }>();
|
||||
|
||||
@@ -51,6 +65,7 @@ const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
|
||||
const pageTitle = computed(() => (route.meta?.pageTitle as string) ?? '');
|
||||
const breadcrumb = computed(() => route.meta?.breadcrumb as Crumb[] | undefined);
|
||||
const displayName = computed(() => auth.user?.name ?? auth.user?.fullName ?? 'کاربر');
|
||||
const initial = computed(() => displayName.value.trim().charAt(0) || 'ک');
|
||||
|
||||
@@ -80,6 +95,9 @@ function onLogout() {
|
||||
font-size: 16px;
|
||||
color: #1c2d56;
|
||||
}
|
||||
.breadcrumb {
|
||||
font-size: 13px;
|
||||
}
|
||||
.user-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -33,45 +33,20 @@
|
||||
<template v-for="(node, i) in visibleMenu" :key="node.url || node.header || i">
|
||||
<!-- header node: a section divider carrying `items` (ant has no icon
|
||||
slot on a menu-item-group; it's a label-only divider, so its
|
||||
children still carry their own icons below). -->
|
||||
children still carry their own icons below). Each child renders
|
||||
through NavMenuNode, which recurses into however many submenu
|
||||
levels it has. -->
|
||||
<a-menu-item-group v-if="node.header" :title="node.i18n || node.header">
|
||||
<template v-for="(child, j) in visibleChildren(node.items)" :key="child.url || j">
|
||||
<a-sub-menu v-if="child.submenu" :key="subKey(child, j)">
|
||||
<template #icon><component :is="resolveIcon(child.icon)" v-if="child.icon" /></template>
|
||||
<template #title>{{ child.i18n || child.name }}</template>
|
||||
<a-menu-item
|
||||
v-for="(leaf, k) in visibleChildren(child.submenu)"
|
||||
:key="leaf.url || `${j}-${k}`"
|
||||
>
|
||||
<template #icon><component :is="resolveIcon(leaf.icon)" v-if="leaf.icon" /></template>
|
||||
{{ leaf.i18n || leaf.name }}
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-menu-item v-else :key="child.url || `${j}`">
|
||||
<template #icon><component :is="resolveIcon(child.icon)" v-if="child.icon" /></template>
|
||||
{{ child.i18n || child.name }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
<NavMenuNode
|
||||
v-for="(child, j) in visibleChildren(node.items)"
|
||||
:key="child.url || `${i}-${j}`"
|
||||
:node="child"
|
||||
:menu-key="child.url || subKey(child, j)"
|
||||
/>
|
||||
</a-menu-item-group>
|
||||
|
||||
<!-- group node -->
|
||||
<a-sub-menu v-else-if="node.submenu" :key="subKey(node, i)">
|
||||
<template #icon><component :is="resolveIcon(node.icon)" v-if="node.icon" /></template>
|
||||
<template #title>{{ node.i18n || node.name }}</template>
|
||||
<a-menu-item
|
||||
v-for="(leaf, k) in visibleChildren(node.submenu)"
|
||||
:key="leaf.url || `${i}-${k}`"
|
||||
>
|
||||
<template #icon><component :is="resolveIcon(leaf.icon)" v-if="leaf.icon" /></template>
|
||||
{{ leaf.i18n || leaf.name }}
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
||||
<!-- leaf node -->
|
||||
<a-menu-item v-else :key="node.url || `${i}`">
|
||||
<template #icon><component :is="resolveIcon(node.icon)" v-if="node.icon" /></template>
|
||||
{{ node.i18n || node.name }}
|
||||
</a-menu-item>
|
||||
<!-- group / leaf node -->
|
||||
<NavMenuNode v-else :node="node" :menu-key="node.url || subKey(node, i)" />
|
||||
</template>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
@@ -80,9 +55,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getMenu, type NavMenuNode } from '../navMenuItems';
|
||||
import { getMenu, type NavMenuNode as NavMenuNodeData } from '../navMenuItems';
|
||||
import { useUsersStore } from '../../stores/users';
|
||||
import { resolveIcon } from '../../components/icons';
|
||||
import NavMenuNode from './NavMenuNode.vue';
|
||||
|
||||
withDefaults(defineProps<{ title?: string; collapsed?: boolean }>(), {
|
||||
title: 'منتزر',
|
||||
@@ -101,17 +77,17 @@ const users = useUsersStore();
|
||||
* items (whose permissionId is commented out in the legacy data) show for everyone.
|
||||
* Preserved deliberately — see docs/ADMIN-PORT-PLAN.md.
|
||||
*/
|
||||
function canSee(node: NavMenuNode): boolean {
|
||||
function canSee(node: NavMenuNodeData): boolean {
|
||||
if (!node.permissionId) return true;
|
||||
return Boolean(users.hasPermission(node.permissionId));
|
||||
}
|
||||
|
||||
function visibleChildren(nodes?: NavMenuNode[]): NavMenuNode[] {
|
||||
function visibleChildren(nodes?: NavMenuNodeData[]): NavMenuNodeData[] {
|
||||
return (nodes ?? []).filter(canSee);
|
||||
}
|
||||
|
||||
/** Hides a group whose children are all hidden, as legacy did. */
|
||||
function hasVisibleContent(node: NavMenuNode): boolean {
|
||||
function hasVisibleContent(node: NavMenuNodeData): boolean {
|
||||
if (node.header) return visibleChildren(node.items).length > 0;
|
||||
if (node.submenu) return visibleChildren(node.submenu).length > 0;
|
||||
return true;
|
||||
@@ -121,7 +97,28 @@ const visibleMenu = computed(() =>
|
||||
getMenu().filter((n) => canSee(n) && hasVisibleContent(n)),
|
||||
);
|
||||
|
||||
const subKey = (node: NavMenuNode, i: number) => `sub-${node.name ?? ''}-${i}`;
|
||||
const subKey = (node: NavMenuNodeData, i: number) => `sub-${node.name ?? ''}-${i}`;
|
||||
|
||||
/**
|
||||
* Finds the chain of sub-menu keys that must be open so the current route's
|
||||
* menu item is visible, at any nesting depth. Mirrors the key each
|
||||
* NavMenuNode instance is actually given (node.url, or menuKey-derived for
|
||||
* url-less group nodes), so a match here really is a key ant's a-menu knows
|
||||
* about.
|
||||
*/
|
||||
function findOpenKeysIn(nodes: NavMenuNodeData[], path: string, parentKey: string): string[] | null {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
const key = node.url || `${parentKey}-${i}`;
|
||||
if (!node.submenu) {
|
||||
if (node.url === path) return [];
|
||||
continue;
|
||||
}
|
||||
const nested = findOpenKeysIn(visibleChildren(node.submenu), path, key);
|
||||
if (nested) return [key, ...nested];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the sub-menu key(s) that must be open so the current route's menu
|
||||
@@ -137,15 +134,18 @@ function findOpenKeysFor(path: string): string[] {
|
||||
const children = visibleChildren(node.items);
|
||||
for (let j = 0; j < children.length; j++) {
|
||||
const child = children[j];
|
||||
if (child.submenu && visibleChildren(child.submenu).some((leaf) => leaf.url === path)) {
|
||||
return [subKey(child, j)];
|
||||
}
|
||||
if (!child.submenu) continue;
|
||||
const childKey = child.url || subKey(child, j);
|
||||
const nested = findOpenKeysIn(visibleChildren(child.submenu), path, childKey);
|
||||
if (nested) return [childKey, ...nested];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.submenu && visibleChildren(node.submenu).some((leaf) => leaf.url === path)) {
|
||||
return [subKey(node, i)];
|
||||
if (node.submenu) {
|
||||
const nodeKey = node.url || subKey(node, i);
|
||||
const nested = findOpenKeysIn(visibleChildren(node.submenu), path, nodeKey);
|
||||
if (nested) return [nodeKey, ...nested];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
@@ -175,7 +175,8 @@ function onClick({ key }: { key: string | number }) {
|
||||
|
||||
<style scoped>
|
||||
.vertical-nav-menu {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
overflow-y: auto; /* pinned in place; its own menu scrolls if it's taller than the viewport */
|
||||
border-left: 1px solid #f0f0f0;
|
||||
}
|
||||
.nav-header {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
<template>
|
||||
<!--
|
||||
Ported from legacy henza_admin/src/layouts/main/Main.vue (281 lines).
|
||||
Structure preserved: nav menu + navbar + breadcrumb/pageTitle header +
|
||||
routed content. Dropped as template-only or Vue-2-only: VxTour, TheCustomizer,
|
||||
vue-backtotop, and the Vuesax transition wrappers.
|
||||
Structure preserved: nav menu + navbar + routed content. Dropped as
|
||||
template-only or Vue-2-only: VxTour, TheCustomizer, vue-backtotop, and the
|
||||
Vuesax transition wrappers. The breadcrumb/pageTitle header row that used
|
||||
to sit above the content as its own row now lives inside TheNavbar, to
|
||||
give pages full-height room for their card.
|
||||
|
||||
Layout is pinned viewport-height: the sidebar and navbar never move, and
|
||||
only #content-area scrolls — instead of the whole page scrolling under a
|
||||
sidebar/navbar that used to scroll away with it.
|
||||
-->
|
||||
<a-layout class="layout--main" :class="{ 'no-scroll': isAppPage }">
|
||||
<VerticalNavMenu :collapsed="collapsed" />
|
||||
@@ -13,19 +19,6 @@
|
||||
|
||||
<a-layout-content id="content-area">
|
||||
<div class="content-wrapper">
|
||||
<!-- legacy router-header: shown only when the route declares one -->
|
||||
<div v-if="breadcrumb || pageTitle" class="router-header">
|
||||
<h2 class="content-area__heading">{{ routeTitle }}</h2>
|
||||
<a-breadcrumb v-if="breadcrumb" class="breadcrumb">
|
||||
<a-breadcrumb-item v-for="(crumb, i) in breadcrumb" :key="i">
|
||||
<router-link v-if="crumb.url && !crumb.active" :to="crumb.url">
|
||||
{{ crumb.title }}
|
||||
</router-link>
|
||||
<span v-else>{{ crumb.title }}</span>
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
</div>
|
||||
|
||||
<div class="content-area__content">
|
||||
<router-view @changeRouteTitle="changeRouteTitle" />
|
||||
</div>
|
||||
@@ -42,26 +35,20 @@ import VerticalNavMenu from '../components/VerticalNavMenu.vue';
|
||||
import TheNavbar from '../components/TheNavbar.vue';
|
||||
import { useLayoutStore } from '../../stores/layout';
|
||||
|
||||
interface Crumb {
|
||||
title: string;
|
||||
url?: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const layout = useLayoutStore();
|
||||
// Persisted via stores/layout.ts (localStorage) so the sidebar stays
|
||||
// collapsed/expanded across reloads instead of resetting every time.
|
||||
const collapsed = computed(() => layout.sidebarCollapsed);
|
||||
|
||||
const pageTitle = computed(() => route.meta?.pageTitle as string | undefined);
|
||||
const breadcrumb = computed(() => route.meta?.breadcrumb as Crumb[] | undefined);
|
||||
// legacy meta.no_scroll -> the 'no-scroll' body class
|
||||
const isAppPage = computed(() => Boolean(route.meta?.no_scroll));
|
||||
|
||||
// legacy: views could override the heading via @changeRouteTitle
|
||||
// legacy: views could override the heading via @changeRouteTitle. The
|
||||
// heading itself now lives in TheNavbar, so this only resets on navigation;
|
||||
// nothing currently emits changeRouteTitle, but the wiring is kept in case a
|
||||
// view starts overriding its own title again.
|
||||
const overriddenTitle = ref<string | undefined>(undefined);
|
||||
const routeTitle = computed(() => overriddenTitle.value ?? pageTitle.value ?? '');
|
||||
|
||||
function changeRouteTitle(title: string) {
|
||||
overriddenTitle.value = title;
|
||||
@@ -77,21 +64,28 @@ watch(
|
||||
|
||||
<style scoped>
|
||||
.layout--main {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
overflow: hidden; /* the sidebar and navbar are pinned; only #content-area scrolls */
|
||||
}
|
||||
.layout--main > :deep(.ant-layout) {
|
||||
height: 100%;
|
||||
}
|
||||
#content-area {
|
||||
padding: 16px;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
.router-header {
|
||||
/* Stretch the routed page (and its card) to fill the viewport height below
|
||||
the navbar, instead of the card sizing to its content and leaving a gray
|
||||
gap underneath on short lists. This is also the ONLY element that
|
||||
scrolls — the navbar above it and the sidebar beside it stay fixed. */
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: 1rem;
|
||||
height: calc(100vh - 61px); /* 61px matches TheNavbar's fixed height */
|
||||
overflow-y: auto;
|
||||
}
|
||||
.content-area__heading {
|
||||
margin: 0;
|
||||
color: #1c2d56;
|
||||
.content-wrapper,
|
||||
.content-area__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -128,39 +128,49 @@ export const adminMenu: NavMenuNode[] = [
|
||||
]
|
||||
},
|
||||
{
|
||||
// Grouped per request: "شعب" and "کارگاه" used to be two separate
|
||||
// top-level entries; they are now one group with each nested inside
|
||||
// as its own submenu, same shape as the other grouped sections.
|
||||
url: null,
|
||||
name: 'Branch',
|
||||
name: 'branch and workshop',
|
||||
icon: 'GridIcon',
|
||||
i18n: 'شعب',
|
||||
permissionId:"branch",
|
||||
i18n: 'شعب و کارگاه',
|
||||
submenu: [
|
||||
// 'افزودن شعب' removed: it is now the add button on the branch list
|
||||
// page. The /branch/add route still works.
|
||||
{
|
||||
url: '/branch/list',
|
||||
name: 'BranchList',
|
||||
slug: 'branch-list',
|
||||
i18n: 'لیست شعب',
|
||||
permissionId:"branch.list"
|
||||
url: null,
|
||||
name: 'Branch',
|
||||
icon: 'GridIcon',
|
||||
i18n: 'شعب',
|
||||
permissionId:"branch",
|
||||
submenu: [
|
||||
// 'افزودن شعب' removed: it is now the add button on the branch
|
||||
// list page. The /branch/add route still works.
|
||||
{
|
||||
url: '/branch/list',
|
||||
name: 'BranchList',
|
||||
slug: 'branch-list',
|
||||
i18n: 'لیست شعب',
|
||||
permissionId:"branch.list"
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
/////////////////////////////////////////////
|
||||
{
|
||||
url: null,
|
||||
name: 'Workshop',
|
||||
icon: 'GridIcon',
|
||||
i18n: 'کارگاه',
|
||||
permissionId:"workshop",
|
||||
submenu: [
|
||||
// 'افزودن کارگاه' removed: it is now the add button on the workshop
|
||||
// list page. The /workshop/add route still works.
|
||||
{
|
||||
url: '/workshop/list',
|
||||
name: 'WorkshopList',
|
||||
slug: 'workshop-list',
|
||||
i18n: 'لیست کارگاه ها',
|
||||
permissionId:"workshop.list"
|
||||
url: null,
|
||||
name: 'Workshop',
|
||||
icon: 'GridIcon',
|
||||
i18n: 'کارگاه',
|
||||
permissionId:"workshop",
|
||||
submenu: [
|
||||
// 'افزودن کارگاه' removed: it is now the add button on the
|
||||
// workshop list page. The /workshop/add route still works.
|
||||
{
|
||||
url: '/workshop/list',
|
||||
name: 'WorkshopList',
|
||||
slug: 'workshop-list',
|
||||
i18n: 'لیست کارگاه ها',
|
||||
permissionId:"workshop.list"
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
@@ -264,59 +274,6 @@ export const adminMenu: NavMenuNode[] = [
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
url: null,
|
||||
name: 'wallet',
|
||||
icon: 'CreditCardIcon',
|
||||
i18n: 'کیف پول',
|
||||
permissionId:"wallet",
|
||||
submenu: [
|
||||
{
|
||||
url: '/wallet',
|
||||
name: 'wallet',
|
||||
slug: 'wallet',
|
||||
i18n: 'لیست کیف پول های شارژ شده',
|
||||
permissionId:"wallet.list"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
url: null,
|
||||
name: 'category',
|
||||
icon: 'ListIcon',
|
||||
i18n: 'دسته بندی ها',
|
||||
permissionId:"collection",
|
||||
submenu: [
|
||||
// 'افزودن دسته بندی' removed: it is now the add button on the
|
||||
// category list page. The /category/add route still works.
|
||||
{
|
||||
url: '/category/list',
|
||||
name: 'categoryList',
|
||||
slug: 'list-category',
|
||||
i18n: 'لیست دسته بندی ها',
|
||||
permissionId:"collection.list"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
url: null,
|
||||
name: 'finical',
|
||||
icon: 'DollarSignIcon',
|
||||
i18n: 'اسناد مالی',
|
||||
permissionId:"document",
|
||||
submenu: [
|
||||
{
|
||||
url: '/finical/list',
|
||||
name: 'finical_list',
|
||||
slug: 'finical_list',
|
||||
i18n: 'لیست اسناد',
|
||||
permissionId:"document.list"
|
||||
},
|
||||
// 'وارد کردن سند مالی مشتری' removed: it is now the add button on
|
||||
// the financial documents list page. The /add/findoc route still
|
||||
// works.
|
||||
]
|
||||
},
|
||||
{
|
||||
url: null,
|
||||
name: 'report',
|
||||
@@ -343,86 +300,89 @@ export const adminMenu: NavMenuNode[] = [
|
||||
i18n: 'لیست تخفیفات ',
|
||||
permissionId:"offers.list"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'report',
|
||||
icon: 'SlashIcon',
|
||||
i18n: 'استثناعات تخفیف',
|
||||
permissionId:"offersIgnore",
|
||||
url: '/major/offersIgnore',
|
||||
slug: 'offersIgnore',
|
||||
},
|
||||
|
||||
{
|
||||
url: null,
|
||||
name: 'Invoice',
|
||||
icon: 'BookIcon',
|
||||
i18n: 'فاکتور انتقال',
|
||||
permissionId:"invoice",
|
||||
submenu: [
|
||||
// 'افزودن فاکتور' removed: it is now the add button on the invoice
|
||||
// list page. The /invoice/add route still works.
|
||||
// Moved under "تخفیفات" per request — was a top-level entry.
|
||||
{
|
||||
url: '/invoice/list',
|
||||
name: 'InvoiceList',
|
||||
slug: 'invoice-list',
|
||||
i18n: 'لیست فاکتورها',
|
||||
permissionId:"invoice.list"
|
||||
name: 'report',
|
||||
icon: 'SlashIcon',
|
||||
i18n: 'استثناعات تخفیف',
|
||||
permissionId:"offersIgnore",
|
||||
url: '/major/offersIgnore',
|
||||
slug: 'offersIgnore',
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
// Grouped per request: "اسناد مالی", "فاکتور انتقال" and "گزارشات"
|
||||
// used to be three separate top-level entries; they are now one
|
||||
// group ("گزارشات و اسناد مالی") with each of them nested inside it
|
||||
// as its own submenu, same 3-level shape as "پیکر بندی" below.
|
||||
url: null,
|
||||
name: 'report',
|
||||
name: 'reports and documents',
|
||||
icon: 'FileTextIcon',
|
||||
i18n: 'گزارشات',
|
||||
permissionId:"report",
|
||||
i18n: 'گزارشات و اسناد مالی',
|
||||
submenu: [
|
||||
{
|
||||
url: '/report/order_item',
|
||||
name: 'report_order_item',
|
||||
slug: 'report_ordedr_item',
|
||||
i18n: 'گزارش تجمیعی سفارش و اسناد',
|
||||
permissionId:"report.order.item"
|
||||
url: null,
|
||||
name: 'finical',
|
||||
icon: 'DollarSignIcon',
|
||||
i18n: 'اسناد مالی',
|
||||
permissionId:"document",
|
||||
submenu: [
|
||||
{
|
||||
url: '/finical/list',
|
||||
name: 'finical_list',
|
||||
slug: 'finical_list',
|
||||
i18n: 'لیست اسناد',
|
||||
permissionId:"document.list"
|
||||
},
|
||||
// 'وارد کردن سند مالی مشتری' removed: it is now the add button
|
||||
// on the financial documents list page. The /add/findoc route
|
||||
// still works.
|
||||
]
|
||||
},
|
||||
{
|
||||
url: '/report/product',
|
||||
name: 'report_product',
|
||||
slug: 'report_product',
|
||||
i18n: 'گزارش محصولات',
|
||||
permissionId:"report.product"
|
||||
url: null,
|
||||
name: 'Invoice',
|
||||
icon: 'BookIcon',
|
||||
i18n: 'فاکتور انتقال',
|
||||
permissionId:"invoice",
|
||||
submenu: [
|
||||
// 'افزودن فاکتور' removed: it is now the add button on the
|
||||
// invoice list page. The /invoice/add route still works.
|
||||
{
|
||||
url: '/invoice/list',
|
||||
name: 'InvoiceList',
|
||||
slug: 'invoice-list',
|
||||
i18n: 'لیست فاکتورها',
|
||||
permissionId:"invoice.list"
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
url: null,
|
||||
name: 'cities offer percent',
|
||||
// tagColor: 'warning',
|
||||
icon: 'PercentIcon',
|
||||
i18n: 'شهر ها',
|
||||
permissionId:"cities",
|
||||
submenu: [
|
||||
// 'افزودن شهر' removed: it is now the add button on the city list
|
||||
// page. The /city/create route still works.
|
||||
{
|
||||
url: '/city/list',
|
||||
name: 'city list',
|
||||
slug: 'city_list',
|
||||
i18n: 'لیست شهر ها',
|
||||
permissionId:"cities.list"
|
||||
url: null,
|
||||
name: 'report',
|
||||
icon: 'FileTextIcon',
|
||||
i18n: 'گزارشات',
|
||||
permissionId:"report",
|
||||
submenu: [
|
||||
{
|
||||
url: '/report/order_item',
|
||||
name: 'report_order_item',
|
||||
slug: 'report_ordedr_item',
|
||||
i18n: 'گزارش تجمیعی سفارش و اسناد',
|
||||
permissionId:"report.order.item"
|
||||
},
|
||||
{
|
||||
url: '/report/product',
|
||||
name: 'report_product',
|
||||
slug: 'report_product',
|
||||
i18n: 'گزارش محصولات',
|
||||
permissionId:"report.product"
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
url: '/comments',
|
||||
name: 'comments',
|
||||
// tagColor: 'warning',
|
||||
icon: 'MessageSquareIcon',
|
||||
i18n: 'دیدگاه ها',
|
||||
permissionId:"comment"
|
||||
},
|
||||
{
|
||||
url: null,
|
||||
name: 'sms',
|
||||
@@ -439,22 +399,6 @@ export const adminMenu: NavMenuNode[] = [
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
url: null,
|
||||
name: 'notify',
|
||||
icon: 'BellIcon',
|
||||
i18n: 'نوتیفیکیشن',
|
||||
permissionId:"notify",
|
||||
submenu: [
|
||||
{
|
||||
url: '/notify/create',
|
||||
name: 'create notify',
|
||||
slug: 'create_notify',
|
||||
i18n: 'ارسال نوتیفیکیشن',
|
||||
permissionId:"notify.create",
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
url: '/statics',
|
||||
name: 'statics',
|
||||
@@ -504,6 +448,85 @@ export const adminMenu: NavMenuNode[] = [
|
||||
i18n: 'مدیریت کیف پول',
|
||||
permissionId:"config.wallet"
|
||||
},
|
||||
// Moved under "پیکر بندی" per request — same node shape as before
|
||||
// (own submenu / plain leaf), just relocated in the tree.
|
||||
{
|
||||
url: null,
|
||||
name: 'category',
|
||||
icon: 'ListIcon',
|
||||
i18n: 'دسته بندی ها',
|
||||
permissionId:"collection",
|
||||
submenu: [
|
||||
// 'افزودن دسته بندی' removed: it is now the add button on the
|
||||
// category list page. The /category/add route still works.
|
||||
{
|
||||
url: '/category/list',
|
||||
name: 'categoryList',
|
||||
slug: 'list-category',
|
||||
i18n: 'لیست دسته بندی ها',
|
||||
permissionId:"collection.list"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
url: null,
|
||||
name: 'wallet',
|
||||
icon: 'CreditCardIcon',
|
||||
i18n: 'کیف پول',
|
||||
permissionId:"wallet",
|
||||
submenu: [
|
||||
{
|
||||
url: '/wallet',
|
||||
name: 'wallet',
|
||||
slug: 'wallet',
|
||||
i18n: 'لیست کیف پول های شارژ شده',
|
||||
permissionId:"wallet.list"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
url: null,
|
||||
name: 'cities offer percent',
|
||||
// tagColor: 'warning',
|
||||
icon: 'PercentIcon',
|
||||
i18n: 'شهر ها',
|
||||
permissionId:"cities",
|
||||
submenu: [
|
||||
// 'افزودن شهر' removed: it is now the add button on the city list
|
||||
// page. The /city/create route still works.
|
||||
{
|
||||
url: '/city/list',
|
||||
name: 'city list',
|
||||
slug: 'city_list',
|
||||
i18n: 'لیست شهر ها',
|
||||
permissionId:"cities.list"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
url: '/comments',
|
||||
name: 'comments',
|
||||
// tagColor: 'warning',
|
||||
icon: 'MessageSquareIcon',
|
||||
i18n: 'دیدگاه ها',
|
||||
permissionId:"comment"
|
||||
},
|
||||
{
|
||||
url: null,
|
||||
name: 'notify',
|
||||
icon: 'BellIcon',
|
||||
i18n: 'نوتیفیکیشن',
|
||||
permissionId:"notify",
|
||||
submenu: [
|
||||
{
|
||||
url: '/notify/create',
|
||||
name: 'create notify',
|
||||
slug: 'create_notify',
|
||||
i18n: 'ارسال نوتیفیکیشن',
|
||||
permissionId:"notify.create",
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
-->
|
||||
<div id="page-user-list">
|
||||
<a-card title="لیست دسته بندی ها">
|
||||
<ListPageHeader rid="collection.create" add-label="افزودن دسته" @add="openAdd" />
|
||||
|
||||
<XTable ref="tbl" :model="model">
|
||||
<template #add>
|
||||
<ListPageHeader rid="collection.create" add-label="افزودن دسته" @add="openAdd" />
|
||||
</template>
|
||||
|
||||
<template #image="{ text }">
|
||||
<img
|
||||
width="90px"
|
||||
|
||||
@@ -141,6 +141,9 @@ async function handleSubmit() {
|
||||
await api.post('branch', form);
|
||||
}
|
||||
message.info('اطلاعات با موفقیت به ثبت رسید.');
|
||||
if (!id) {
|
||||
Object.assign(form, { name: '', phone: '', address: '', user_id: undefined });
|
||||
}
|
||||
if (asModal) {
|
||||
emit('success');
|
||||
emit('update:open', false);
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
growing a v-if per caller.
|
||||
-->
|
||||
<a-card :title="title">
|
||||
<ListPageHeader v-if="addRid" :rid="addRid" :add-label="addLabel" @add="openAdd()" />
|
||||
|
||||
<XTable ref="tbl" :model="model">
|
||||
<template v-if="addRid" #add>
|
||||
<ListPageHeader :rid="addRid" :add-label="addLabel" @add="openAdd()" />
|
||||
</template>
|
||||
|
||||
<template #actions="{ record }">
|
||||
<div class="flex gap-2">
|
||||
<a-tooltip v-if="addRid" title="ویرایش">
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
-->
|
||||
<div id="page-user-list">
|
||||
<a-card title="لیست شهر ها">
|
||||
<ListPageHeader rid="cities.create" add-label="افزودن شهر" @add="addOpen = true" />
|
||||
|
||||
<XTable ref="citiesTable" :model="model">
|
||||
<template #add>
|
||||
<ListPageHeader rid="cities.create" add-label="افزودن شهر" @add="addOpen = true" />
|
||||
</template>
|
||||
|
||||
<template #percent="{ text }">
|
||||
{{ text ?? '-' }}
|
||||
</template>
|
||||
|
||||
@@ -13,19 +13,16 @@
|
||||
-->
|
||||
<div id="page-user-list">
|
||||
<a-card :title="title">
|
||||
<ListPageHeader
|
||||
v-if="showAddButton"
|
||||
rid="persons.create.client"
|
||||
add-label="افزودن مشتری عمده"
|
||||
@add="openAdd"
|
||||
/>
|
||||
|
||||
<div v-if="query && query.count && query.total">
|
||||
{{ query.total }} رکورد یافت شد -
|
||||
<span class="text-danger">{{ query.count }} رکورد غیر فعال</span>
|
||||
</div>
|
||||
|
||||
<XTable ref="tbl" :model="model" @query="setCount">
|
||||
<template v-if="showAddButton" #add>
|
||||
<ListPageHeader rid="persons.create.client" add-label="افزودن مشتری عمده" @add="openAdd" />
|
||||
</template>
|
||||
|
||||
<template #city="{ record }">
|
||||
<div v-if="record.city && record.city.title">{{ record.city.title }}</div>
|
||||
<div v-else>-</div>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<!--
|
||||
Ported from legacy views/customer/omde/OmdeList.vue (615 lines).
|
||||
The only one of OmdeCustomerList's five callers with an add button — see
|
||||
OmdeCustomerList.vue's showAddButton prop for why it isn't on all five.
|
||||
Add button removed per request: hide the "افزودن مشتری عمده" action on
|
||||
this list. OmdeCustomerList.vue's showAddButton prop still exists in case
|
||||
it needs to come back.
|
||||
-->
|
||||
<OmdeCustomerList :title="title" :model="model" show-add-button />
|
||||
<OmdeCustomerList :title="title" :model="model" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
<div id="page-user-list">
|
||||
<a-spin :spinning="loading">
|
||||
<a-card title="لیست اسناد مالی">
|
||||
<ListPageHeader rid="document.create" add-label="ثبت سند مالی" @add="openAdd" />
|
||||
|
||||
<XTable ref="tbl" :model="model">
|
||||
<template #add>
|
||||
<ListPageHeader rid="document.create" add-label="ثبت سند مالی" @add="openAdd" />
|
||||
</template>
|
||||
|
||||
<template #confirmed="{ text }">
|
||||
<a-tag v-if="text === 1" color="green">تایید شده</a-tag>
|
||||
<a-tag v-else>تایید نشده</a-tag>
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
-->
|
||||
<div id="page-user-list">
|
||||
<a-card title="لیست فاکتورها">
|
||||
<ListPageHeader rid="invoice.create" add-label="افزودن فاکتور" @add="openAdd" />
|
||||
|
||||
<XTable ref="tbl" :model="model">
|
||||
<template #add>
|
||||
<ListPageHeader rid="invoice.create" add-label="افزودن فاکتور" @add="openAdd" />
|
||||
</template>
|
||||
|
||||
<template #user="{ record }">
|
||||
{{ record.allocator_user?.name ?? '-' }}
|
||||
</template>
|
||||
|
||||
@@ -8,12 +8,14 @@
|
||||
-->
|
||||
<div id="page-user-list">
|
||||
<a-card title="لیست تخفیف ها">
|
||||
<div class="list-page-header-row">
|
||||
<ListPageHeader rid="offers.create" add-label="افزودن تخفیف عمده" @add="openAdd('omde')" />
|
||||
<ListPageHeader rid="offers.create.tak" add-label="افزودن تخفیف تک" @add="openAdd('tak')" />
|
||||
</div>
|
||||
|
||||
<XTable ref="tbl" :model="model">
|
||||
<template #add>
|
||||
<div class="list-page-header-row">
|
||||
<ListPageHeader rid="offers.create" add-label="افزودن تخفیف عمده" @add="openAdd('omde')" />
|
||||
<ListPageHeader rid="offers.create.tak" add-label="افزودن تخفیف تک" @add="openAdd('tak')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #sts="{ record }">
|
||||
<a-tag v-if="record.status === 1" color="green">فعال</a-tag>
|
||||
<a-tag v-else color="red">غیر فعال</a-tag>
|
||||
@@ -136,13 +138,7 @@ async function deletePromotion(record: Record<string, any>) {
|
||||
<style scoped>
|
||||
.list-page-header-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
/* ListPageHeader's own margin-bottom would double up when two sit side by side */
|
||||
.list-page-header-row :deep(.list-page-header) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
-->
|
||||
<div id="page-user-list">
|
||||
<a-card title="لیست مدیران">
|
||||
<!-- legacy ListUser.vue:4 — the add-user button, gated on the same rid. -->
|
||||
<ListPageHeader rid="user.list.create" add-label="افزودن کاربر" @add="openAdd" />
|
||||
|
||||
<XTable ref="tbl" :model="model">
|
||||
<!-- legacy ListUser.vue:4 — the add-user button, gated on the same rid. -->
|
||||
<template #add>
|
||||
<ListPageHeader rid="user.list.create" add-label="افزودن کاربر" @add="openAdd" />
|
||||
</template>
|
||||
|
||||
<template #ut="{ text }">
|
||||
{{ userTypeTitle(text) }}
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user