Merge branch 'Baghi/conflict' of https://git2.tavasi.ir/Baghi/conflict-nuxt-4 into shadi/conflict

This commit is contained in:
Mehdi104797 2026-02-14 10:06:41 +03:30
commit c9fe6b38c5
5 changed files with 296 additions and 179 deletions

View File

@ -1,4 +1,3 @@
<!-- app.vue -->
<template> <template>
<UApp> <UApp>
<NuxtLayout> <NuxtLayout>
@ -11,7 +10,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useHead } from "#imports"; import { useHead } from "#imports";
import { onMounted } from "vue"; import { onMounted } from "vue";
import { composSystemTheme } from "~/composables/composSystemTheme"; import { composSystemTheme } from "@/composables/composSystemTheme";
// تنظیم تم سیستم // تنظیم تم سیستم
useHead({ useHead({
script: [ script: [

View File

@ -6,7 +6,7 @@
<div class="col-span-3 lg:col-span-2 xl:col-span-3 hidden lg:block"> <div class="col-span-3 lg:col-span-2 xl:col-span-3 hidden lg:block">
<template v-if="headerSchema.breadcrumb"> <template v-if="headerSchema.breadcrumb">
<Breadcrumb <Breadcrumb
:breadcrumbData="[]" :breadcrumbData="defaultSidebar.topMenu"
:tabs="tabs" :tabs="tabs"
:activeTabId="activeTabModel" :activeTabId="activeTabModel"
/> />
@ -14,7 +14,7 @@
<template v-if="headerSchema.logo"> <template v-if="headerSchema.logo">
<nuxt-link :to="{ name: 'DashboardBasePage' }"> <nuxt-link :to="{ name: 'DashboardBasePage' }">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<img :src="useSystemTheme.logo.value" alt="" class="h-9 w-9" /> <img :src="logoSrc" alt="" class="h-9 w-9" />
<div v-if="useSystemTheme.currentTheme.value" class="flex flex-col"> <div v-if="useSystemTheme.currentTheme.value" class="flex flex-col">
<span class="font-bold text-gray-900 dark:text-light-primary"> <span class="font-bold text-gray-900 dark:text-light-primary">
{{ useSystemTheme.currentTheme.value.title || "" }} {{ useSystemTheme.currentTheme.value.title || "" }}
@ -29,8 +29,8 @@
</div> </div>
<div class="col-span-3 lg:col-span-2 xl:col-span-3 lg:hidden"> <div class="col-span-3 lg:col-span-2 xl:col-span-3 lg:hidden">
<button <button
@click="toggleSidebarMenu" @click="commonStore.toggleSidebar"
class="flex items-center justify-center w-8 h-8 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-dark-primary-800 transition-colors duration-200" class="flex cursor-pointer items-center justify-center w-8 h-8 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-dark-primary-800 transition-colors duration-200"
aria-label="باز کردن منو" aria-label="باز کردن منو"
> >
<UIcon name="i-heroicons-bars-3" class="w-5 h-5" /> <UIcon name="i-heroicons-bars-3" class="w-5 h-5" />
@ -123,6 +123,7 @@
import { ref, computed, onMounted } from "vue"; import { ref, computed, onMounted } from "vue";
import headerItems from "@/json/header/header.json"; import headerItems from "@/json/header/header.json";
import { composSystemTheme } from "@/composables/composSystemTheme"; import { composSystemTheme } from "@/composables/composSystemTheme";
import defaultSidebar from "@/json/sidebar/dashboard.json";
import { useCommonStore } from "@/stores/commonStore"; import { useCommonStore } from "@/stores/commonStore";
import { useAuthStore } from "@/stores/authStore"; import { useAuthStore } from "@/stores/authStore";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
@ -144,7 +145,7 @@ const emit = defineEmits([
"tab-change", "tab-change",
"user-menu-select", "user-menu-select",
]); ]);
const logoSrc = ref("");
/* ---------------- ACTIVE TAB ---------------- */ /* ---------------- ACTIVE TAB ---------------- */
const activeTabModel = computed({ const activeTabModel = computed({
get: () => props.activeTab, get: () => props.activeTab,
@ -197,9 +198,6 @@ const userMenuItems = computed(() =>
// const userAvatar = "https://api.dicebear.com/7.x/avataaars/svg?seed=admin"; // const userAvatar = "https://api.dicebear.com/7.x/avataaars/svg?seed=admin";
const isClient = ref(false); const isClient = ref(false);
onMounted(() => {
isClient.value = true;
});
const userAvatar = computed(() => { const userAvatar = computed(() => {
if (!process.client) return null; if (!process.client) return null;
@ -237,7 +235,14 @@ const userInitial = computed(() => {
return "؟"; return "؟";
} }
}); });
function toggleSidebarMenu() { // function toggleSidebarMenu() {
commonStore.isSidebarOpen(); // commonStore.isSidebarOpen();
} // }
onMounted(() => {
isClient.value = true;
useSystemTheme.applyTheme();
setTimeout(() => {
logoSrc.value = useSystemTheme.logo.value;
}, 300);
});
</script> </script>

View File

@ -4,41 +4,44 @@
side="left" side="left"
:collapsed="collapsed" :collapsed="collapsed"
collapsible collapsible
:min-size="5"
:default-size="10"
:max-size="15"
:ui="sidebarUI" :ui="sidebarUI"
class="sidebar-gradient" class="sidebar-gradient"
:style="{ width: collapsed ? '2%' : '8%' }"
dir="rtl" dir="rtl"
@update:collapsed="onCollapse" @update:collapsed="onCollapse"
> >
<template #header> <template #header>
<nuxt-link :to="{ name: 'DashboardBasePage' }"> <nuxt-link
:to="{ name: 'DashboardBasePage' }"
class="w-full flex justify-center items-center"
>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<img :src="useSystemTheme.logo.value" alt="" class="h-9 w-9" /> <img :src="logoSrc" alt="" class="h-9 w-9" />
<div
v-if="useSystemTheme.currentTheme.value && !collapsed"
class="flex flex-col"
>
<span class="font-bold text-gray-900 dark:text-light-primary">
{{ useSystemTheme.currentTheme.value.title || "" }}
</span>
<span class="text-xs text-gray-500 dark:text-gray-400">
{{ useSystemTheme.currentTheme.value.subTitle || "" }}
</span>
</div>
</div> </div>
</nuxt-link> </nuxt-link>
</template> </template>
<!-- <div
v-if="useSystemTheme?.currentTheme?.value && !collapsed"
class="flex flex-col"
>
<span class="font-bold text-gray-900 dark:text-light-primary">
{{ useSystemTheme?.currentTheme?.value?.title || "" }}
</span>
<span class="text-xs text-gray-500 dark:text-gray-400">
{{ useSystemTheme?.currentTheme?.value?.subTitle || "" }}
</span>
</div> -->
<template #default> <template #default>
<div class=""> <div class="">
<UNavigationMenu <UNavigationMenu
dir="rtl" dir="rtl"
:collapsed="collapsed"
:items="getSideBarSchema()?.topMenu || []"
orientation="vertical" orientation="vertical"
:items="getSideBarSchema()?.topMenu || []"
:collapsed="collapsed"
:ui="navigationUI" :ui="navigationUI"
class="w-full"
/> />
</div> </div>
@ -50,21 +53,36 @@
orientation="vertical" orientation="vertical"
:ui="navigationUI" :ui="navigationUI"
/> />
<UTooltip
arrow
dir="rtl"
:text="user?.first_name + ' ' + user?.last_name"
>
<UButton
:avatar="
user?.avatar
? { src: user?.avatar }
: {
label:
(user?.first_name?.[0] || '') +
(user?.last_name?.[0] || ''),
}
"
:label="
collapsed ? '' : user?.first_name + ' ' + user?.last_name || ''
"
color="neutral"
variant="ghost"
class="w-full"
:block="collapsed"
/></UTooltip>
</div> </div>
</template> </template>
<template #footer="{ collapsed }"> <template #footer>
<div> <div>
<UButton
:avatar="{ src: '' }"
:label="collapsed ? undefined : 'مدیرفنی سامانه'"
color="neutral"
variant="ghost"
class="w-full"
:block="collapsed"
/>
<!-- دکمههای باز/بسته کردن سایدبار --> <!-- دکمههای باز/بسته کردن سایدبار -->
<div class="flex justify-center items-center gap-2 mt-2"> <div class="flex justify-center items-center gap-2 mt-4">
<!-- دکمه باز کردن --> <!-- دکمه باز کردن -->
<UButton <UButton
v-if="collapsed" v-if="collapsed"
@ -89,12 +107,85 @@
</div> </div>
</template> </template>
</UDashboardSidebar> </UDashboardSidebar>
<USlideover
v-model:open="sidebarOpenComputed"
side="right"
overlay
class="lg:hidden"
:dismissible="true"
dir="rtl"
:title="
useSystemTheme?.currentTheme?.value?.title +
' - ' +
useSystemTheme?.currentTheme?.value?.subTitle || ''
"
>
<template #body>
<div class="p-4 flex flex-col h-full">
<UNavigationMenu
orientation="vertical"
:items="filteredSidebar.topMenu"
@click="onMenuSelect"
dir="rtl"
/>
<div class="mt-auto">
<UNavigationMenu
orientation="vertical"
:items="filteredSidebar.bottomMenu"
@click="onMenuSelect"
dir="rtl"
/>
</div>
</div>
</template>
<template #footer="{ collapsed }">
<div>
<UButton
:avatar="
user?.avatar
? { src: user?.avatar }
: {
label:
(user?.first_name?.[0] || '') +
(user?.last_name?.[0] || ''),
}
"
:label="
collapsed ? '' : user?.first_name + ' ' + user?.last_name || ''
"
color="neutral"
variant="ghost"
class="w-full"
:block="collapsed"
/>
</div>
</template>
</USlideover>
</template> </template>
<script setup> <script setup>
import { ref} from "vue"; import { ref, onMounted } from "vue";
import { composSystemTheme } from "@/composables/composSystemTheme"; import { composSystemTheme } from "@/composables/composSystemTheme";
import { storeToRefs } from "pinia";
import { useCommonStore } from "@/stores/commonStore";
const commonStore = useCommonStore();
const { sidebarOpen } = storeToRefs(commonStore);
// proxy امن برای v-model:open
const sidebarOpenComputed = computed({
get: () => sidebarOpen.value,
set: (v) => (commonStore.sidebarOpen = v),
});
const filteredSidebar = computed(
() => props.sidebarItems || { topMenu: [], bottomMenu: [] },
);
function onMenuSelect() {
// وقتی کاربر آیتم منو رو زد، اسلایداور رو ببند
commonStore.closeSidebar();
}
const useSystemTheme = composSystemTheme(); const useSystemTheme = composSystemTheme();
const props = defineProps({ const props = defineProps({
@ -110,27 +201,35 @@ const props = defineProps({
const emit = defineEmits(["update:collapsed"]); const emit = defineEmits(["update:collapsed"]);
const logoSrc = ref("");
const collapsed = ref(false); const collapsed = ref(false);
const user = ref({ first_name: "", last_name: "", avatar: "" });
const onCollapse = (value) => { const onCollapse = (value) => {
collapsed.value = value; collapsed.value = value;
emit("update:collapsed", value); emit("update:collapsed", value);
}; };
function getSideBarSchema() {
function getSideBarSchema(){
const config = useRuntimeConfig(); const config = useRuntimeConfig();
const IS_DEVLOP_MODE = config.public.IS_DEVLOP_MODE || 1; const NUXT_PUBLIC_IS_DEVLOP_MODE = Number(
config.public?.NUXT_PUBLIC_IS_DEVLOP_MODE ?? 1,
);
let result = {} const filterMenu = (menu) => {
if (NUXT_PUBLIC_IS_DEVLOP_MODE === 1) return menu || [];
return (
menu?.filter(
(el) => el.develop === undefined || Number(el.develop) === 0,
) || []
);
};
result.topMenu = props.sidebarItems?.topMenu.filter((el) => (!el.develop || el.develop == IS_DEVLOP_MODE) ) const topMenuFiltered = filterMenu(props.sidebarItems?.topMenu);
result.bottomMenu = props.sidebarItems?.bottomMenu.filter((el) => (!el.develop || el.develop == IS_DEVLOP_MODE) ) const bottomMenuFiltered = filterMenu(props.sidebarItems?.bottomMenu);
// console.log("SideBar IS_DEVLOP_MODE ", IS_DEVLOP_MODE, result); return {
topMenu: topMenuFiltered,
return result bottomMenu: bottomMenuFiltered,
};
} }
// تابع برای باز/بسته کردن نرم سایدبار // تابع برای باز/بسته کردن نرم سایدبار
@ -140,8 +239,6 @@ const toggleSidebar = () => {
}; };
const sidebarUI = { const sidebarUI = {
width: "w-72",
collapsed: { width: "w-16" },
wrapper: wrapper:
"z-30 h-[calc(100vh-64px)] top-16 rounded-r-2xl border-r border-gray-200/50 dark:border-dark-primary-800/50 backdrop-blur-sm transition-all duration-300", "z-30 h-[calc(100vh-64px)] top-16 rounded-r-2xl border-r border-gray-200/50 dark:border-dark-primary-800/50 backdrop-blur-sm transition-all duration-300",
base: "backdrop-blur-sm transition-all duration-300 ease-out", base: "backdrop-blur-sm transition-all duration-300 ease-out",
@ -158,6 +255,21 @@ const navigationUI = {
"bg-gradient-to-r from-blue-100 to-blue-50/30 dark:from-blue-900/30 dark:to-blue-900/10 text-blue-600 dark:text-blue-400 font-semibold shadow-sm", "bg-gradient-to-r from-blue-100 to-blue-50/30 dark:from-blue-900/30 dark:to-blue-900/10 text-blue-600 dark:text-blue-400 font-semibold shadow-sm",
icon: { base: "transition-transform duration-300", active: "scale-110" }, icon: { base: "transition-transform duration-300", active: "scale-110" },
}; };
onMounted(() => {
useSystemTheme.applyTheme();
setTimeout(() => {
logoSrc.value = useSystemTheme.logo.value;
}, 300);
const stored = localStorage.getItem("user");
if (stored) {
try {
user.value = JSON.parse(stored);
} catch (e) {
console.error("خطا در خواندن کاربر از localStorage:", e);
}
}
});
</script> </script>
<style scoped> <style scoped>

View File

@ -1,124 +1,21 @@
<template> <template>
<div> <div>
<!-- تولبار ادیتور --> <!-- تولبار ادیتور -->
<div v-if="editor" class="editor-toolbar"> <div class="editor-toolbar" v-if="editor">
<div class="toolbar-group"> <div
class="toolbar-group"
v-for="(group, gIndex) in toolbarGroups"
:key="gIndex"
>
<button <button
@click="editor.chain().focus().toggleBold().run()" v-for="(btn, index) in group"
:class="{ 'is-active': editor.isActive('bold') }" :key="index"
title="بولد" @click="btn.action(editor)"
:class="{ 'is-active': btn.isActive ? btn.isActive(editor) : false }"
:disabled="btn.disabled ? !btn.disabled(editor) : false"
:title="btn.title"
> >
<span class="toolbar-icon">𝐁</span> <span class="toolbar-icon">{{ btn.icon }}</span>
</button>
<button
@click="editor.chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
title="ایتالیک"
>
<span class="toolbar-icon">𝐼</span>
</button>
<button
@click="editor.chain().focus().toggleUnderline().run()"
:class="{ 'is-active': editor.isActive('underline') }"
title="زیرخط"
>
<span class="toolbar-icon">𝑈</span>
</button>
<button
@click="editor.chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
title="خط خورده"
>
<span class="toolbar-icon">̶S̶</span>
</button>
</div>
<div class="toolbar-group">
<button
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
title="عنوان ۱"
>
H1
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
title="عنوان ۲"
>
H2
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
title="عنوان ۳"
>
H3
</button>
</div>
<div class="toolbar-group">
<button
@click="editor.chain().focus().toggleBulletList().run()"
:class="{ 'is-active': editor.isActive('bulletList') }"
title="لیست بولت‌دار"
>
<span class="toolbar-icon"></span>
</button>
<button
@click="editor.chain().focus().toggleOrderedList().run()"
:class="{ 'is-active': editor.isActive('orderedList') }"
title="لیست شماره‌دار"
>
<span class="toolbar-icon">1.</span>
</button>
<button
@click="editor.chain().focus().toggleBlockquote().run()"
:class="{ 'is-active': editor.isActive('blockquote') }"
title="نقل قول"
>
<span class="toolbar-icon">"</span>
</button>
<button
@click="editor.chain().focus().toggleCodeBlock().run()"
:class="{ 'is-active': editor.isActive('codeBlock') }"
title="کد"
>
<span class="toolbar-icon">{ }</span>
</button>
</div>
<div class="toolbar-group">
<button
@click="editor.chain().focus().setDetails().run()"
:disabled="!editor.can().setDetails()"
title="افزودن جزییات"
>
<span class="toolbar-icon">📋</span>
</button>
<button
@click="editor.chain().focus().unsetDetails().run()"
:disabled="!editor.can().unsetDetails()"
title="حذف جزییات"
>
<span class="toolbar-icon">🗑</span>
</button>
</div>
<div class="toolbar-group">
<button
@click="editor.chain().focus().undo().run()"
:disabled="!editor.can().undo()"
title="بازگشت"
>
<span class="toolbar-icon"></span>
</button>
<button
@click="editor.chain().focus().redo().run()"
:disabled="!editor.can().redo()"
title="جلو"
>
<span class="toolbar-icon"></span>
</button> </button>
</div> </div>
</div> </div>
@ -158,6 +55,116 @@ function isList(text) {
/^\d+\./.test(line.trim()), /^\d+\./.test(line.trim()),
); );
} }
const toolbarGroups = [
// گروه فرمت متن
[
{
title: "بولد",
icon: "𝐁",
action: (editor) => editor.chain().focus().toggleBold().run(),
isActive: (editor) => editor.isActive("bold"),
},
{
title: "ایتالیک",
icon: "𝐼",
action: (editor) => editor.chain().focus().toggleItalic().run(),
isActive: (editor) => editor.isActive("italic"),
},
{
title: "زیرخط",
icon: "𝑈",
action: (editor) => editor.chain().focus().toggleUnderline().run(),
isActive: (editor) => editor.isActive("underline"),
},
{
title: "خط خورده",
icon: "̶S̶",
action: (editor) => editor.chain().focus().toggleStrike().run(),
isActive: (editor) => editor.isActive("strike"),
},
],
// گروه هدینگها
[
{
title: "عنوان ۱",
icon: "H1",
action: (editor) =>
editor.chain().focus().toggleHeading({ level: 1 }).run(),
isActive: (editor) => editor.isActive("heading", { level: 1 }),
},
{
title: "عنوان ۲",
icon: "H2",
action: (editor) =>
editor.chain().focus().toggleHeading({ level: 2 }).run(),
isActive: (editor) => editor.isActive("heading", { level: 2 }),
},
{
title: "عنوان ۳",
icon: "H3",
action: (editor) =>
editor.chain().focus().toggleHeading({ level: 3 }).run(),
isActive: (editor) => editor.isActive("heading", { level: 3 }),
},
],
// گروه لیست و نقل قول
[
{
title: "لیست بولت‌دار",
icon: "•",
action: (editor) => editor.chain().focus().toggleBulletList().run(),
isActive: (editor) => editor.isActive("bulletList"),
},
{
title: "لیست شماره‌دار",
icon: "1.",
action: (editor) => editor.chain().focus().toggleOrderedList().run(),
isActive: (editor) => editor.isActive("orderedList"),
},
{
title: "نقل قول",
icon: '"',
action: (editor) => editor.chain().focus().toggleBlockquote().run(),
isActive: (editor) => editor.isActive("blockquote"),
},
{
title: "کد",
icon: "{ }",
action: (editor) => editor.chain().focus().toggleCodeBlock().run(),
isActive: (editor) => editor.isActive("codeBlock"),
},
],
// گروه جزییات
[
{
title: "افزودن جزییات",
icon: "📋",
action: (editor) => editor.chain().focus().setDetails().run(),
disabled: (editor) => !editor.can().setDetails(),
},
{
title: "حذف جزییات",
icon: "🗑️",
action: (editor) => editor.chain().focus().unsetDetails().run(),
disabled: (editor) => !editor.can().unsetDetails(),
},
],
// گروه Undo/Redo
[
{
title: "بازگشت",
icon: "↩",
action: (editor) => editor.chain().focus().undo().run(),
disabled: (editor) => !editor.can().undo(),
},
{
title: "جلو",
icon: "↪",
action: (editor) => editor.chain().focus().redo().run(),
disabled: (editor) => !editor.can().redo(),
},
],
];
function formatListToHtml(text) { function formatListToHtml(text) {
const lines = text.split("\n").filter((line) => line.trim() !== ""); const lines = text.split("\n").filter((line) => line.trim() !== "");

View File

@ -7,12 +7,6 @@
"icon": "i-lucide-list", "icon": "i-lucide-list",
"active": true "active": true
}, },
{
"id": "RuleEdit",
"label": "احکام",
"key": "RuleEdit",
"icon": "i-lucide-scroll-text"
},
{ {
"id": "RelationEdit", "id": "RelationEdit",
"key": "RelationEdit", "key": "RelationEdit",