first commit
This commit is contained in:
commit
cc647ffaba
7
.env
Executable file
7
.env
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
NUXT_PUBLIC_API_NAME=api/
|
||||
|
||||
IS_DEVLOP_MODE=1
|
||||
|
||||
NUXT_PUBLIC_SYSTEM=monir
|
||||
NUXT_PUBLIC_BASE_URL=http://192.168.23.60/
|
||||
NUXT_PUBLIC_BASE_URL2=https://hamfahmi.ir/
|
||||
9
.env.db
Executable file
9
.env.db
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
# Active System
|
||||
NUXT_PUBLIC_SYSTEM=monir
|
||||
|
||||
IS_DEVLOP_MODE=0
|
||||
|
||||
# (اختیاری – اگر بعداً خواستی)
|
||||
NUXT_PUBLIC_APP_NAME=Monir System
|
||||
NUXT_PUBLIC_BASE_URL=https://asr.hamfahmi.ir/
|
||||
# NUXT_PUBLIC_BASE_URL2=https://hamfahmi.ir/
|
||||
9
.env.monir
Executable file
9
.env.monir
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
# Active System
|
||||
NUXT_PUBLIC_SYSTEM=monir
|
||||
|
||||
IS_DEVLOP_MODE=1
|
||||
|
||||
# (اختیاری – اگر بعداً خواستی)
|
||||
NUXT_PUBLIC_APP_NAME=Monir Hamfahmi
|
||||
NUXT_PUBLIC_BASE_URL=http://192.168.23.60/
|
||||
# NUXT_PUBLIC_BASE_URL2=https://hamfahmi.ir/
|
||||
5
.env.tavasi
Executable file
5
.env.tavasi
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
# Active System
|
||||
NUXT_PUBLIC_SYSTEM=majles
|
||||
|
||||
# (اختیاری)
|
||||
NUXT_PUBLIC_APP_NAME=Majles System
|
||||
26
.gitignore
vendored
Executable file
26
.gitignore
vendored
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.vscode
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
node_modules*
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
# .env
|
||||
# .env.*
|
||||
# !.env.example
|
||||
248
README.md
Executable file
248
README.md
Executable file
|
|
@ -0,0 +1,248 @@
|
|||
# serve in windows
|
||||
|
||||
## install nvm , node
|
||||
install nvm in windows :
|
||||
- downlowd nvm-setup.exe : https://github.com/coreybutler/nvm-windows/releases
|
||||
- install nvm-setup.exe
|
||||
|
||||
install node 24.0.0 :
|
||||
nvm install 24.0.0
|
||||
nvm use 24.0.0
|
||||
|
||||
## install node_modules
|
||||
serve
|
||||
```
|
||||
git clone --recurse-submodules https://git2.tavasi.ir/nuxt_v4/front2.git
|
||||
cd front2
|
||||
npm install
|
||||
|
||||
npm run dev-monir
|
||||
```
|
||||
|
||||
# build to ubuntu server
|
||||
|
||||
#build in local windows :
|
||||
- ?? r&d ?? : #NUXT_PUBLIC_BASE_URL=http://192.168.23.160/
|
||||
- npm run build-monir
|
||||
-- r&d : زمان بیلد بسیار زیاد چرا ؟
|
||||
|
||||
sudo apt update
|
||||
sudo apt install -y curl git
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
||||
source ~/.bashrc
|
||||
nvm --version
|
||||
nvm install 24.0.0
|
||||
nvm use --lts
|
||||
nvm alias default lts/*
|
||||
npm install -g pm2
|
||||
PORT=3000 pm2 start .output/server/index.mjs --name "nuxt-app"
|
||||
pm2 save
|
||||
pm2 startup
|
||||
http://192.168.23.103:3000/dashboard/base
|
||||
|
||||
|
||||
install nvm in ubuntu :
|
||||
- ????
|
||||
|
||||
install node 24.0.0 :
|
||||
- ???
|
||||
|
||||
run server :
|
||||
1. npm install -g pm2
|
||||
2. pm2 start .output/server/index.mjs --name "nuxt-app"
|
||||
2. port : PORT=3003 pm2 start .output/server/index.mjs --name "nuxt-app"
|
||||
3. pm2 save
|
||||
4. pm2 startup
|
||||
|
||||
|
||||
restart :
|
||||
pm2 restart "???-app"
|
||||
|
||||
|
||||
|
||||
|
||||
####
|
||||
PORT=3000 HOST=0.0.0.0 pm2 start .output/server/index.mjs --name test-nuxt
|
||||
pm2 list
|
||||
pm2 delete test-nuxt
|
||||
|
||||
|
||||
میتونم یک **ردمی کامل برای نصب، آپدیت و اجرای Nuxt روی اوبونتو** بر اساس توضیحات شما آماده کنم. این شامل نصب NVM، Node.js، PM2 و راهاندازی سرور است:
|
||||
|
||||
---
|
||||
|
||||
# راهنمای نصب و اجرا روی Ubuntu
|
||||
|
||||
### ۱. بهروزرسانی سیستم و نصب پیشنیازها
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y curl git build-essential
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۲. نصب NVM (Node Version Manager)
|
||||
|
||||
```bash
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
||||
source ~/.bashrc
|
||||
nvm --version
|
||||
```
|
||||
|
||||
> اگر بعد از `source ~/.bashrc` دستور `nvm` را نشناخت، ترمینال را ببندید و دوباره باز کنید.
|
||||
|
||||
---
|
||||
|
||||
### ۳. نصب Node.js 24 و انتخاب آن به عنوان پیشفرض
|
||||
|
||||
```bash
|
||||
nvm install 24
|
||||
nvm use 24
|
||||
nvm alias default 24
|
||||
node -v
|
||||
npm -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۴. نصب PM2 برای مدیریت فرآیندها
|
||||
|
||||
```bash
|
||||
npm install -g pm2
|
||||
pm2 -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۵. کلون پروژه Nuxt و نصب وابستگیها
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://git2.tavasi.ir/nuxt_v4/base_ui.git
|
||||
cd base_ui
|
||||
npm install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۶. اجرای محیط توسعه (Development)
|
||||
|
||||
```bash
|
||||
npm run dev-monir
|
||||
```
|
||||
|
||||
> سرور روی `http://localhost:3000` یا IP ماشین شما قابل دسترسی است.
|
||||
|
||||
---
|
||||
|
||||
### ۷. ساخت برنامه برای تولید (Build)
|
||||
|
||||
```bash
|
||||
# اگر نیاز به تغییر متغیر محیطی دارید:
|
||||
export NUXT_PUBLIC_BASE_URL=http://192.168.23.160/
|
||||
|
||||
npm run build-monir
|
||||
```
|
||||
|
||||
> ⚠️ زمان بیلد طولانی میتواند به دلیل حجم پروژه یا سیستم باشد. برای کاهش زمان، از `pnpm` یا استفاده از Docker cache هم میتوان کمک گرفت.
|
||||
|
||||
---
|
||||
|
||||
### ۸. اجرای برنامه روی سرور با PM2 (Production)
|
||||
|
||||
```bash
|
||||
# نصب اگر قبلا انجام نشده:
|
||||
npm install -g pm2
|
||||
|
||||
# اجرای برنامه با پورت مشخص
|
||||
PORT=3000 HOST=0.0.0.0 pm2 start .output/server/index.mjs --name "nuxt-app"
|
||||
|
||||
# ذخیره وضعیت pm2 برای اجرای خودکار پس از ریبوت
|
||||
pm2 save
|
||||
pm2 startup
|
||||
```
|
||||
|
||||
> بعد از اجرای `pm2 startup`، دستور نمایش داده شده را کپی و اجرا کنید تا PM2 روی بوت اوبونتو فعال شود.
|
||||
|
||||
---
|
||||
|
||||
### ۹. دسترسی
|
||||
|
||||
به مرورگر بروید و آدرس زیر را باز کنید:
|
||||
|
||||
```
|
||||
http://<IP-Server>:3000/dashboard/base
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
💡 **نکات تکمیلی:**
|
||||
|
||||
* برای بروزرسانی Node.js:
|
||||
|
||||
```bash
|
||||
nvm install 24 --reinstall-packages-from=24
|
||||
nvm use 24
|
||||
```
|
||||
|
||||
* اگر میخواهید تغییرات در کد بدون ریستارت دستی سرور اعمال شود، از `pm2 reload nuxt-app` استفاده کنید.
|
||||
|
||||
|
||||
|
||||
|
||||
٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫ جایگذینی ورژن جدید ٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫٫
|
||||
|
||||
1.
|
||||
npm run build {buid name project}
|
||||
|
||||
2.
|
||||
نصب نرم افزار winscp و لاگین کردن در سرور و جایگذینی فایل output جدید
|
||||
|
||||
3.
|
||||
با دستورات زیر در ترمینال جایگذاری انجام میگردد
|
||||
|
||||
ssh sabr@192.168.23.103
|
||||
|
||||
sabr@frant:~$ pm2 restart nuxt-app
|
||||
|
||||
|
||||
|
||||
|
||||
# نکاتی برای استفاده از npm mirror
|
||||
|
||||
|
||||
### سایتها
|
||||
|
||||
https://parswebserver.com/mirror-storages-for-pip/
|
||||
|
||||
https://mirror-npm.runflare.com/
|
||||
https://mirror-pypi.runflare.com/
|
||||
https://archive.ito.gov.ir/npm/
|
||||
|
||||
|
||||
npm install --registry https://registry.npmmirror.com express
|
||||
|
||||
npm config set registry "https://archive.ito.gov.ir/npm/"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
>> npm config list
|
||||
|
||||
; "project" config from H:\my_mindmap\my-nuxt-project\.npmrc
|
||||
|
||||
@baghi330:registry = "https://npm.pkg.github.com/"
|
||||
//npm.pkg.github.com/:_authToken = (protected)
|
||||
|
||||
; node bin location = C:\Program Files\nodejs\node.exe
|
||||
; node version = v22.13.1
|
||||
; npm local prefix = H:\my_mindmap\my-nuxt-project
|
||||
; npm version = 11.3.0
|
||||
; cwd = H:\my_mindmap\my-nuxt-project
|
||||
; HOME = C:\Users\user
|
||||
; Run `npm config ls -l` to show all defaults.
|
||||
20
app/app.config.ts
Executable file
20
app/app.config.ts
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
// app.config.ts
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
colors: {
|
||||
primary: "primary",
|
||||
secondary: "purple",
|
||||
neutral: "zinc",
|
||||
},
|
||||
button: {
|
||||
slots: {
|
||||
base: ["cursor-pointer"],
|
||||
},
|
||||
defaultVariants: {
|
||||
color: "primary",
|
||||
variant: "solid",
|
||||
size: "lg", // ← همه دکمهها پیشفرض lg میشن
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
40
app/app.vue
Executable file
40
app/app.vue
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
<!-- app.vue -->
|
||||
<template>
|
||||
<UApp>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</UApp>
|
||||
<ConfirmModal />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useHead } from "#imports";
|
||||
import { onMounted } from "vue";
|
||||
import { composSystemTheme } from "~/composables/composSystemTheme";
|
||||
// تنظیم تم سیستم
|
||||
useHead({
|
||||
script: [
|
||||
{
|
||||
innerHTML: `
|
||||
(function() {
|
||||
var mode = localStorage.getItem('theme-mode');
|
||||
if (mode === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
})();
|
||||
`,
|
||||
type: "text/javascript",
|
||||
tagPriority: -1,
|
||||
},
|
||||
],
|
||||
});
|
||||
const { applyTheme } = composSystemTheme();
|
||||
onMounted(() => {
|
||||
applyTheme();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
75
app/assets/css/main.css
Executable file
75
app/assets/css/main.css
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
/* app/assets/css/main.css */
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@theme static {
|
||||
/* light-colors */
|
||||
--color-light-primary: #fff;
|
||||
--color-text__orange: #e86c6b;
|
||||
--color-link-color: #2563eb;
|
||||
|
||||
/* dark-colors */
|
||||
--color-dark-primary: #111827;
|
||||
--color-dark-primary-800: #1f2937;
|
||||
--color-dark-primary-700: #374151;
|
||||
--color-dark-primary-600: #4b5563;
|
||||
--color-dark-primary-500: #6b7280;
|
||||
--color-dark-primary-400: #9ca3af;
|
||||
--color-dark-primary-300: #d1d5db;
|
||||
--color-dark-primary-200: #e5e7eb;
|
||||
--color-dark-primary-100: #f3f4f6;
|
||||
--color-dark-primary-50: #f9fafb;
|
||||
|
||||
/* رنگ متن معمولی */
|
||||
--color-white-normal: #ffffff; /* متن سفید معمولی */
|
||||
--color-black-normal: #111827; /* متن مشکی معمولی */
|
||||
|
||||
/* متن سفید */
|
||||
--color-white-bold: #ffffff; /* متن پررنگ سفید */
|
||||
--color-white-light: rgba(255, 255, 255, 0.6); /* متن کمرنگ سفید */
|
||||
--color-white-strong: #ffffff; /* متن بولد سفید */
|
||||
|
||||
/* متن خاکستری / دارک */
|
||||
--color-dark-normal: #e5e7eb; /* متن خاکستری روشن */
|
||||
--color-dark-bold: #111827; /* متن روشنتر و بولد */
|
||||
--color-dark-light: #6b7280; /* متن کمرنگ خاکستری */
|
||||
--color-dark-strong: #ffffff; /* متن بولد خاکستری روشن */
|
||||
|
||||
/* متن غیر فعال */
|
||||
--color-text-disabled: #9ca3af;
|
||||
|
||||
/* رنگ های دلخواه دیگر */
|
||||
--color-text-orange: #e86c6b;
|
||||
--color-link-color: #2563eb;
|
||||
}
|
||||
|
||||
/* تعریف رنگ دلخواه */
|
||||
/* primary-colors */
|
||||
/* --color-primary: #00b6e3; */
|
||||
/* --color-primary-50: #e0f6fb;
|
||||
--color-primary-100: #b3e9f7;
|
||||
--color-primary-200: #80d8f3;
|
||||
--color-primary-300: #4dc6ef;
|
||||
--color-primary-400: #1ab4eb;
|
||||
--color-primary-500: #00b6e3;
|
||||
--color-primary-600: #0097b0;
|
||||
--color-primary-700: #007580;
|
||||
--color-primary-800: #005250;
|
||||
--color-primary-900: #002f30;
|
||||
--color-primary-950: #001519; */
|
||||
|
||||
* {
|
||||
font-family: var(--app-font), sans-serif !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#__nuxt,
|
||||
.root {
|
||||
font-family: var(--app-font), sans-serif !important;
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
button,a {
|
||||
cursor: pointer;
|
||||
}
|
||||
37
app/assets/majles/theme.json
Executable file
37
app/assets/majles/theme.json
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "MajlesSystem",
|
||||
"title": "قانون یار",
|
||||
"subTitle": "مرجع رسمی قوانین و مقررات کشور",
|
||||
"logo": {
|
||||
"light": "/logo/majles/light_logo.png",
|
||||
"dark": "/logo/majles/dark_logo.png"
|
||||
},
|
||||
"font": "sahel",
|
||||
"fontFiles": [
|
||||
{
|
||||
"weight": "normal",
|
||||
"style": "normal",
|
||||
"src": "/fonts/sahel/Sahel-SemiBold.woff2"
|
||||
},
|
||||
{
|
||||
"weight": "bold",
|
||||
"style": "normal",
|
||||
"src": "/fonts/sahel/Sahel-Bold.woff2"
|
||||
}
|
||||
],
|
||||
"colors": {
|
||||
"primary": {
|
||||
"50": "#e6fafa",
|
||||
"100": "#c8f5f5",
|
||||
"200": "#a6f0f0",
|
||||
"300": "#80ebeb",
|
||||
"400": "#57e6e6",
|
||||
"500": "#00baba",
|
||||
"600": "#009b9b",
|
||||
"700": "#007b7b",
|
||||
"800": "#005c5c",
|
||||
"900": "#003e3e",
|
||||
"950": "#002020"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
app/assets/monir/logo.png
Executable file
BIN
app/assets/monir/logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
42
app/assets/monir/theme.json
Executable file
42
app/assets/monir/theme.json
Executable file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "MonirSystem",
|
||||
"title": "هم فهمی",
|
||||
"subTitle": "زیست بوم پژوهشگران",
|
||||
"logo": {
|
||||
"light": "/logo/monir/logo.png",
|
||||
"dark": "/logo/monir/logo.png"
|
||||
},
|
||||
"font": "Vazirmatn",
|
||||
"fontFiles": [
|
||||
{
|
||||
"weight": "normal",
|
||||
"style": "normal",
|
||||
"src": "/fonts/vazir/Vazirmatn-Regular.woff2"
|
||||
},
|
||||
{
|
||||
"weight": "bold",
|
||||
"style": "normal",
|
||||
"src": "/fonts/vazir/Vazirmatn-Bold.woff2"
|
||||
},
|
||||
{
|
||||
"weight": "500",
|
||||
"style": "normal",
|
||||
"src": "/fonts/vazir/Vazirmatn-Medium.woff2"
|
||||
}
|
||||
],
|
||||
"colors": {
|
||||
"primary": {
|
||||
"50": "#e6f8fd",
|
||||
"100": "#cceffd",
|
||||
"200": "#99e0fa",
|
||||
"300": "#66d1f7",
|
||||
"400": "#33c2f0",
|
||||
"500": "#00b6e3",
|
||||
"600": "#0092b8",
|
||||
"700": "#006f8d",
|
||||
"800": "#004c62",
|
||||
"900": "#002937",
|
||||
"950": "#00151d"
|
||||
}
|
||||
}
|
||||
}
|
||||
207
app/components/auto-import/BaseModal.vue
Executable file
207
app/components/auto-import/BaseModal.vue
Executable file
|
|
@ -0,0 +1,207 @@
|
|||
<template>
|
||||
<UModal
|
||||
v-model:open="isModalOpen"
|
||||
:title="localModalSchema.title || ''"
|
||||
:description="localModalSchema.description || ''"
|
||||
:ui="modalUi"
|
||||
:overlay="localModalSchema.overlay !== false"
|
||||
:dismissible="localModalSchema.dismissible !== false"
|
||||
:scrollable="localModalSchema.scrollable || false"
|
||||
:fullscreen="localModalSchema.fullscreen || false"
|
||||
>
|
||||
<!-- Body -->
|
||||
<template #body>
|
||||
<slot name="modal-body-content" />
|
||||
</template>
|
||||
|
||||
<!-- Footer -->
|
||||
<template v-if="hasFooter" #footer>
|
||||
<div class="flex w-full flex-wrap items-center justify-between gap-3">
|
||||
<!-- right actions -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<UButton
|
||||
v-for="action in rightActions"
|
||||
:key="action.key"
|
||||
v-bind="resolveActionProps(action)"
|
||||
@click="onActionClick(action)"
|
||||
size="lg"
|
||||
class="dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- left actions -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UButton
|
||||
v-for="action in leftActions"
|
||||
:key="action.key"
|
||||
v-bind="resolveActionProps(action)"
|
||||
@click="onActionClick(action)"
|
||||
size="lg"
|
||||
class="dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="footerText" class="mt-2 text-sm text-gray-600">
|
||||
{{ footerText }}
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from "vue";
|
||||
|
||||
/* ---------------- props & emits ---------------- */
|
||||
|
||||
const props = defineProps({
|
||||
modalSchema: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
title: "مدال",
|
||||
description: "",
|
||||
size: "lg",
|
||||
footerDescription: "",
|
||||
actions: {
|
||||
left: [
|
||||
{
|
||||
key: "close",
|
||||
label: "بستن",
|
||||
variant: "outline",
|
||||
closeOnClick: true,
|
||||
},
|
||||
{
|
||||
key: "save",
|
||||
label: "ذخیره",
|
||||
color: "primary",
|
||||
closeOnClick: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["modal-action", "update-isOpen", "modal-close"]);
|
||||
|
||||
/* ---------------- state ---------------- */
|
||||
|
||||
const isModalOpen = ref(props.isOpen);
|
||||
const footerText = ref("");
|
||||
|
||||
/* ---------------- computed ---------------- */
|
||||
|
||||
const localModalSchema = computed(() => props.modalSchema || {});
|
||||
|
||||
const actions = computed(() => localModalSchema.value.actions || {});
|
||||
const leftActions = computed(() => {
|
||||
const actionsList = actions.value.left || [];
|
||||
return actionsList.filter((action) => action && action.key); // فیلتر کردن actions خالی
|
||||
});
|
||||
const rightActions = computed(() => {
|
||||
const actionsList = actions.value.right || [];
|
||||
return actionsList.filter((action) => action && action.key); // فیلتر کردن actions خالی
|
||||
});
|
||||
|
||||
const hasFooter = computed(
|
||||
() => leftActions.value.length > 0 || rightActions.value.length > 0,
|
||||
);
|
||||
|
||||
/* -------- modal width by size -------- */
|
||||
|
||||
const modalWidthMap = {
|
||||
sm: "max-w-[400px]",
|
||||
md: "max-w-[600px]",
|
||||
lg: "max-w-[900px]",
|
||||
xl: "max-w-[1200px]",
|
||||
};
|
||||
|
||||
const modalUi = computed(() => {
|
||||
const size = localModalSchema.value.size || "md";
|
||||
const widthClass = modalWidthMap[size] || modalWidthMap.md;
|
||||
|
||||
return {
|
||||
...localModalSchema.value.ui,
|
||||
content: [widthClass, localModalSchema.value.ui?.content]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
header: [
|
||||
localModalSchema.value.ui?.header,
|
||||
"bg-gray-50 dark:bg-dark-primary",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
footer: [
|
||||
localModalSchema.value.ui?.footer,
|
||||
"bg-gray-50 dark:bg-dark-primary",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
};
|
||||
});
|
||||
|
||||
/* ---------------- watchers ---------------- */
|
||||
|
||||
/* sync parent → modal */
|
||||
watch(
|
||||
() => props.isOpen,
|
||||
(val) => {
|
||||
isModalOpen.value = val;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/* sync modal → parent (❌ / ESC / overlay) */
|
||||
watch(isModalOpen, (newVal) => {
|
||||
emit("update-isOpen", newVal);
|
||||
|
||||
// اگر مودال بسته شد، event مخصوص close هم emit کن
|
||||
if (newVal === false) {
|
||||
emit("modal-close");
|
||||
}
|
||||
});
|
||||
|
||||
// /* وقتی overlay یا ESC بزند */
|
||||
// function onOverlayClick() {
|
||||
// emit("modal-action", {
|
||||
// action: "overlay-close",
|
||||
// });
|
||||
// }
|
||||
|
||||
/* footer description */
|
||||
watch(
|
||||
() => localModalSchema.value.footerDescription,
|
||||
(val) => {
|
||||
footerText.value = val || "";
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/* ---------------- methods ---------------- */
|
||||
|
||||
function onActionClick(action) {
|
||||
// همیشه event را emit کن (حتی برای close)
|
||||
emit("modal-action", action.key);
|
||||
|
||||
// اگر دکمه باید مودال را ببندد
|
||||
if (action.closeOnClick) {
|
||||
isModalOpen.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveActionProps(action) {
|
||||
return {
|
||||
label: action.label,
|
||||
icon: action.icon,
|
||||
color: action.color || "gray",
|
||||
variant: action.variant || "solid",
|
||||
size: action.size || "sm",
|
||||
loading: action.loading || false,
|
||||
disabled: action.disabled || false,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
521
app/components/auto-import/BlockMenu.vue
Executable file
521
app/components/auto-import/BlockMenu.vue
Executable file
|
|
@ -0,0 +1,521 @@
|
|||
<template>
|
||||
<div v-if="visible" class="block-menu" :style="menuStyle" @click.stop>
|
||||
<!-- هدر بلوک -->
|
||||
<div class="block-header">
|
||||
<div class="block-type">
|
||||
<span class="block-icon">{{ blockIcon }}</span>
|
||||
<span class="block-name">{{ blockTypeName }}</span>
|
||||
</div>
|
||||
<button class="close-btn" @click="closeMenu">×</button>
|
||||
</div>
|
||||
|
||||
<!-- تنظیمات بلوک -->
|
||||
<div class="menu-section">
|
||||
<div class="section-title">تنظیمات بلوک</div>
|
||||
|
||||
<!-- رنگ بلوک -->
|
||||
<div class="block-color-picker">
|
||||
<div class="color-label">رنگ:</div>
|
||||
<div class="color-grid">
|
||||
<button
|
||||
v-for="color in blockColors"
|
||||
:key="color.value"
|
||||
class="color-option"
|
||||
:class="{ selected: blockColor === color.value }"
|
||||
:style="{ backgroundColor: color.value }"
|
||||
@click="handleAction('color', color.value)"
|
||||
:title="color.name"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ترازبندی -->
|
||||
<div class="block-alignment">
|
||||
<div class="alignment-label">تراز:</div>
|
||||
<div class="alignment-buttons">
|
||||
<button
|
||||
v-for="align in alignments"
|
||||
:key="align.value"
|
||||
class="align-btn"
|
||||
:class="{ selected: blockAlign === align.value }"
|
||||
@click="handleAction('align', align.value)"
|
||||
:title="align.name"
|
||||
>
|
||||
{{ align.icon }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- تبدیل نوع بلوک -->
|
||||
<div class="menu-section">
|
||||
<div class="section-title">تبدیل به</div>
|
||||
<div class="block-types-grid">
|
||||
<button
|
||||
v-for="type in availableBlockTypes"
|
||||
:key="type.id"
|
||||
class="type-btn"
|
||||
:class="{ current: blockType === type.id }"
|
||||
@click="handleAction('convert', type.id)"
|
||||
>
|
||||
<span class="type-icon">{{ type.icon }}</span>
|
||||
<span class="type-name">{{ type.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- عملیات بلوک -->
|
||||
<div class="menu-section">
|
||||
<div class="section-title">عملیات</div>
|
||||
<button class="menu-item" @click="handleAction('duplicate')">
|
||||
<span class="item-icon">📋</span>
|
||||
تکثیر بلوک
|
||||
<span class="hint">ایجاد کپی</span>
|
||||
</button>
|
||||
<button class="menu-item" @click="handleAction('comment')">
|
||||
<span class="item-icon">💬</span>
|
||||
افزودن نظر
|
||||
<span class="hint">کامنت</span>
|
||||
</button>
|
||||
<button class="menu-item" @click="handleAction('move-up')">
|
||||
<span class="item-icon">⬆️</span>
|
||||
انتقال به بالا
|
||||
</button>
|
||||
<button class="menu-item" @click="handleAction('move-down')">
|
||||
<span class="item-icon">⬇️</span>
|
||||
انتقال به پایین
|
||||
</button>
|
||||
<button
|
||||
class="menu-item text-danger"
|
||||
@click="handleAction('delete-block')"
|
||||
>
|
||||
<span class="item-icon">🗑️</span>
|
||||
حذف بلوک
|
||||
<span class="hint danger">برای همیشه</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- AI مخصوص بلوک -->
|
||||
<div class="menu-section">
|
||||
<div class="section-title">AI برای این بلوک</div>
|
||||
<button class="menu-item ai-item" @click="handleAction('ai-rewrite')">
|
||||
<span class="item-icon">✍️</span>
|
||||
بازنویسی هوشمند
|
||||
<span class="ai-badge">AI</span>
|
||||
</button>
|
||||
<button class="menu-item ai-item" @click="handleAction('ai-expand')">
|
||||
<span class="item-icon">🔍</span>
|
||||
گسترش محتوا
|
||||
<span class="ai-badge">AI</span>
|
||||
</button>
|
||||
<button class="menu-item ai-item" @click="handleAction('ai-simplify')">
|
||||
<span class="item-icon">📖</span>
|
||||
سادهسازی
|
||||
<span class="ai-badge">AI</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- تنظیمات پیشرفته -->
|
||||
<div class="menu-section">
|
||||
<button class="menu-item" @click="handleAction('settings')">
|
||||
<span class="item-icon">⚙️</span>
|
||||
تنظیمات پیشرفته
|
||||
</button>
|
||||
<button class="menu-item" @click="handleAction('export')">
|
||||
<span class="item-icon">📤</span>
|
||||
خروجی گرفتن
|
||||
</button>
|
||||
<button class="menu-item" @click="handleAction('history')">
|
||||
<span class="item-icon">🕒</span>
|
||||
مشاهده تاریخچه
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
visible: Boolean,
|
||||
position: {
|
||||
type: Object,
|
||||
default: () => ({ x: 0, y: 0 }),
|
||||
},
|
||||
blockType: {
|
||||
type: String,
|
||||
default: "paragraph",
|
||||
},
|
||||
blockData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close", "action"]);
|
||||
|
||||
const menuStyle = computed(() => ({
|
||||
top: `${props.position.y}px`,
|
||||
left: `${props.position.x}px`,
|
||||
display: props.visible ? "block" : "none",
|
||||
}));
|
||||
|
||||
// دادههای بلوک
|
||||
const blockColor = ref(props.blockData.color || "#ffffff");
|
||||
const blockAlign = ref(props.blockData.align || "right");
|
||||
|
||||
// آیکون و نام بلوک
|
||||
const blockIcon = computed(() => {
|
||||
const icons = {
|
||||
paragraph: "📝",
|
||||
heading1: "H1",
|
||||
heading2: "H2",
|
||||
heading3: "H3",
|
||||
todo: "✓",
|
||||
bullet: "•",
|
||||
number: "1.",
|
||||
code: "{ }",
|
||||
quote: '"',
|
||||
image: "🖼️",
|
||||
file: "📎",
|
||||
table: "📊",
|
||||
callout: "💡",
|
||||
};
|
||||
return icons[props.blockType] || "📝";
|
||||
});
|
||||
|
||||
const blockTypeName = computed(() => {
|
||||
const names = {
|
||||
paragraph: "متن",
|
||||
heading1: "عنوان ۱",
|
||||
heading2: "عنوان ۲",
|
||||
heading3: "عنوان ۳",
|
||||
todo: "لیست کار",
|
||||
bullet: "لیست نقطهای",
|
||||
number: "لیست شمارهای",
|
||||
code: "کد",
|
||||
quote: "نقل قول",
|
||||
image: "تصویر",
|
||||
file: "فایل",
|
||||
table: "جدول",
|
||||
callout: "کالاوت",
|
||||
};
|
||||
return names[props.blockType] || "بلوک";
|
||||
});
|
||||
|
||||
// رنگهای بلوک
|
||||
const blockColors = [
|
||||
{ name: "پیشفرض", value: "#ffffff" },
|
||||
{ name: "آبی روشن", value: "#dbeafe" },
|
||||
{ name: "سبز روشن", value: "#dcfce7" },
|
||||
{ name: "زرد روشن", value: "#fef3c7" },
|
||||
{ name: "صورتی روشن", value: "#fce7f3" },
|
||||
{ name: "بنفش روشن", value: "#f3e8ff" },
|
||||
{ name: "خاکستری", value: "#f3f4f6" },
|
||||
];
|
||||
|
||||
// ترازبندی
|
||||
const alignments = [
|
||||
{ name: "راست", value: "right", icon: "←" },
|
||||
{ name: "وسط", value: "center", icon: "↔" },
|
||||
{ name: "چپ", value: "left", icon: "→" },
|
||||
];
|
||||
|
||||
// انواع بلوکهای قابل تبدیل
|
||||
const availableBlockTypes = [
|
||||
{ id: "paragraph", name: "متن", icon: "📝" },
|
||||
{ id: "heading1", name: "عنوان ۱", icon: "H1" },
|
||||
{ id: "heading2", name: "عنوان ۲", icon: "H2" },
|
||||
{ id: "heading3", name: "عنوان ۳", icon: "H3" },
|
||||
{ id: "todo", name: "لیست کار", icon: "✓" },
|
||||
{ id: "bullet", name: "لیست نقطهای", icon: "•" },
|
||||
{ id: "number", name: "لیست شمارهای", icon: "1." },
|
||||
{ id: "code", name: "کد", icon: "{ }" },
|
||||
{ id: "quote", name: "نقل قول", icon: '"' },
|
||||
{ id: "callout", name: "کالاوت", icon: "💡" },
|
||||
];
|
||||
|
||||
const handleAction = (action, value = null) => {
|
||||
emit("action", {
|
||||
action,
|
||||
value,
|
||||
blockType: props.blockType,
|
||||
blockData: props.blockData,
|
||||
});
|
||||
};
|
||||
|
||||
const closeMenu = () => {
|
||||
emit("close");
|
||||
};
|
||||
|
||||
// بستن منو با کلیک خارج
|
||||
const handleClickOutside = (event) => {
|
||||
if (!event.target.closest(".block-menu")) {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.block-menu {
|
||||
position: fixed;
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||
width: 320px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
z-index: 10001;
|
||||
animation: slideIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* هدر بلوک */
|
||||
.block-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
|
||||
.block-type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.block-icon {
|
||||
font-size: 24px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.block-name {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #6b7280;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
/* تنظیمات بلوک */
|
||||
.block-color-picker,
|
||||
.block-alignment {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.color-label,
|
||||
.alignment-label {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.color-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.color-option {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.color-option:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.color-option.selected {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.alignment-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.align-btn {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
background: #f3f4f6;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.align-btn:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.align-btn.selected {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
/* تبدیل نوع بلوک */
|
||||
.block-types-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.type-btn {
|
||||
padding: 12px 8px;
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.type-btn:hover {
|
||||
background: #f3f4f6;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.type-btn.current {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.type-name {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* آیتمهای منو */
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
font-size: 18px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.hint.danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.ai-badge {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
background: linear-gradient(135deg, #8b5cf6, #ec4899);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.ai-item {
|
||||
background: linear-gradient(135deg, #f5f3ff 0%, #fdf2f8 100%);
|
||||
}
|
||||
|
||||
.ai-item:hover {
|
||||
background: linear-gradient(135deg, #ede9fe 0%, #fce7f3 100%);
|
||||
}
|
||||
|
||||
.menu-item.text-danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.menu-item.text-danger:hover {
|
||||
background-color: #fef2f2;
|
||||
}
|
||||
|
||||
/* ریسپانسیو */
|
||||
@media (max-width: 768px) {
|
||||
.block-menu {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.block-types-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
75
app/components/auto-import/Breadcrumb.vue
Executable file
75
app/components/auto-import/Breadcrumb.vue
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const props = defineProps({
|
||||
breadcrumbData: Array, // مسیرهای اصلی
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
activeTabId: String, // تب فعال
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
let breadcrumbTabs = props.tabs;
|
||||
/**
|
||||
* پیدا کردن مسیر breadcrumb از breadcrumbData
|
||||
*/
|
||||
function findBreadcrumbPath(path, items, parentPath = "") {
|
||||
const result = [];
|
||||
for (const item of items) {
|
||||
const fullPath = item.to || parentPath;
|
||||
if (path.startsWith(fullPath)) {
|
||||
result.push({
|
||||
label: item.label,
|
||||
to: fullPath,
|
||||
icon: item.icon,
|
||||
children: item.children,
|
||||
});
|
||||
if (item.children) findBreadcrumbPath(item.children, fullPath);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const breadcrumbItems = computed(() => {
|
||||
if (route.path === "/dashboard/base") return [];
|
||||
|
||||
const crumbs = findBreadcrumbPath(route.path, props.breadcrumbData);
|
||||
|
||||
if (breadcrumbTabs.length) {
|
||||
let tabToShow = breadcrumbTabs[0]; // default tab
|
||||
|
||||
if (props.activeTabId) {
|
||||
const activeTab = breadcrumbTabs.find((t) => t.id === props.activeTabId);
|
||||
if (activeTab) tabToShow = activeTab;
|
||||
}
|
||||
|
||||
crumbs.push({
|
||||
label: tabToShow.label,
|
||||
to: undefined,
|
||||
icon: tabToShow.icon,
|
||||
});
|
||||
}
|
||||
|
||||
return crumbs;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UBreadcrumb
|
||||
v-if="breadcrumbItems.length"
|
||||
:items="breadcrumbItems"
|
||||
separator-icon="i-lucide-arrow-right"
|
||||
>
|
||||
<template #separator="{ ui }">
|
||||
<span class="mx-2 text-muted">/</span>
|
||||
</template>
|
||||
<template #item-label="{ item, active }">
|
||||
<span :class="active ? 'font-semibold text-primary' : ''">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</template>
|
||||
</UBreadcrumb>
|
||||
</template>
|
||||
45
app/components/auto-import/ConfirmModal.vue
Executable file
45
app/components/auto-import/ConfirmModal.vue
Executable file
|
|
@ -0,0 +1,45 @@
|
|||
<!-- ~/components/global/GlobalConfirm.vue -->
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="state.isOpen"
|
||||
class="fixed inset-0 z-[1001] flex items-center justify-center bg-black/40 pointer-events-auto"
|
||||
@click="actions.ucoCancelConfirmModal"
|
||||
>
|
||||
<div
|
||||
class="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full mx-4 pointer-events-auto"
|
||||
@click.stop
|
||||
>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
{{ state.title }}
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-6">
|
||||
{{ state.message }}
|
||||
</p>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition pointer-events-auto"
|
||||
@click="actions.ucoCancelConfirmModal"
|
||||
>
|
||||
انصراف
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-primary hover:bg-primary-700 rounded-md transition pointer-events-auto"
|
||||
@click="actions.ucoConfirm"
|
||||
>
|
||||
تأیید
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useConfirmState, useConfirmActions } from "~/composables/useConfirm";
|
||||
|
||||
const state = useConfirmState();
|
||||
const actions = useConfirmActions();
|
||||
</script>
|
||||
263
app/components/auto-import/ContextMenu.vue
Executable file
263
app/components/auto-import/ContextMenu.vue
Executable file
|
|
@ -0,0 +1,263 @@
|
|||
<template>
|
||||
<div v-if="visible" class="context-menu" :style="menuStyle" @click.stop>
|
||||
<!-- بخش AI (مشابه Notion AI) -->
|
||||
<div class="menu-section ai-section">
|
||||
<div class="section-title">
|
||||
<span class="ai-icon">✨</span>
|
||||
هوش مصنوعی نوشن
|
||||
</div>
|
||||
<button class="menu-item" @click="handleAction('ai-summarize')">
|
||||
<span class="item-icon">📝</span>
|
||||
خلاصهسازی
|
||||
<span class="shortcut">AI</span>
|
||||
</button>
|
||||
<button class="menu-item" @click="handleAction('ai-improve')">
|
||||
<span class="item-icon">✨</span>
|
||||
بهبود نوشتار
|
||||
<span class="shortcut">AI</span>
|
||||
</button>
|
||||
<button class="menu-item" @click="handleAction('ai-translate')">
|
||||
<span class="item-icon">🌍</span>
|
||||
ترجمه
|
||||
<span class="shortcut">AI</span>
|
||||
</button>
|
||||
<button class="menu-item" @click="handleAction('ai-explain')">
|
||||
<span class="item-icon">💡</span>
|
||||
توضیح
|
||||
<span class="shortcut">AI</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- بخش قالببندی -->
|
||||
<div class="menu-section">
|
||||
<div class="section-title">قالببندی</div>
|
||||
<button class="menu-item" @click="handleAction('format-bold')">
|
||||
<span class="item-icon">𝐁</span>
|
||||
پررنگ
|
||||
<span class="shortcut">Ctrl+B</span>
|
||||
</button>
|
||||
<button class="menu-item" @click="handleAction('format-italic')">
|
||||
<span class="item-icon">𝐼</span>
|
||||
کج
|
||||
<span class="shortcut">Ctrl+I</span>
|
||||
</button>
|
||||
<button class="menu-item" @click="handleAction('format-code')">
|
||||
<span class="item-icon">{ }</span>
|
||||
کد
|
||||
<span class="shortcut">Ctrl+E</span>
|
||||
</button>
|
||||
<button class="menu-item" @click="handleAction('format-link')">
|
||||
<span class="item-icon">🔗</span>
|
||||
لینک
|
||||
<span class="shortcut">Ctrl+K</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- بخش عملیات متنی -->
|
||||
<div class="menu-section">
|
||||
<div class="section-title">عملیات</div>
|
||||
<button class="menu-item" @click="handleAction('copy')">
|
||||
<span class="item-icon">📋</span>
|
||||
کپی
|
||||
<span class="shortcut">Ctrl+C</span>
|
||||
</button>
|
||||
<button class="menu-item" @click="handleAction('cut')">
|
||||
<span class="item-icon">✂️</span>
|
||||
برش
|
||||
<span class="shortcut">Ctrl+X</span>
|
||||
</button>
|
||||
<button class="menu-item" @click="handleAction('paste')">
|
||||
<span class="item-icon">📝</span>
|
||||
چسباندن
|
||||
<span class="shortcut">Ctrl+V</span>
|
||||
</button>
|
||||
<button class="menu-item text-danger" @click="handleAction('delete')">
|
||||
<span class="item-icon">🗑️</span>
|
||||
حذف
|
||||
<span class="shortcut">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- بخش جستجو -->
|
||||
<div class="menu-section">
|
||||
<button class="menu-item" @click="handleAction('search-web')">
|
||||
<span class="item-icon">🔍</span>
|
||||
جستجو در وب
|
||||
</button>
|
||||
<button class="menu-item" @click="handleAction('search-page')">
|
||||
<span class="item-icon">📄</span>
|
||||
جستجو در صفحه
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
visible: Boolean,
|
||||
position: {
|
||||
type: Object,
|
||||
default: () => ({ x: 0, y: 0 }),
|
||||
},
|
||||
selectedText: String,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close", "action"]);
|
||||
|
||||
const menuStyle = computed(() => ({
|
||||
top: `${props.position.y}px`,
|
||||
left: `${props.position.x}px`,
|
||||
display: props.visible ? "block" : "none",
|
||||
}));
|
||||
|
||||
const handleAction = (action) => {
|
||||
emit("action", {
|
||||
action,
|
||||
text: props.selectedText,
|
||||
});
|
||||
emit("close");
|
||||
};
|
||||
|
||||
// بستن منو با کلیک خارج یا Escape
|
||||
const closeMenu = () => {
|
||||
if (props.visible) {
|
||||
emit("close");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscape = (event) => {
|
||||
if (event.key === "Escape") {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (!event.target.closest(".context-menu")) {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
document.addEventListener("contextmenu", handleClickOutside);
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
document.removeEventListener("contextmenu", handleClickOutside);
|
||||
document.removeEventListener("keydown", handleEscape);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||
width: 300px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
z-index: 10000;
|
||||
animation: slideIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-section {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.menu-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ai-section {
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
||||
border-radius: 12px 12px 0 0;
|
||||
margin: -1px -1px 0 -1px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ai-icon {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
font-size: 18px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
font-family: monospace;
|
||||
padding: 2px 6px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 4px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.menu-item.text-danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.menu-item.text-danger:hover {
|
||||
background-color: #fef2f2;
|
||||
}
|
||||
|
||||
/* ریسپانسیو */
|
||||
@media (max-width: 768px) {
|
||||
.context-menu {
|
||||
width: 280px;
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
341
app/components/auto-import/DropdownSelect.vue
Executable file
341
app/components/auto-import/DropdownSelect.vue
Executable file
|
|
@ -0,0 +1,341 @@
|
|||
<template>
|
||||
<div :class="gridColumnClass">
|
||||
<!-- Input Menu -->
|
||||
<UInputMenu
|
||||
v-if="selectSchema.selectType === 'input'"
|
||||
:model-value="resolvedModelValue"
|
||||
:items="normalizedItems"
|
||||
:multiple="selectSchema.multiple"
|
||||
:searchable="selectSchema.searchable"
|
||||
:creatable="selectSchema.creatable"
|
||||
:placeholder="selectSchema.placeholder"
|
||||
:option-attribute="selectSchema.optionAttribute"
|
||||
:value-attribute="selectSchema.valueAttribute"
|
||||
:by="selectSchema.compareBy"
|
||||
v-bind="selectSchema.additionalProps"
|
||||
@update:model-value="onChange"
|
||||
@update:open="emitEvent('open', $event)"
|
||||
@update:search-term="handleSearchTerm"
|
||||
@create="emitEvent('create', $event)"
|
||||
:class="[gridColumnClass, 'cursor-pointer w-full']"
|
||||
size="xl"
|
||||
dir="rtl"
|
||||
color="primary"
|
||||
highlight
|
||||
:ui="{ trailing: 'hidden' }"
|
||||
>
|
||||
<!-- Forward Slots -->
|
||||
<template
|
||||
v-for="(_, slotName) in $slots"
|
||||
:key="slotName"
|
||||
v-slot:[slotName]="slotProps"
|
||||
>
|
||||
<slot :name="slotName" v-bind="slotProps" />
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<slot name="empty">دادهای پیدا نشد</slot>
|
||||
</template>
|
||||
|
||||
<template #loading>
|
||||
<slot name="loading">در حال بارگذاری...</slot>
|
||||
</template>
|
||||
</UInputMenu>
|
||||
|
||||
<!-- Select Menu -->
|
||||
<USelectMenu
|
||||
v-if="selectSchema.selectType === 'select'"
|
||||
:model-value="resolvedModelValue"
|
||||
:items="normalizedItems"
|
||||
:multiple="selectSchema.multiple"
|
||||
:searchable="selectSchema.searchable"
|
||||
:creatable="selectSchema.creatable"
|
||||
:loading="isLoading"
|
||||
:placeholder="selectSchema.placeholder"
|
||||
:option-attribute="selectSchema.optionAttribute"
|
||||
:value-attribute="selectSchema.valueAttribute"
|
||||
:by="selectSchema.compareBy"
|
||||
v-bind="selectSchema.additionalProps"
|
||||
@update:model-value="onChange"
|
||||
@update:open="emitEvent('open', $event)"
|
||||
@update:search-term="handleSearchTerm"
|
||||
@create="emitEvent('create', $event)"
|
||||
:class="[gridColumnClass, 'cursor-pointer']"
|
||||
:ui="{ wrapper: 'w-full', base: 'w-full' }"
|
||||
dir="rtl"
|
||||
color="primary"
|
||||
size="xl"
|
||||
highlight
|
||||
>
|
||||
<!-- Forward Slots -->
|
||||
<template
|
||||
v-for="(_, slotName) in $slots"
|
||||
:key="slotName"
|
||||
v-slot:[slotName]="slotProps"
|
||||
>
|
||||
<slot :name="slotName" v-bind="slotProps" />
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<slot name="empty">دادهای پیدا نشد</slot>
|
||||
</template>
|
||||
|
||||
<template #loading>
|
||||
<slot name="loading">در حال بارگذاری...</slot>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { debounce } from "lodash-es";
|
||||
|
||||
const props = defineProps({
|
||||
dropdownSchema: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["dropdownSelectEvents"]);
|
||||
const { $http: httpService } = useNuxtApp();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const apiItems = ref([]);
|
||||
|
||||
/* ---------------- SCHEMA BASE ---------------- */
|
||||
const selectSchema = computed(() => {
|
||||
const i = props.dropdownSchema || {};
|
||||
|
||||
return {
|
||||
fieldId: i.key || i.name || i.id || null,
|
||||
modelValue: i.modelValue ?? i.value ?? null,
|
||||
selectType: i.selectType ?? "select",
|
||||
items: Array.isArray(i.items)
|
||||
? i.items
|
||||
: Array.isArray(i.options)
|
||||
? i.options
|
||||
: [],
|
||||
optionAttribute: i.optionAttribute ?? "label",
|
||||
valueAttribute: i.valueAttribute ?? "value",
|
||||
multiple:
|
||||
i.multiple === true || i.multi_select === 1 || i.multi_select === true,
|
||||
searchable: i.searchable ?? true,
|
||||
creatable: i.creatable ?? i.allowCreate ?? false,
|
||||
loading: i.loading ?? false,
|
||||
placeholder: i.placeholder || "لطفاً انتخاب کنید...",
|
||||
compareBy: i.compareBy,
|
||||
additionalProps: i.additionalProps || i.extraProps || {},
|
||||
columnSpan: resolveColSpan(i),
|
||||
mode: i.mode || "default",
|
||||
apiConfig: i.apiConfig || {},
|
||||
};
|
||||
});
|
||||
|
||||
/* ---------------- GRID ---------------- */
|
||||
function resolveColSpan(input) {
|
||||
let span = 12;
|
||||
if (input.colSpan) span = input.colSpan;
|
||||
if (input.colClass?.includes("col-")) {
|
||||
const m = input.colClass.match(/col-(\d+)/);
|
||||
if (m) span = Number(m[1]);
|
||||
}
|
||||
return Math.min(12, Math.max(1, span));
|
||||
}
|
||||
|
||||
const gridColumnClass = computed(
|
||||
() => `col-span-${selectSchema.value.columnSpan}`
|
||||
);
|
||||
|
||||
/* ---------------- NORMALIZE ITEMS ---------------- */
|
||||
const normalizedItems = computed(() => {
|
||||
const { optionAttribute, valueAttribute, mode } = selectSchema.value;
|
||||
const sourceItems =
|
||||
mode === "api" ? apiItems.value : selectSchema.value.items;
|
||||
|
||||
return sourceItems.map((item) => {
|
||||
if (typeof item !== "object") {
|
||||
return {
|
||||
[optionAttribute]: String(item),
|
||||
[valueAttribute]: item,
|
||||
raw: item,
|
||||
};
|
||||
}
|
||||
return { ...item, raw: item };
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------------- RESOLVED MODEL VALUE ---------------- */
|
||||
const resolvedModelValue = computed(() => {
|
||||
const { modelValue, multiple } = selectSchema.value;
|
||||
|
||||
if (multiple && Array.isArray(modelValue)) {
|
||||
return modelValue.map(resolveSingleValue).filter(Boolean);
|
||||
}
|
||||
return resolveSingleValue(modelValue);
|
||||
});
|
||||
|
||||
function resolveSingleValue(val) {
|
||||
if (val === null || val === undefined) return null;
|
||||
if (typeof val === "object") return val;
|
||||
|
||||
let item = normalizedItems.value.find(
|
||||
(i) => i?.[selectSchema.value.valueAttribute] === val
|
||||
);
|
||||
|
||||
// اگر پیدا نشد و mode API است → آیتم موقت اضافه کن
|
||||
if (!item && selectSchema.value.mode === "api") {
|
||||
item = {
|
||||
[selectSchema.value.optionAttribute]: val,
|
||||
[selectSchema.value.valueAttribute]: val,
|
||||
raw: val,
|
||||
__temp: true,
|
||||
};
|
||||
apiItems.value.push(item);
|
||||
}
|
||||
|
||||
return item || null;
|
||||
}
|
||||
|
||||
/* ---------------- EMIT HANDLERS ---------------- */
|
||||
function emitEvent(action, payload) {
|
||||
emit("dropdownSelectEvents", {
|
||||
action,
|
||||
payload,
|
||||
fieldId: selectSchema.value.fieldId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
function onChange(val) {
|
||||
// اضافه کردن به apiItems اگر وجود ندارد
|
||||
if (selectSchema.value.mode === "api") {
|
||||
const values = Array.isArray(val) ? val : [val];
|
||||
values.forEach((v) => {
|
||||
const valueKey =
|
||||
typeof v === "object" ? v[selectSchema.value.valueAttribute] : v;
|
||||
if (
|
||||
!apiItems.value.find(
|
||||
(i) => i[selectSchema.value.valueAttribute] === valueKey
|
||||
)
|
||||
) {
|
||||
apiItems.value.push(
|
||||
typeof v === "object"
|
||||
? v
|
||||
: {
|
||||
[selectSchema.value.optionAttribute]: valueKey,
|
||||
[selectSchema.value.valueAttribute]: valueKey,
|
||||
raw: v,
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
props.dropdownSchema.modelValue = val;
|
||||
emitEvent("change", val);
|
||||
}
|
||||
watch(
|
||||
() => normalizedItems.value.length,
|
||||
(len) => {
|
||||
if (!len) return;
|
||||
|
||||
const { multiple, modelValue, selectType } = selectSchema.value;
|
||||
if (selectType !== "select") return;
|
||||
|
||||
if (
|
||||
(multiple && Array.isArray(modelValue) && modelValue.length) ||
|
||||
(!multiple && modelValue != null)
|
||||
)
|
||||
return;
|
||||
|
||||
onChange(multiple ? [normalizedItems.value[0]] : normalizedItems.value[0]);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
/* ---------------- SEARCH WITH DEBOUNCE ---------------- */
|
||||
const debouncedFetch = debounce(
|
||||
async (searchText, url, crud = "GET", payload = "") => {
|
||||
if (!searchText) {
|
||||
apiItems.value = [];
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const { searchField = "name" } = selectSchema.value.apiConfig;
|
||||
url = url.replace("{{filter}}", searchText);
|
||||
payload = payload?.replace("{{filter}}", searchText);
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
if (crud === "GET") {
|
||||
httpService
|
||||
.getRequest(url)
|
||||
.then((res) => {
|
||||
const mapped = res
|
||||
.filter((item) =>
|
||||
String(item?.[searchField] || "")
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase())
|
||||
)
|
||||
.map((item) => ({
|
||||
[selectSchema.value.optionAttribute]: item[searchField],
|
||||
[selectSchema.value.valueAttribute]: item.id ?? item[searchField],
|
||||
raw: item,
|
||||
}));
|
||||
|
||||
// جایگزینی آیتم موقت با آیتم واقعی
|
||||
apiItems.value = apiItems.value.map((existing) =>
|
||||
existing.__temp
|
||||
? mapped.find(
|
||||
(m) =>
|
||||
m[selectSchema.value.valueAttribute] ===
|
||||
existing[selectSchema.value.valueAttribute]
|
||||
) || existing
|
||||
: existing
|
||||
);
|
||||
|
||||
// اضافه کردن آیتمهای جدید
|
||||
mapped.forEach((m) => {
|
||||
if (
|
||||
!apiItems.value.find(
|
||||
(i) =>
|
||||
i[selectSchema.value.valueAttribute] ===
|
||||
m[selectSchema.value.valueAttribute]
|
||||
)
|
||||
) {
|
||||
apiItems.value.push(m);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) =>
|
||||
console.error("Remote search API error:", crud, error)
|
||||
)
|
||||
.finally(() => (isLoading.value = false));
|
||||
} else if (crud === "POST") {
|
||||
let payload_obj = payload ? JSON.parse(payload) : {};
|
||||
httpService
|
||||
.postRequest(url, payload_obj)
|
||||
.then((res) => {
|
||||
apiItems.value = res.data;
|
||||
})
|
||||
.catch((error) =>
|
||||
console.error("Remote search API error:", crud, error)
|
||||
)
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
},
|
||||
300
|
||||
);
|
||||
|
||||
function handleSearchTerm(searchText) {
|
||||
emitEvent("search", searchText);
|
||||
|
||||
const { mode, apiConfig } = selectSchema.value;
|
||||
if (mode === "api" && apiConfig?.url) {
|
||||
debouncedFetch(
|
||||
searchText,
|
||||
apiConfig.url,
|
||||
apiConfig.crud,
|
||||
apiConfig.payload
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
243
app/components/auto-import/Header.vue
Executable file
243
app/components/auto-import/Header.vue
Executable file
|
|
@ -0,0 +1,243 @@
|
|||
<template>
|
||||
<header
|
||||
class="bg-gray-100 h-16 dark:bg-dark-primary border-gray-200 dark:border-dark-primary-800 px-6 grid grid-cols-12 items-center sticky top-0 z-40 gap-4"
|
||||
>
|
||||
<!-- سمت چپ هدر -->
|
||||
<div class="col-span-3 lg:col-span-2 xl:col-span-3 hidden lg:block">
|
||||
<template v-if="headerSchema.breadcrumb">
|
||||
<Breadcrumb
|
||||
:breadcrumbData="[]"
|
||||
:tabs="tabs"
|
||||
:activeTabId="activeTabModel"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="headerSchema.logo">
|
||||
<nuxt-link :to="{ name: 'DashboardBasePage' }">
|
||||
<div class="flex items-center gap-3">
|
||||
<img :src="useSystemTheme.logo.value" alt="" class="h-9 w-9" />
|
||||
<div v-if="useSystemTheme.currentTheme.value" 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>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
</div>
|
||||
<div class="col-span-3 lg:col-span-2 xl:col-span-3 lg:hidden">
|
||||
<button
|
||||
@click="toggleSidebarMenu"
|
||||
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"
|
||||
aria-label="باز کردن منو"
|
||||
>
|
||||
<UIcon name="i-heroicons-bars-3" class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- وسط هدر -->
|
||||
<div
|
||||
class="col-span-6 lg:col-span-8 xl:col-span-6 flex justify-center pt-4"
|
||||
>
|
||||
<TabBar
|
||||
:tabs="tabs"
|
||||
v-model:active-tab="activeTabModel"
|
||||
mode="soft"
|
||||
:width="tabBarWidth"
|
||||
@tab-change="emit('tab-change', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- راست هدر -->
|
||||
<div
|
||||
class="col-span-3 lg:col-span-2 xl:col-span-3 flex items-center justify-end gap-3"
|
||||
>
|
||||
<!-- Dark mode -->
|
||||
<UButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="gray"
|
||||
:icon="
|
||||
useSystemTheme.isDark.value ? 'i-heroicons-sun' : 'i-heroicons-moon'
|
||||
"
|
||||
@click="useSystemTheme.toggleDarkMode"
|
||||
/>
|
||||
|
||||
<!-- Language -->
|
||||
<UDropdownMenu :items="headerItems.languages" dir="rtl">
|
||||
<UButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon="i-heroicons-language"
|
||||
label="فارسی"
|
||||
/>
|
||||
</UDropdownMenu>
|
||||
|
||||
<!-- Notifications -->
|
||||
<UButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon="i-heroicons-bell"
|
||||
:badge="unreadNotifications"
|
||||
/>
|
||||
|
||||
<!-- User menu -->
|
||||
<UDropdownMenu
|
||||
v-if="isClient && userAvatar"
|
||||
:items="userMenuItems"
|
||||
dir="rtl"
|
||||
>
|
||||
<!-- <UAvatar
|
||||
v-if="userInitial"
|
||||
:label="userInitial"
|
||||
size="sm"
|
||||
class="cursor-pointer"
|
||||
/> -->
|
||||
<!-- <span v-if="userInitial" class="text-white text-lg font-bold">{{
|
||||
userInitial
|
||||
}}</span>
|
||||
<UAvatar v-else :src="userAvatar" size="sm" class="cursor-pointer" /> -->
|
||||
|
||||
<!-- اگر avatar داشت -->
|
||||
<UAvatar :src="userAvatar" size="sm" class="cursor-pointer" />
|
||||
|
||||
<!-- اگر avatar نداشت -->
|
||||
<!-- <UAvatar
|
||||
v-else
|
||||
:label="userInitial"
|
||||
size="sm"
|
||||
class="cursor-pointer bg-primary text-white font-bold"
|
||||
/> -->
|
||||
</UDropdownMenu>
|
||||
|
||||
<UDropdownMenu v-else-if="isClient" :items="userMenuItems" dir="rtl">
|
||||
<span class="text-white text-lg font-bold">{{ userInitial }}</span>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import headerItems from "@/json/header/header.json";
|
||||
import { composSystemTheme } from "@/composables/composSystemTheme";
|
||||
import { useCommonStore } from "@/stores/commonStore";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const commonStore = useCommonStore();
|
||||
const authStore = useAuthStore();
|
||||
const router = useRouter();
|
||||
/* ---------------- PROPS ---------------- */
|
||||
const props = defineProps({
|
||||
tabs: { type: Array, required: true, default: [] },
|
||||
activeTab: { type: String, default: "" },
|
||||
unreadNotifications: { type: Number, default: 0 },
|
||||
tabBarWidth: { type: String, default: "40em" },
|
||||
headerSchema: { type: Object, default: {} },
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
"update:activeTab",
|
||||
"tab-change",
|
||||
"user-menu-select",
|
||||
]);
|
||||
|
||||
/* ---------------- ACTIVE TAB ---------------- */
|
||||
const activeTabModel = computed({
|
||||
get: () => props.activeTab,
|
||||
set: (val) => emit("update:activeTab", val),
|
||||
});
|
||||
|
||||
/* ---------------- THEME ---------------- */
|
||||
const useSystemTheme = composSystemTheme();
|
||||
|
||||
/* ---------------- USER MENU HANDLER ---------------- */
|
||||
const handleUserMenu = async (action) => {
|
||||
if (action.key === "logout") {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
const cookies = document.cookie.split(";");
|
||||
for (let cookie of cookies) {
|
||||
const eqPos = cookie.indexOf("=");
|
||||
const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim();
|
||||
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/";
|
||||
}
|
||||
authStore.userReset();
|
||||
navigateTo("/login");
|
||||
|
||||
await router.push("/login");
|
||||
} else if (action.key === "profile") {
|
||||
await router.push("/profile");
|
||||
} else if (action.key === "settings") {
|
||||
await router.push("/settings");
|
||||
} else if (action.key === "developer") {
|
||||
await router.push("/developer");
|
||||
} else if (action.key === "admin") {
|
||||
await router.push("/admin");
|
||||
} else {
|
||||
emit("user-menu-select", action);
|
||||
}
|
||||
};
|
||||
|
||||
/* ---------------- USER MENU ITEMS ---------------- */
|
||||
const userMenuItems = computed(() =>
|
||||
headerItems.userMenu.map((group) =>
|
||||
group.map((item) => ({
|
||||
...item,
|
||||
onSelect: () => handleUserMenu(item),
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
/* ---------------- AVATAR ---------------- */
|
||||
|
||||
// const userAvatar = "https://api.dicebear.com/7.x/avataaars/svg?seed=admin";
|
||||
const isClient = ref(false);
|
||||
onMounted(() => {
|
||||
isClient.value = true;
|
||||
});
|
||||
|
||||
const userAvatar = computed(() => {
|
||||
if (!process.client) return null;
|
||||
|
||||
try {
|
||||
const user = JSON.parse(localStorage.getItem("user") || "{}");
|
||||
const avatar = user?.user_data?.avatar;
|
||||
|
||||
if (!avatar) return null;
|
||||
const BASE_URL = "https://hamfahmi.ir/";
|
||||
const url = BASE_URL + `api/media${avatar}`;
|
||||
|
||||
// console.log("avatar ==> ", url);
|
||||
|
||||
return url;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const userInitial = computed(() => {
|
||||
if (!process.client) return "؟";
|
||||
|
||||
try {
|
||||
const user = JSON.parse(localStorage.getItem("user") || {});
|
||||
const firstName = user?.user_data?.first_name?.trim() || "";
|
||||
const lastName = user?.user_data?.last_name?.trim() || "";
|
||||
|
||||
const first = firstName[0] || "";
|
||||
const second = lastName[0] || firstName[1] || "";
|
||||
|
||||
return first + second || "؟";
|
||||
} catch (e) {
|
||||
return "؟";
|
||||
}
|
||||
});
|
||||
function toggleSidebarMenu() {
|
||||
commonStore.isSidebarOpen();
|
||||
}
|
||||
</script>
|
||||
31
app/components/auto-import/MyLoading.vue
Executable file
31
app/components/auto-import/MyLoading.vue
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<div class="flex flex-col items-center justify-center h-full py-8">
|
||||
<div :class="['relative', sizeClass]" role="status" aria-label="در حال بارگذاری">
|
||||
<!-- ساده و مینیمال اسپینر دایرهای با tail پرایمری (آبی) -->
|
||||
<div class="w-16 h-16 rounded-full border-8 border-gray-200 dark:border-gray-700 border-t-primary-500 dark:border-t-primary-500 animate-spin"></div>
|
||||
</div>
|
||||
|
||||
<p class="mt-6 text-lg font-medium text-gray-700 dark:text-gray-300">{{ loadingText }}</p>
|
||||
<p class="mt-2 text-sm text-primary-600 dark:text-primary-400 animate-pulse">لطفاً صبر کنید...</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
loadingText: { type: String, default: 'در حال بارگذاری...' },
|
||||
loadingSize: { type: String, default: 'md' }, // sm, md, lg
|
||||
})
|
||||
|
||||
const sizeClass = computed(() => {
|
||||
switch (props.loadingSize) {
|
||||
case 'sm':
|
||||
return 'scale-75'
|
||||
case 'lg':
|
||||
return 'scale-150'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
171
app/components/auto-import/Sidebar.vue
Executable file
171
app/components/auto-import/Sidebar.vue
Executable file
|
|
@ -0,0 +1,171 @@
|
|||
<!-- app/components/Sidebar.vue -->
|
||||
<template>
|
||||
<UDashboardSidebar
|
||||
side="left"
|
||||
:collapsed="collapsed"
|
||||
collapsible
|
||||
:min-size="5"
|
||||
:default-size="10"
|
||||
:max-size="15"
|
||||
:ui="sidebarUI"
|
||||
class="sidebar-gradient"
|
||||
dir="rtl"
|
||||
@update:collapsed="onCollapse"
|
||||
>
|
||||
<template #header>
|
||||
<nuxt-link :to="{ name: 'DashboardBasePage' }">
|
||||
<div class="flex items-center gap-3">
|
||||
<img :src="useSystemTheme.logo.value" 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>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<div class="">
|
||||
<UNavigationMenu
|
||||
dir="rtl"
|
||||
:collapsed="collapsed"
|
||||
:items="getSideBarSchema()?.topMenu || []"
|
||||
orientation="vertical"
|
||||
:ui="navigationUI"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto pb-2 flex flex-col gap-2">
|
||||
<UNavigationMenu
|
||||
dir="rtl"
|
||||
:collapsed="collapsed"
|
||||
:items="getSideBarSchema()?.bottomMenu || []"
|
||||
orientation="vertical"
|
||||
:ui="navigationUI"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer="{ collapsed }">
|
||||
<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">
|
||||
<!-- دکمه باز کردن -->
|
||||
<UButton
|
||||
v-if="collapsed"
|
||||
icon="stash-chevron-double-left-solid"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="transition-all duration-300"
|
||||
@click="toggleSidebar"
|
||||
/>
|
||||
<!-- دکمه بستن -->
|
||||
<UButton
|
||||
v-else
|
||||
icon="stash-chevron-double-right-solid"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="transition-all duration-300 absolute left-0 -translate-y-1/2"
|
||||
@click="toggleSidebar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UDashboardSidebar>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref} from "vue";
|
||||
import { composSystemTheme } from "@/composables/composSystemTheme";
|
||||
|
||||
const useSystemTheme = composSystemTheme();
|
||||
|
||||
const props = defineProps({
|
||||
sidebarItems: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({
|
||||
topMenu: [],
|
||||
bottomMenu: [],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:collapsed"]);
|
||||
|
||||
const collapsed = ref(false);
|
||||
|
||||
const onCollapse = (value) => {
|
||||
collapsed.value = value;
|
||||
emit("update:collapsed", value);
|
||||
};
|
||||
|
||||
function getSideBarSchema(){
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
const IS_DEVLOP_MODE = config.public.IS_DEVLOP_MODE || 1;
|
||||
|
||||
|
||||
let result = {}
|
||||
|
||||
result.topMenu = props.sidebarItems?.topMenu.filter((el) => (!el.develop || el.develop == IS_DEVLOP_MODE) )
|
||||
result.bottomMenu = props.sidebarItems?.bottomMenu.filter((el) => (!el.develop || el.develop == IS_DEVLOP_MODE) )
|
||||
|
||||
// console.log("SideBar IS_DEVLOP_MODE ", IS_DEVLOP_MODE, result);
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// تابع برای باز/بسته کردن نرم سایدبار
|
||||
const toggleSidebar = () => {
|
||||
collapsed.value = !collapsed?.value;
|
||||
emit("update:collapsed", collapsed?.value);
|
||||
};
|
||||
|
||||
const sidebarUI = {
|
||||
width: "w-72",
|
||||
collapsed: { width: "w-16" },
|
||||
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",
|
||||
base: "backdrop-blur-sm transition-all duration-300 ease-out",
|
||||
header: "border-b border-gray-200/30 dark:border-dark-primary-800/30",
|
||||
footer: "border-t border-gray-200/30 dark:border-dark-primary-800/30",
|
||||
};
|
||||
|
||||
const navigationUI = {
|
||||
base: "space-y-1",
|
||||
wrapper: (c) => (c ? "items-center" : ""),
|
||||
inactive:
|
||||
"text-dark-primary-700 dark:text-gray-300 hover:bg-gradient-to-r hover:from-blue-50/50 hover:to-transparent dark:hover:from-blue-900/20 hover:text-blue-600 dark:hover:text-blue-400",
|
||||
active:
|
||||
"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" },
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar-gradient {
|
||||
background: linear-gradient(to bottom, #ffffff, #f9fafb);
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
.dark .sidebar-gradient {
|
||||
background: linear-gradient(to bottom, #111827, #1f2937);
|
||||
}
|
||||
</style>
|
||||
36
app/components/auto-import/SlashMenu.vue
Executable file
36
app/components/auto-import/SlashMenu.vue
Executable file
|
|
@ -0,0 +1,36 @@
|
|||
<script setup>
|
||||
defineProps({
|
||||
items: Array,
|
||||
command: Function,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="slash-menu">
|
||||
<button v-for="item in items" :key="item.title" @click="command(item)">
|
||||
{{ item.title }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.slash-menu {
|
||||
background: white;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.slash-menu button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
text-align: right;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.slash-menu button:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
278
app/components/auto-import/TabBar.vue
Executable file
278
app/components/auto-import/TabBar.vue
Executable file
|
|
@ -0,0 +1,278 @@
|
|||
<template>
|
||||
<div class="relative flex items-center" :style="{ width: customWidth }">
|
||||
<!-- نسخه دسکتاپ / تبلت: همان قبلی -->
|
||||
<div v-if="!isMobile" class="w-full">
|
||||
<UButton
|
||||
v-if="hasOverflow"
|
||||
icon="i-heroicons-chevron-left"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
class="absolute -left-10 top-1/2 -translate-y-1/2 z-10 flex-shrink-0 bg-light-primary dark:bg-dark-primary-800 shadow-md border border-gray-200 dark:border-dark-primary-700"
|
||||
@click="scrollLeft"
|
||||
/>
|
||||
|
||||
<div
|
||||
ref="tabsContainer"
|
||||
class="flex overflow-x-auto scrollbar-hide w-full justify-start"
|
||||
@wheel.prevent="handleWheelScroll"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
v-for="tab in getListTabs()"
|
||||
:key="tab.id"
|
||||
@click="handleTabClick(tab)"
|
||||
:class="getTabClass(activeTab === tab.id)"
|
||||
:data-active="activeTab === tab.id"
|
||||
>
|
||||
<UIcon
|
||||
v-if="tab.icon"
|
||||
:name="tab.icon"
|
||||
class="w-5 h-5 flex-shrink-0"
|
||||
/>
|
||||
<span class="text-sm font-medium">{{ tab.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
v-if="hasOverflow"
|
||||
icon="i-heroicons-chevron-right"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
class="absolute -right-10 top-1/2 -translate-y-1/2 z-10 flex-shrink-0 bg-light-primary dark:bg-dark-primary-800 shadow-md border border-gray-200 dark:border-dark-primary-700"
|
||||
@click="scrollRight"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- نسخه موبایل -->
|
||||
<div
|
||||
v-else
|
||||
class="fixed inset-x-0 bottom-0 z-50 bg-light-primary dark:bg-dark-primary-800 border-t border-gray-200 dark:border-dark-primary-700 shadow-lg pb-safe"
|
||||
>
|
||||
<!-- اگر ۵ یا کمتر → justify-around و بدون اسکرول -->
|
||||
<!-- اگر بیشتر از ۵ → اسکرول افقی با flex-nowrap -->
|
||||
|
||||
<div
|
||||
class="flex items-center"
|
||||
:class="getListTabs().length <= 5 ? '' : 'flex-nowrap'"
|
||||
>
|
||||
<button
|
||||
v-for="tab in getListTabs()"
|
||||
:key="tab.id"
|
||||
@click="handleTabClick(tab)"
|
||||
class="flex flex-col items-center justify-center py-3 transition-colors relative mx-4 "
|
||||
:class="[
|
||||
getListTabs().length <= 5
|
||||
? 'flex-1 min-w-0 min-h-[56px]'
|
||||
: 'px-4 min-w-[80px] min-h-[56px] flex-shrink-0',
|
||||
]"
|
||||
>
|
||||
<!-- آیکن -->
|
||||
<UIcon
|
||||
v-if="tab.icon"
|
||||
:name="tab.icon"
|
||||
class="w-6 h-6 mb-1"
|
||||
:class="
|
||||
activeTab === tab.id
|
||||
? 'text-primary-600 dark:text-primary-400'
|
||||
: 'text-gray-600 dark:text-gray-400'
|
||||
"
|
||||
/>
|
||||
<!-- اگر آیکن نبود، فقط متن با فاصله مناسب -->
|
||||
<div v-else class="h-7"></div>
|
||||
|
||||
<!-- متن -->
|
||||
<span
|
||||
class="text-xs font-medium whitespace-nowrap"
|
||||
:class="
|
||||
activeTab === tab.id
|
||||
? 'text-primary-600 dark:text-primary-400'
|
||||
: 'text-gray-600 dark:text-gray-400'
|
||||
"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</span>
|
||||
|
||||
<!-- اندیکاتور فعال (خط زیر تب) -->
|
||||
<div
|
||||
v-if="activeTab === tab.id"
|
||||
class="absolute bottom-0 w-12 h-1 bg-primary-600 dark:bg-primary-400 rounded-t-full"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
tabs: { type: Array, default: () => [] },
|
||||
activeTab: { type: String, default: "" },
|
||||
width: { type: String, default: "40em" },
|
||||
mode: {
|
||||
type: String,
|
||||
default: "soft",
|
||||
validator: (v) => ["soft", "chrome"].includes(v),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["tab-change", "update:activeTab"]);
|
||||
|
||||
const tabsContainer = ref(null);
|
||||
const hasOverflow = ref(false);
|
||||
const isMobile = ref(false);
|
||||
|
||||
const customWidth =
|
||||
props.width.includes("px") ||
|
||||
props.width.includes("em") ||
|
||||
props.width.includes("%")
|
||||
? props.width
|
||||
: `${props.width}px`;
|
||||
|
||||
// بروزرسانی وضعیت موبایل (امن برای SSR)
|
||||
const updateMobileStatus = () => {
|
||||
if (typeof window === "undefined") return;
|
||||
isMobile.value = window.innerWidth < 768;
|
||||
};
|
||||
|
||||
|
||||
function getListTabs(){
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
const IS_DEVLOP_MODE = config.public.IS_DEVLOP_MODE || 1;
|
||||
|
||||
|
||||
let result = {}
|
||||
|
||||
result = props.tabs.filter((el) => (!el.develop || el.develop == IS_DEVLOP_MODE) )
|
||||
|
||||
// console.log("TabBar IS_DEVLOP_MODE ", IS_DEVLOP_MODE, result);
|
||||
|
||||
return result
|
||||
}
|
||||
const getTabClass = (isActive) => {
|
||||
const base =
|
||||
"flex items-center justify-center gap-2 px-6 py-3 cursor-pointer whitespace-nowrap transition-all duration-200 flex-shrink-0 min-w-[140px] w-[140px]";
|
||||
if (!isActive) {
|
||||
return `${base} border-transparent text-dark-primary-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-light-primary hover:bg-gray-50 dark:hover:bg-dark-primary-800`;
|
||||
}
|
||||
if (props.mode === "chrome") {
|
||||
return `${base} py-4 text-gray-900 dark:text-light-primary bg-light-primary dark:bg-dark-primary-800 rounded-t-xl`;
|
||||
}
|
||||
return `${base} border-primary-500 text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-900/20 border-b-2`;
|
||||
};
|
||||
|
||||
const handleTabClick = (tab) => {
|
||||
emit("update:activeTab", tab.id);
|
||||
emit("tab-change", tab);
|
||||
if (!isMobile.value) {
|
||||
nextTick(scrollToActiveTab);
|
||||
}
|
||||
};
|
||||
|
||||
// توابع دسکتاپ (همان قبلی)
|
||||
const checkOverflow = () => {
|
||||
if (!tabsContainer.value || isMobile.value) {
|
||||
hasOverflow.value = false;
|
||||
return;
|
||||
}
|
||||
const container = tabsContainer.value;
|
||||
hasOverflow.value = container.scrollWidth > container.clientWidth + 1;
|
||||
};
|
||||
|
||||
const scrollLeft = () => {
|
||||
if (!tabsContainer.value) return;
|
||||
tabsContainer.value.scrollLeft -= 200;
|
||||
requestAnimationFrame(checkOverflow);
|
||||
};
|
||||
|
||||
const scrollRight = () => {
|
||||
if (!tabsContainer.value) return;
|
||||
const container = tabsContainer.value;
|
||||
const maxScroll = container.scrollWidth - container.clientWidth;
|
||||
container.scrollLeft = Math.min(maxScroll, container.scrollLeft + 200);
|
||||
requestAnimationFrame(checkOverflow);
|
||||
};
|
||||
|
||||
const scrollToActiveTab = () => {
|
||||
if (!tabsContainer.value || isMobile.value) return;
|
||||
const container = tabsContainer.value;
|
||||
const activeEl = container.querySelector('[data-active="true"]');
|
||||
if (!activeEl) return;
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const elRect = activeEl.getBoundingClientRect();
|
||||
|
||||
if (elRect.left < containerRect.left) {
|
||||
container.scrollLeft += elRect.left - containerRect.left - 16;
|
||||
} else if (elRect.right > containerRect.right) {
|
||||
container.scrollLeft += elRect.right - containerRect.right + 16;
|
||||
}
|
||||
};
|
||||
|
||||
const handleWheelScroll = (event) => {
|
||||
if (!tabsContainer.value || isMobile.value) return;
|
||||
tabsContainer.value.scrollLeft += event.deltaY;
|
||||
requestAnimationFrame(checkOverflow);
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
updateMobileStatus();
|
||||
nextTick(() => {
|
||||
checkOverflow();
|
||||
if (!isMobile.value) scrollToActiveTab();
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.activeTab,
|
||||
() => {
|
||||
if (!isMobile.value) nextTick(scrollToActiveTab);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.tabs,
|
||||
() => {
|
||||
nextTick(checkOverflow);
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
updateMobileStatus();
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
if (tabsContainer.value) {
|
||||
tabsContainer.value.addEventListener("scroll", checkOverflow);
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
checkOverflow();
|
||||
if (!isMobile.value) scrollToActiveTab();
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
if (tabsContainer.value) {
|
||||
tabsContainer.value.removeEventListener("scroll", checkOverflow);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pb-safe {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
</style>
|
||||
2038
app/components/auto-import/TiptapEditor.vue
Executable file
2038
app/components/auto-import/TiptapEditor.vue
Executable file
File diff suppressed because it is too large
Load Diff
1600
app/components/auto-import/TiptapEditor2.vue
Executable file
1600
app/components/auto-import/TiptapEditor2.vue
Executable file
File diff suppressed because it is too large
Load Diff
270
app/components/auto-import/myPagination.vue
Executable file
270
app/components/auto-import/myPagination.vue
Executable file
|
|
@ -0,0 +1,270 @@
|
|||
<!-- components/myPagination.vue -->
|
||||
<template>
|
||||
<div
|
||||
class="jahat-pagination flex items-center flex-wrap border-t border-gray-200 p-2 rounded-none mt-2"
|
||||
:class="pagination.alignment"
|
||||
>
|
||||
<!-- نمایش محدوده رکوردها و انتخاب تعداد سطر -->
|
||||
<div v-if="showTotalRecords" class="flex items-center mb-0 mr-3 text-sm">
|
||||
<div class="hidden md:block">
|
||||
<span>{{ recordRange.start }} </span>
|
||||
<span>-</span>
|
||||
<span> {{ recordRange.end }}</span>
|
||||
<span> از </span>
|
||||
</div>
|
||||
<span>{{ totalRecords }}</span>
|
||||
<span> رکورد</span>
|
||||
|
||||
<label for="pagination-limit" class="ml-2 mr-4 mb-0 whitespace-nowrap">
|
||||
<span class="hidden md:block"> تعداد سطرها </span>
|
||||
<span class="md:hidden"> سطرها </span>
|
||||
</label>
|
||||
|
||||
<select
|
||||
:id="limitSelectId"
|
||||
v-model.number="internalLimit"
|
||||
@change="onLimitChange"
|
||||
class="min-w-[3.5em] py-1 px-2 border dark:bg-dark-primary-800 border-gray-300 rounded-md bg-white text-sm"
|
||||
>
|
||||
<option v-for="opt in limitOptions" :key="opt" :value="opt">
|
||||
{{ opt }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- ورودی عددی + UPagination -->
|
||||
<div v-if="showPageSelection" class="flex items-center gap-2">
|
||||
<!-- ورودی عددی (همیشه نمایش داده شود) -->
|
||||
<div v-if="isMobile" class="flex gap-1 justify-center w-full">
|
||||
<UButton
|
||||
:disabled="internalPage <= 1"
|
||||
size="sm"
|
||||
color="gray"
|
||||
@click="onPrevClick"
|
||||
aria-label="صفحه قبلی"
|
||||
>
|
||||
<UIcon name="i-lucide-chevron-right" size="xl" class=""></UIcon>
|
||||
</UButton>
|
||||
<input
|
||||
type="number"
|
||||
dir="ltr"
|
||||
v-model.number="internalPage"
|
||||
class="py-1 px-2 border border-gray-300 rounded-md font-bold text-sm w-16 text-center no-spinner"
|
||||
:id="pageInputId"
|
||||
placeholder="00"
|
||||
:min="1"
|
||||
:max="totalPages"
|
||||
@keyup="debouncedPageChange"
|
||||
@keydown="clearDebounce"
|
||||
aria-label="وارد کردن شماره صفحه"
|
||||
/>
|
||||
|
||||
<!-- نمایش فقط دکمههای قبلی/بعدی در موبایل -->
|
||||
|
||||
<UButton
|
||||
:disabled="internalPage >= totalPages"
|
||||
size="sm"
|
||||
color="gray"
|
||||
@click="onNextClick"
|
||||
aria-label="صفحه بعدی"
|
||||
>
|
||||
<UIcon name="i-lucide-chevron-left" size="xl" class=""></UIcon>
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- نمایش UPagination کامل در دسکتاپ -->
|
||||
<UPagination
|
||||
v-else
|
||||
:page="internalPage"
|
||||
:total="totalRecords"
|
||||
:page-count="totalPages"
|
||||
size="xl"
|
||||
:max="3"
|
||||
:sibling-count="1"
|
||||
:show-edges="true"
|
||||
@update:page="onPageClick"
|
||||
:prev-button="{ label: 'قبلی', color: 'gray' }"
|
||||
:next-button="{ label: 'بعدی', trailing: true, color: 'gray' }"
|
||||
:active-button="{ variant: 'outline' }"
|
||||
:inactive-button="{ color: 'gray' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useMediaQuery } from "@vueuse/core";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import { useRoute, useRouter } from "#imports";
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* اطلاعات پیجینیشن ورودی
|
||||
* مثال: { pages: 1000, total: 10000, page: 1, offset: 0, limit: 10 }
|
||||
*/
|
||||
paginationInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return (
|
||||
typeof value.total === "number" &&
|
||||
typeof value.page === "number" &&
|
||||
typeof value.limit === "number"
|
||||
);
|
||||
},
|
||||
},
|
||||
showTotalRecords: { type: Boolean, default: true },
|
||||
showPageSelection: { type: Boolean, default: true },
|
||||
limitOptions: {
|
||||
type: Array,
|
||||
default: () => [5, 10, 15, 20, 25, 50, 75, 100],
|
||||
},
|
||||
limitSelectId: { type: String, default: "pagination-limit" },
|
||||
pageInputId: { type: String, default: "pagination-page" },
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
"update:paginationInfo",
|
||||
"pageChanged",
|
||||
"limitChanged",
|
||||
]);
|
||||
|
||||
// تشخیص موبایل (مثلاً عرض کمتر از 768px)
|
||||
const isMobile = useMediaQuery("(max-width: 767px)");
|
||||
const onPrevClick = () => {
|
||||
const newPage = Math.max(1, internalPage.value - 1);
|
||||
if (newPage !== internalPage.value) {
|
||||
internalPage.value = newPage;
|
||||
onPageClick(newPage);
|
||||
}
|
||||
};
|
||||
|
||||
const onNextClick = () => {
|
||||
const newPage = Math.min(totalPages.value, internalPage.value + 1);
|
||||
if (newPage !== internalPage.value) {
|
||||
internalPage.value = newPage;
|
||||
onPageClick(newPage);
|
||||
}
|
||||
};
|
||||
// استخراج route و router برای بهروزرسانی URL
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// --- State داخلی ---
|
||||
const internalPage = ref(props.paginationInfo.page);
|
||||
const internalLimit = ref(props.paginationInfo.limit);
|
||||
|
||||
// --- محاسبات کمکی ---
|
||||
const totalPages = computed(
|
||||
() =>
|
||||
props.paginationInfo.pages ||
|
||||
Math.ceil(props.paginationInfo.total / internalLimit.value) ||
|
||||
1
|
||||
);
|
||||
|
||||
const totalRecords = computed(() => props.paginationInfo.total);
|
||||
|
||||
const recordRange = computed(() => {
|
||||
const start = (internalPage.value - 1) * internalLimit.value + 1;
|
||||
const end = Math.min(
|
||||
internalPage.value * internalLimit.value,
|
||||
totalRecords.value
|
||||
);
|
||||
return { start, end };
|
||||
});
|
||||
|
||||
const pagination = {
|
||||
alignment: "justify-between",
|
||||
};
|
||||
|
||||
// --- Debounce برای ورودی عددی ---
|
||||
const debouncedEmit = useDebounceFn(() => {
|
||||
const page = Math.max(1, Math.min(internalPage.value, totalPages.value));
|
||||
internalPage.value = page; // normalize
|
||||
emitPageChange(page, internalLimit.value);
|
||||
}, 500);
|
||||
|
||||
const debouncedPageChange = () => {
|
||||
debouncedEmit();
|
||||
};
|
||||
|
||||
const clearDebounce = () => {
|
||||
debouncedEmit.clear();
|
||||
};
|
||||
|
||||
// --- هندلرهای اصلی ---
|
||||
const onPageClick = (page) => {
|
||||
internalPage.value = page;
|
||||
emitPageChange(page, internalLimit.value);
|
||||
// بهروزرسانی URL
|
||||
router.replace({
|
||||
query: { ...route.query, page: page.toString() },
|
||||
});
|
||||
};
|
||||
|
||||
const onLimitChange = () => {
|
||||
internalPage.value = 1; // reset to first page
|
||||
emitLimitChange(1, internalLimit.value);
|
||||
|
||||
// بهروزرسانی URL
|
||||
router.replace({
|
||||
query: { ...route.query, page: "1", limit: internalLimit.value.toString() },
|
||||
});
|
||||
};
|
||||
|
||||
// --- ارسال event و sync با والد ---
|
||||
const emitPageChange = (page, limit) => {
|
||||
const newPagination = {
|
||||
page,
|
||||
limit,
|
||||
total: props.paginationInfo.total,
|
||||
pages: totalPages.value,
|
||||
offset: (page - 1) * limit,
|
||||
};
|
||||
emit("update:paginationInfo", newPagination);
|
||||
emit("pageChanged", { pageNumber: page, limit });
|
||||
};
|
||||
|
||||
const emitLimitChange = (page, limit) => {
|
||||
const newPagination = {
|
||||
page,
|
||||
limit,
|
||||
total: props.paginationInfo.total,
|
||||
pages: Math.ceil(props.paginationInfo.total / limit),
|
||||
offset: 0,
|
||||
};
|
||||
emit("update:paginationInfo", newPagination);
|
||||
emit("limitChanged", { pageNumber: page, limit });
|
||||
};
|
||||
|
||||
// --- همگامسازی با prop خارجی (در صورت تغییر از خارج) ---
|
||||
watch(
|
||||
() => props.paginationInfo,
|
||||
(newVal) => {
|
||||
internalPage.value = newVal.page;
|
||||
internalLimit.value = newVal.limit;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.jahat-pagination {
|
||||
direction: rtl;
|
||||
}
|
||||
/* حذف اسپینر در این کامپوننت */
|
||||
.no-spinner {
|
||||
-moz-appearance: textfield; /* فایرفاکس */
|
||||
}
|
||||
|
||||
.no-spinner::-webkit-inner-spin-button,
|
||||
.no-spinner::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.no-spinner {
|
||||
appearance: textfield;
|
||||
}
|
||||
</style>
|
||||
9
app/components/lazy-load/data-entry/MainList.vue
Normal file
9
app/components/lazy-load/data-entry/MainList.vue
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<h1>main list</h1>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
11
app/components/lazy-load/data-entry/RelationEdit.vue
Normal file
11
app/components/lazy-load/data-entry/RelationEdit.vue
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<h1>RelationEdit</h1>
|
||||
|
||||
<TiptapEditor></TiptapEditor>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
11
app/components/lazy-load/data-entry/RuleEdit.vue
Normal file
11
app/components/lazy-load/data-entry/RuleEdit.vue
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<h1>RuleEdit</h1>
|
||||
|
||||
<TiptapEditor></TiptapEditor>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
137
app/composables/composSystemTheme.ts
Executable file
137
app/composables/composSystemTheme.ts
Executable file
|
|
@ -0,0 +1,137 @@
|
|||
// composables/composSystemTheme.ts
|
||||
import type { Ref } from "vue";
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
import { useState } from "#imports";
|
||||
|
||||
type Theme = {
|
||||
name: string;
|
||||
title: string;
|
||||
subTitle: string;
|
||||
logo: {
|
||||
light: string;
|
||||
dark: string;
|
||||
};
|
||||
font: string;
|
||||
fontFiles: Array<{ weight: string; style: string; src: string }>;
|
||||
colors: {
|
||||
primary: Record<
|
||||
| "50"
|
||||
| "100"
|
||||
| "200"
|
||||
| "300"
|
||||
| "400"
|
||||
| "500"
|
||||
| "600"
|
||||
| "700"
|
||||
| "800"
|
||||
| "900"
|
||||
| "950",
|
||||
string
|
||||
>;
|
||||
};
|
||||
};
|
||||
|
||||
export function composSystemTheme() {
|
||||
const currentTheme = useState<Theme | null>("system-theme", () => null);
|
||||
const isDark = useState<boolean>("is-dark", () => false);
|
||||
const isReady = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
const saved = localStorage.getItem("theme-mode");
|
||||
const shouldBeDark = saved === "dark";
|
||||
if (isDark.value !== shouldBeDark) {
|
||||
isDark.value = shouldBeDark;
|
||||
}
|
||||
isReady.value = true;
|
||||
});
|
||||
|
||||
// فقط هنگامی که کاربر دستی تم را تغییر داد، ذخیره کن
|
||||
watch(
|
||||
() => isDark.value,
|
||||
(val) => {
|
||||
if (!isReady.value || process.server) return;
|
||||
|
||||
localStorage.setItem("theme-mode", val ? "dark" : "light");
|
||||
const root = document.documentElement;
|
||||
val ? root.classList.add("dark") : root.classList.remove("dark");
|
||||
},
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
isDark.value = !isDark.value;
|
||||
};
|
||||
|
||||
const applyTheme = async () => {
|
||||
const system = useRuntimeConfig().public.system as string;
|
||||
if (!system) {
|
||||
console.warn("No system specified in runtime config");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 🔹 دقت: مسیر دقیقاً ~/assets/${system}/theme.json
|
||||
const themeModule = await import(`~/assets/${system}/theme.json`);
|
||||
const theme = themeModule.default || themeModule;
|
||||
|
||||
if (process.client) {
|
||||
const root = document.documentElement;
|
||||
|
||||
// رنگها
|
||||
Object.entries(theme.colors.primary).forEach(([key, value]) => {
|
||||
root.style.setProperty(`--color-primary-${key}`, value);
|
||||
});
|
||||
|
||||
// فونت
|
||||
if (theme.font && theme.fontFiles) {
|
||||
root.style.setProperty("--app-font", theme.font);
|
||||
|
||||
const fontId = `font-${theme.font}`;
|
||||
if (!document.getElementById(fontId)) {
|
||||
const style = document.createElement("style");
|
||||
style.id = fontId;
|
||||
let rules = "";
|
||||
theme.fontFiles.forEach((file) => {
|
||||
rules += `
|
||||
@font-face {
|
||||
font-family: "${theme.font}";
|
||||
src: url("${file.src}") format("woff2");
|
||||
font-weight: ${file.weight};
|
||||
font-style: ${file.style};
|
||||
font-display: swap;
|
||||
}
|
||||
`;
|
||||
});
|
||||
style.textContent = rules;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentTheme.value = theme;
|
||||
return theme;
|
||||
} catch (error) {
|
||||
console.error(`Failed to load theme for system: ${system}`, error);
|
||||
}
|
||||
};
|
||||
|
||||
const logo = computed(() => {
|
||||
if (!currentTheme.value) return "";
|
||||
return isDark.values
|
||||
? currentTheme.value.logo.dark
|
||||
: currentTheme.value.logo.light;
|
||||
});
|
||||
|
||||
const primaryColor = computed(() => {
|
||||
return currentTheme.value?.colors.primary["500"] || "#3b82f6";
|
||||
});
|
||||
|
||||
return {
|
||||
applyTheme,
|
||||
currentTheme: currentTheme as Ref<Theme | null>,
|
||||
isDark,
|
||||
toggleDarkMode,
|
||||
logo,
|
||||
primaryColor,
|
||||
};
|
||||
}
|
||||
45
app/composables/useConfirm.ts
Executable file
45
app/composables/useConfirm.ts
Executable file
|
|
@ -0,0 +1,45 @@
|
|||
// ~/composables/confirm.ts
|
||||
import { reactive, readonly } from "vue";
|
||||
|
||||
// یک state مشترک برای تمام کامپوننتها
|
||||
const confirmState = reactive({
|
||||
isOpen: false,
|
||||
title: "تأیید عملیات",
|
||||
message: "آیا از انجام این عملیات اطمینان دارید؟",
|
||||
resolve: null as ((value: boolean) => void) | null,
|
||||
});
|
||||
|
||||
export const useConfirmState = () => readonly(confirmState);
|
||||
|
||||
export const useConfirm = () => {
|
||||
const ucoShowConfirmModal = (
|
||||
options: { title?: string; message?: string } = {}
|
||||
): Promise<boolean> => {
|
||||
confirmState.title = options.title || "تأیید عملیات";
|
||||
confirmState.message =
|
||||
options.message || "آیا از انجام این عملیات اطمینان دارید؟";
|
||||
confirmState.isOpen = true;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
confirmState.resolve = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
return { ucoShowConfirmModal };
|
||||
};
|
||||
|
||||
export const useConfirmActions = () => {
|
||||
const ucoConfirm = () => {
|
||||
confirmState.isOpen = false;
|
||||
if (confirmState.resolve) confirmState.resolve(true);
|
||||
confirmState.resolve = null;
|
||||
};
|
||||
|
||||
const ucoCancelConfirmModal = () => {
|
||||
confirmState.isOpen = false;
|
||||
if (confirmState.resolve) confirmState.resolve(false);
|
||||
confirmState.resolve = null;
|
||||
};
|
||||
|
||||
return { ucoConfirm, ucoCancelConfirmModal };
|
||||
};
|
||||
441
app/json/EditorSchema.json
Executable file
441
app/json/EditorSchema.json
Executable file
|
|
@ -0,0 +1,441 @@
|
|||
{
|
||||
"toolbarButtons": [
|
||||
{
|
||||
"group": "textSettings",
|
||||
"title": "تنظیمات متن",
|
||||
"buttons": [
|
||||
{
|
||||
"action": "toggleHeading",
|
||||
"level": 1,
|
||||
"label": "عنوان ۱",
|
||||
"title": "عنوان 1",
|
||||
"class": "text-sm font-semibold"
|
||||
},
|
||||
{
|
||||
"action": "toggleHeading",
|
||||
"level": 2,
|
||||
"label": "عنوان ۲",
|
||||
"title": "عنوان 2",
|
||||
"class": "text-sm font-semibold"
|
||||
},
|
||||
{
|
||||
"action": "toggleHeading",
|
||||
"level": 3,
|
||||
"label": "عنوان ۳",
|
||||
"title": "عنوان 3",
|
||||
"class": "text-sm font-semibold"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "textFormatting",
|
||||
"title": "قالببندی متن",
|
||||
"buttons": [
|
||||
{
|
||||
"action": "toggleBold",
|
||||
"label": "ب",
|
||||
"title": "پررنگ (Ctrl+B)",
|
||||
"class": "font-bold text-sm"
|
||||
},
|
||||
{
|
||||
"action": "toggleItalic",
|
||||
"label": "ک",
|
||||
"title": "کج (Ctrl+I)",
|
||||
"class": "italic text-sm"
|
||||
},
|
||||
{
|
||||
"action": "toggleLink",
|
||||
"label": "🔗",
|
||||
"title": "لینک (Ctrl+K)",
|
||||
"class": "text-sm"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "lists",
|
||||
"title": "لیستها",
|
||||
"buttons": [
|
||||
{
|
||||
"action": "toggleBulletList",
|
||||
"label": "•",
|
||||
"title": "لیست نقطهای",
|
||||
"class": "text-lg"
|
||||
},
|
||||
{
|
||||
"action": "toggleOrderedList",
|
||||
"label": "۱.",
|
||||
"title": "لیست شمارهای",
|
||||
"class": ""
|
||||
},
|
||||
{
|
||||
"action": "toggleTaskList",
|
||||
"label": "✓",
|
||||
"title": "لیست کارها",
|
||||
"class": "text-sm"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "blocks",
|
||||
"title": "بلوکها",
|
||||
"buttons": [
|
||||
{
|
||||
"action": "toggleCodeBlock",
|
||||
"label": "{ }",
|
||||
"title": "بلوک کد",
|
||||
"class": "text-sm"
|
||||
},
|
||||
{
|
||||
"action": "toggleBlockquote",
|
||||
"label": "\"",
|
||||
"title": "نقل قول",
|
||||
"class": "text-lg"
|
||||
},
|
||||
{
|
||||
"action": "setHorizontalRule",
|
||||
"label": "―",
|
||||
"title": "خط جداکننده",
|
||||
"class": "text-lg"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "alignment",
|
||||
"title": "ترازبندی",
|
||||
"buttons": [
|
||||
{
|
||||
"action": "setTextAlign",
|
||||
"value": "right",
|
||||
"label": "→|",
|
||||
"title": "تراز راست",
|
||||
"class": "text-sm"
|
||||
},
|
||||
{
|
||||
"action": "setTextAlign",
|
||||
"value": "center",
|
||||
"label": "|→|",
|
||||
"title": "تراز وسط",
|
||||
"class": "text-sm"
|
||||
},
|
||||
{
|
||||
"action": "setTextAlign",
|
||||
"value": "left",
|
||||
"label": "|←",
|
||||
"title": "تراز چپ",
|
||||
"class": "text-sm"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "operations",
|
||||
"title": "عملیات",
|
||||
"buttons": [
|
||||
{
|
||||
"action": "undo",
|
||||
"label": "↶",
|
||||
"title": "بازگشت (Ctrl+Z)",
|
||||
"class": "text-lg"
|
||||
},
|
||||
{
|
||||
"action": "redo",
|
||||
"label": "↷",
|
||||
"title": "بازگشت به جلو (Ctrl+Shift+Z)",
|
||||
"class": "text-lg"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"contextMenuItems": [
|
||||
{
|
||||
"section": "ai",
|
||||
"label": "هوش مصنوعی",
|
||||
"icon": "i-lucide-sparkles",
|
||||
"items": [
|
||||
[
|
||||
{
|
||||
"label": "بهبود نوشتار",
|
||||
"icon": "i-lucide-pen-line",
|
||||
"kbds": ["AI", "I"],
|
||||
"action": "improve"
|
||||
},
|
||||
{
|
||||
"label": "ادامه دادن",
|
||||
"icon": "i-lucide-redo",
|
||||
"kbds": ["AI", "I"],
|
||||
"action": "continue"
|
||||
},
|
||||
{
|
||||
"label": "تیتر گذاری",
|
||||
"icon": "i-lucide-heading",
|
||||
"kbds": ["AI", "I"],
|
||||
"action": "title"
|
||||
},
|
||||
{
|
||||
"label": "خلاصهسازی",
|
||||
"icon": "i-lucide-file-text",
|
||||
"kbds": ["AI", "S"],
|
||||
"action": "summarize"
|
||||
},
|
||||
|
||||
{
|
||||
"label": "اصلاح املایی",
|
||||
"icon": "i-lucide-eraser",
|
||||
"kbds": ["AI", "I"],
|
||||
"action": "spellcheck"
|
||||
},
|
||||
{
|
||||
"label": "سادهسازی",
|
||||
"icon": "i-lucide-spell-check",
|
||||
"kbds": ["AI", "I"],
|
||||
"action": "simplify"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"label": "ترجمه",
|
||||
"icon": "i-lucide-languages",
|
||||
"kbds": ["AI", "T"],
|
||||
"action": "translate",
|
||||
"children": [
|
||||
[
|
||||
{
|
||||
"label": "انگلیسی",
|
||||
"icon": "i-lucide-globe",
|
||||
"action": "translate",
|
||||
"lang": "انگلیسی"
|
||||
},
|
||||
{
|
||||
"label": "فارسی",
|
||||
"icon": "i-lucide-globe",
|
||||
"action": "translate",
|
||||
"lang": "فارسی"
|
||||
},
|
||||
{
|
||||
"label": "عربی",
|
||||
"icon": "i-lucide-globe",
|
||||
"action": "translate",
|
||||
"lang": "عربی"
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "توضیح",
|
||||
"icon": "i-lucide-lightbulb",
|
||||
"kbds": ["AI", "E"],
|
||||
"action": "explain"
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "formatting",
|
||||
"label": "قالببندی",
|
||||
"icon": "i-lucide-type",
|
||||
"items": [
|
||||
[
|
||||
{
|
||||
"label": "پررنگ",
|
||||
"icon": "i-lucide-bold",
|
||||
"kbds": ["ctrl", "B"],
|
||||
"action": "toggleBold"
|
||||
},
|
||||
{
|
||||
"label": "کج",
|
||||
"icon": "i-lucide-italic",
|
||||
"kbds": ["ctrl", "I"],
|
||||
"action": "toggleItalic"
|
||||
},
|
||||
{
|
||||
"label": "زیرخط",
|
||||
"icon": "i-lucide-underline",
|
||||
"kbds": ["ctrl", "U"],
|
||||
"action": "toggleUnderline"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"label": "کد",
|
||||
"icon": "i-lucide-code",
|
||||
"kbds": ["ctrl", "E"],
|
||||
"action": "toggleCode"
|
||||
},
|
||||
{
|
||||
"label": "لینک",
|
||||
"icon": "i-lucide-link",
|
||||
"kbds": ["ctrl", "K"],
|
||||
"action": "toggleLink"
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "operations",
|
||||
"label": "عملیات",
|
||||
"icon": "i-lucide-scissors",
|
||||
"items": [
|
||||
[
|
||||
{
|
||||
"label": "کپی",
|
||||
"icon": "i-lucide-copy",
|
||||
"kbds": ["ctrl", "C"],
|
||||
"action": "copy"
|
||||
},
|
||||
{
|
||||
"label": "برش",
|
||||
"icon": "i-lucide-scissors",
|
||||
"kbds": ["ctrl", "X"],
|
||||
"action": "cut"
|
||||
},
|
||||
{
|
||||
"label": "چسباندن",
|
||||
"icon": "i-lucide-clipboard-paste",
|
||||
"kbds": ["ctrl", "V"],
|
||||
"action": "paste"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"label": "حذف",
|
||||
"icon": "i-lucide-trash",
|
||||
"kbds": ["Delete"],
|
||||
"action": "delete",
|
||||
"color": "error"
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "convert",
|
||||
"label": "تبدیل به",
|
||||
"icon": "i-lucide-shapes",
|
||||
"items": [
|
||||
[
|
||||
{
|
||||
"label": "عنوان ۱",
|
||||
"icon": "i-lucide-heading-1",
|
||||
"action": "convertToHeading1"
|
||||
},
|
||||
{
|
||||
"label": "عنوان ۲",
|
||||
"icon": "i-lucide-heading-2",
|
||||
"action": "convertToHeading2"
|
||||
},
|
||||
{
|
||||
"label": "عنوان ۳",
|
||||
"icon": "i-lucide-heading-3",
|
||||
"action": "convertToHeading3"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"label": "لیست نقطهای",
|
||||
"icon": "i-lucide-list",
|
||||
"action": "convertToBulletList"
|
||||
},
|
||||
{
|
||||
"label": "لیست شمارهای",
|
||||
"icon": "i-lucide-list-ordered",
|
||||
"action": "convertToOrderedList"
|
||||
},
|
||||
{
|
||||
"label": "لیست کارها",
|
||||
"icon": "i-lucide-check-square",
|
||||
"action": "convertToTaskList"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"label": "کد",
|
||||
"icon": "i-lucide-code",
|
||||
"action": "convertToCodeBlock"
|
||||
},
|
||||
{
|
||||
"label": "نقل قول",
|
||||
"icon": "i-lucide-quote",
|
||||
"action": "convertToBlockquote"
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "alignment",
|
||||
"label": "ترازبندی",
|
||||
"icon": "i-lucide-align-left",
|
||||
"items": [
|
||||
[
|
||||
{
|
||||
"label": "راست",
|
||||
"icon": "i-lucide-align-right",
|
||||
"action": "alignRight"
|
||||
},
|
||||
{
|
||||
"label": "وسط",
|
||||
"icon": "i-lucide-align-center",
|
||||
"action": "alignCenter"
|
||||
},
|
||||
{
|
||||
"label": "چپ",
|
||||
"icon": "i-lucide-align-left",
|
||||
"action": "alignLeft"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"blockIcons": {
|
||||
"paragraph": "📝",
|
||||
"heading1": "H1",
|
||||
"heading2": "H2",
|
||||
"heading3": "H3",
|
||||
"todo": "✓",
|
||||
"bullet": "•",
|
||||
"number": "1.",
|
||||
"code": "{ }",
|
||||
"quote": "\"",
|
||||
"image": "🖼️",
|
||||
"file": "📎",
|
||||
"table": "📊",
|
||||
"callout": "💡"
|
||||
},
|
||||
|
||||
"blockTypeNames": {
|
||||
"paragraph": "متن",
|
||||
"heading1": "عنوان ۱",
|
||||
"heading2": "عنوان ۲",
|
||||
"heading3": "عنوان ۳",
|
||||
"todo": "لیست کار",
|
||||
"bullet": "لیست نقطهای",
|
||||
"number": "لیست شمارهای",
|
||||
"code": "کد",
|
||||
"quote": "نقل قول",
|
||||
"callout": "کالاوت"
|
||||
},
|
||||
|
||||
"alignmentNames": {
|
||||
"right": "راست",
|
||||
"center": "وسط",
|
||||
"left": "چپ"
|
||||
},
|
||||
|
||||
"editorConfig": {
|
||||
"placeholder": "شروع به نوشتن کنید...",
|
||||
"emptyEditorClass": "is-editor-empty",
|
||||
"attributes": {
|
||||
"class": "notion-editor",
|
||||
"dir": "rtl",
|
||||
"spellcheck": "false"
|
||||
},
|
||||
"linkAttributes": {
|
||||
"class": "text-blue-600 hover:text-blue-800 underline transition-colors",
|
||||
"dir": "ltr",
|
||||
"target": "_blank",
|
||||
"rel": "noopener noreferrer"
|
||||
},
|
||||
"defaultAlignment": "right"
|
||||
},
|
||||
|
||||
"welcomeContent": "<div style=\"text-align: right;\">\n <p>فلسفه اعم از همه علوم و معارف است، زيرا موضوع آن (موجود) عام ترين موضوعات و در برگيرنده همه چيزهاست. علوم كلًاّ از حيث ثبوت موضوع متوقف بر فلسفه اند، اما فلسفه در ثبوت موضوع خود بر هيچ يك از علوم مبتنى نيست</p> </div>"
|
||||
}
|
||||
18
app/json/header/header.json
Executable file
18
app/json/header/header.json
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"languages": [
|
||||
{ "label": "فارسی", "icon": "i-flagpack-ir" },
|
||||
{ "label": "English", "icon": "i-flagpack-gb-ukm" },
|
||||
{ "label": "العربية", "icon": "i-flagpack-sa" }
|
||||
],
|
||||
"userMenu": [
|
||||
[
|
||||
{ "label": "پروفایل", "icon": "i-heroicons-user-circle","key":"profile" },
|
||||
{ "label": "تنظیمات", "icon": "i-heroicons-cog-6-tooth" ,"key":"settings" },
|
||||
{ "label": "برنامهنویس فنی", "icon": "i-heroicons-code-bracket" ,"key":"developer" },
|
||||
{ "label": " پنل مدیریت", "icon": "i-heroicons-cog-6-tooth" ,"key":"admin" }
|
||||
],
|
||||
[
|
||||
{ "label": "خروج", "icon": "i-heroicons-arrow-left-on-rectangle", "color": "red", "key": "logout" }
|
||||
]
|
||||
]
|
||||
}
|
||||
12
app/json/sidebar/dashboard.json
Executable file
12
app/json/sidebar/dashboard.json
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"topMenu": [
|
||||
{
|
||||
"label": "پیشخوان",
|
||||
"icon": "i-lucide-home",
|
||||
"to": "/dashboard/base",
|
||||
"develop": 0,
|
||||
"active": true
|
||||
}
|
||||
],
|
||||
"bottomMenu": []
|
||||
}
|
||||
50
app/json/tab-bar/dashboard/dashboard.json
Executable file
50
app/json/tab-bar/dashboard/dashboard.json
Executable file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"tabs": [
|
||||
{
|
||||
"id": "dashboard",
|
||||
"label": "داشبورد",
|
||||
"icon": "i-heroicons-chart-bar"
|
||||
},
|
||||
{
|
||||
"id": "dashboard1",
|
||||
"label": "داشبورد",
|
||||
"icon": "i-heroicons-chart-bar"
|
||||
},
|
||||
{
|
||||
"id": "dashboard3",
|
||||
"label": "داشبورد",
|
||||
"icon": "i-heroicons-chart-bar"
|
||||
},
|
||||
{
|
||||
"id": "dashboard5",
|
||||
"label": "داشبورد",
|
||||
"icon": "i-heroicons-chart-bar"
|
||||
},
|
||||
{
|
||||
"id": "dashboard8",
|
||||
"label": "داشبورد",
|
||||
"icon": "i-heroicons-chart-bar"
|
||||
},
|
||||
{
|
||||
"id": "dashboard7",
|
||||
"label": "داشبورد",
|
||||
"icon": "i-heroicons-chart-bar"
|
||||
},
|
||||
{
|
||||
"id": "dashboard11",
|
||||
"label": "داشبورد",
|
||||
"icon": "i-heroicons-chart-bar"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "users",
|
||||
"label": "کاربران",
|
||||
"icon": "i-heroicons-users"
|
||||
},
|
||||
{
|
||||
"id": "users2",
|
||||
"label": "کاربران",
|
||||
"icon": "i-heroicons-users"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
app/json/tab-bar/data-entry/dataEntry.json
Executable file
26
app/json/tab-bar/data-entry/dataEntry.json
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"tabs": [
|
||||
{
|
||||
"id": "MainList",
|
||||
"key": "MainList",
|
||||
"label": "فهرست",
|
||||
"icon": "emojione-closed-book",
|
||||
"develop": 0,
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": "RuleEdit",
|
||||
"label": "احکام",
|
||||
"key": "RuleEdit",
|
||||
"develop": 0,
|
||||
"icon": "i-mdi-library-outline"
|
||||
},
|
||||
{
|
||||
"id": "RelationEdit",
|
||||
"key": "RelationEdit",
|
||||
"icon": "emojione-orange-book",
|
||||
"develop": 0,
|
||||
"label": "روابط"
|
||||
}
|
||||
]
|
||||
}
|
||||
29
app/json/tab-bar/data-entry/manage.json
Executable file
29
app/json/tab-bar/data-entry/manage.json
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"tabs": [
|
||||
{
|
||||
"id": "info",
|
||||
"label": "اطلاعات پایه",
|
||||
"key": "infoContent",
|
||||
"develop": 0,
|
||||
"icon": "i-mdi-database-outline"
|
||||
},
|
||||
{
|
||||
"label": "آمار",
|
||||
"id": "Statistics",
|
||||
"develop": 1,
|
||||
"icon": "i-lucide-bar-chart"
|
||||
},
|
||||
{
|
||||
"label": "فعالیت کاربران",
|
||||
"id": "UserReports",
|
||||
"develop": 1,
|
||||
"icon": "i-lucide-users"
|
||||
},
|
||||
{
|
||||
"label": "گزارشات",
|
||||
"to": "Reports",
|
||||
"develop": 1,
|
||||
"icon": "i-heroicons-chart-bar"
|
||||
}
|
||||
]
|
||||
}
|
||||
325
app/json/tab-bar/data-entry/sampelData.json
Executable file
325
app/json/tab-bar/data-entry/sampelData.json
Executable file
|
|
@ -0,0 +1,325 @@
|
|||
{
|
||||
"items": [],
|
||||
"tableActions": [
|
||||
{
|
||||
"key": "importInfo",
|
||||
"icon": "i-lucide-info",
|
||||
"title": "ورود اطلاعات"
|
||||
},
|
||||
|
||||
{
|
||||
"key": "manageFiles",
|
||||
"icon": "i-lucide-upload",
|
||||
"title": "بارگذاری فایل"
|
||||
}
|
||||
],
|
||||
"tableColumns": [
|
||||
{
|
||||
"key": "branch",
|
||||
"title": "دوره",
|
||||
"isLink": true,
|
||||
"width": "18%",
|
||||
"contextmenu": true
|
||||
},
|
||||
{ "key": "title", "title": "عنوان", "width": "15%", "contextmenu": true },
|
||||
{ "key": "subtitle", "title": "عنوان محتوایی", "width": "25%" },
|
||||
{ "key": "meet_code", "title": "کد جلسه", "width": "12%" },
|
||||
{
|
||||
"key": "begin_date",
|
||||
"title": "تاریخ",
|
||||
"isLink": true,
|
||||
"width": "12%",
|
||||
"contextmenu": true
|
||||
},
|
||||
{ "key": "author", "title": "مولف", "width": "18%" }
|
||||
],
|
||||
"menuItems": [
|
||||
[
|
||||
{
|
||||
"label": "ویرایش سریع",
|
||||
"key": "quick-edit",
|
||||
"icon": "heroicons:pencil-square"
|
||||
},
|
||||
{
|
||||
"label": "کپی مقدار",
|
||||
"key": "copy",
|
||||
"icon": "heroicons:clipboard-document"
|
||||
},
|
||||
{
|
||||
"label": "جزئیات",
|
||||
"key": "details",
|
||||
"icon": "heroicons:information-circle"
|
||||
}
|
||||
]
|
||||
],
|
||||
"schemaItems": {
|
||||
"collapse_items": {
|
||||
"key": "collapse",
|
||||
"items": [
|
||||
{
|
||||
"key": "title",
|
||||
"items": [
|
||||
{
|
||||
"key": "branch",
|
||||
"source_key": "branch",
|
||||
"label": "دوره :",
|
||||
"style": "search-label",
|
||||
"hilight_key": "branch",
|
||||
"link_route": {
|
||||
"id": "_id",
|
||||
"name": "navigationView",
|
||||
"key": "sanad",
|
||||
"query": "listkey=branch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "author",
|
||||
"source_key": "author",
|
||||
"label": "",
|
||||
"style": "search-label"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "inner_hits",
|
||||
"array_key": "inner_hits.by_collapse.hits.hits",
|
||||
"items": [
|
||||
{
|
||||
"key": "format",
|
||||
"source_key": "format",
|
||||
"label": "",
|
||||
"style": "search-tag"
|
||||
},
|
||||
{
|
||||
"key": "meet_lid",
|
||||
"source_key": "meet_lid",
|
||||
"label": "کدداخلی :",
|
||||
"style": "search-label"
|
||||
},
|
||||
{
|
||||
"key": "meet_code",
|
||||
"source_key": "meet_code",
|
||||
"label": "کد جلسه :",
|
||||
"style": "search-label"
|
||||
},
|
||||
{
|
||||
"key": "begin_date",
|
||||
"source_key": "begin_date",
|
||||
"label": "تاریخ :",
|
||||
"style": "search-label",
|
||||
"process": "convert_date"
|
||||
},
|
||||
{
|
||||
"key": "title",
|
||||
"source_key": "title,subtitle",
|
||||
"label": "",
|
||||
"style": "search-title",
|
||||
"link_route": {
|
||||
"id": "_id",
|
||||
"name": "navigationView",
|
||||
"key": "sanad"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"key": "title",
|
||||
"items": [
|
||||
{
|
||||
"key": "format",
|
||||
"source_key": "format",
|
||||
"label": "",
|
||||
"style": "search-tag"
|
||||
},
|
||||
{
|
||||
"key": "title",
|
||||
"source_key": "title,subtitle",
|
||||
"label": "",
|
||||
"style": "search-title",
|
||||
"link_route": {
|
||||
"id": "_id",
|
||||
"name": "navigationView",
|
||||
"key": "sanad"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "subtitle",
|
||||
"items": [
|
||||
{
|
||||
"key": "meet_lid",
|
||||
"source_key": "meet_lid",
|
||||
"label": "کدداخلی :",
|
||||
"style": "search-label"
|
||||
},
|
||||
{
|
||||
"key": "meet_code",
|
||||
"source_key": "meet_code",
|
||||
"label": "کد جلسه :",
|
||||
"style": "search-label"
|
||||
},
|
||||
{
|
||||
"key": "research_code",
|
||||
"source_key": "research_code",
|
||||
"label": "کد دوره :",
|
||||
"style": "search-label"
|
||||
},
|
||||
{
|
||||
"key": "id",
|
||||
"source_key": "id",
|
||||
"label": "کد پورتال :",
|
||||
"style": "search-label"
|
||||
},
|
||||
{
|
||||
"key": "begin_date",
|
||||
"source_key": "begin_date",
|
||||
"label": "تاریخ :",
|
||||
"style": "search-label",
|
||||
"process": "convert_date"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "subtitle2",
|
||||
"items": [
|
||||
{
|
||||
"key": "branch",
|
||||
"source_key": "branch",
|
||||
"label": "دوره :",
|
||||
"style": "search-label",
|
||||
"hilight_key": "branch",
|
||||
"link_route": {
|
||||
"id": "_id",
|
||||
"name": "navigationView",
|
||||
"key": "sanad",
|
||||
"query": "listkey=branch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "author",
|
||||
"source_key": "author",
|
||||
"label": "",
|
||||
"style": "search-label"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "body",
|
||||
"items": [
|
||||
{
|
||||
"key": "content",
|
||||
"source_key": "content,mindex,mintro",
|
||||
"label": "",
|
||||
"hilight_key": "content",
|
||||
"style": "search-body"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "keywords",
|
||||
"items": [
|
||||
{
|
||||
"key": "keywords",
|
||||
"source_key": "keywords",
|
||||
"label": "واژهگان :",
|
||||
"style": "search-label"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "media",
|
||||
"items": [
|
||||
{
|
||||
"key": "films",
|
||||
"source_key": "films",
|
||||
"label": "فیلم :",
|
||||
"style": "search-label"
|
||||
},
|
||||
{
|
||||
"key": "voices",
|
||||
"source_key": "voices",
|
||||
"label": "صوت :",
|
||||
"style": "search-label"
|
||||
},
|
||||
{
|
||||
"key": "photos",
|
||||
"source_key": "photos",
|
||||
"label": "تصاویر :",
|
||||
"style": "search-label"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "subject",
|
||||
"items": [
|
||||
{
|
||||
"key": "subject",
|
||||
"isArray": 1,
|
||||
"source_key": "subject",
|
||||
"label": "موضوع :",
|
||||
"style": "search-label"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"title": "علاقه مندی ها",
|
||||
"key": "tbookmark",
|
||||
"can": "favorite_create",
|
||||
"type": "button",
|
||||
"api_items": {
|
||||
"data_type": "bookmark",
|
||||
"ref_key": "sanad",
|
||||
"id": "_id",
|
||||
"title": "_source.title"
|
||||
},
|
||||
|
||||
"toggle_icons": {
|
||||
"icon1": "i-heroicons-bookmark-solid",
|
||||
"icon2": "i-heroicons-bookmark"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"icon": "i-heroicons-information-circle",
|
||||
"title": "مشخصات",
|
||||
"key": "summary",
|
||||
"can": "search_summary",
|
||||
"type": "button"
|
||||
},
|
||||
|
||||
{
|
||||
"icon": "i-mdi-content-copy",
|
||||
"title": "کپی لینک",
|
||||
"key": "copy",
|
||||
"type": "button",
|
||||
"link_route": { "id": "_id", "name": "navigationView", "key": "sanad" }
|
||||
}
|
||||
]
|
||||
},
|
||||
"pagination": {
|
||||
"pages": 0,
|
||||
"total": 0,
|
||||
"page": 1,
|
||||
"offset": 0,
|
||||
"limit": 10
|
||||
},
|
||||
"switchSchema": [
|
||||
{ "key": "list", "label": "نمایش جدول", "icon": "i-ri:table-fill" },
|
||||
{
|
||||
"key": "table",
|
||||
"label": "نمایش لیستی",
|
||||
"icon": "i-lsicon:list-outline"
|
||||
}
|
||||
],
|
||||
"viewMode": "table",
|
||||
"contentKey": "sanad",
|
||||
"textSearch": "",
|
||||
"isSearchingState": true,
|
||||
"height": "calc(-20em + 100vh)"
|
||||
}
|
||||
333
app/json/tab-bar/data-entry/sampelDataDb.json
Executable file
333
app/json/tab-bar/data-entry/sampelDataDb.json
Executable file
|
|
@ -0,0 +1,333 @@
|
|||
{
|
||||
"items": [],
|
||||
"tableActions": [
|
||||
{
|
||||
"key": "meta_infos",
|
||||
"icon": "i-lucide-info",
|
||||
"title": "ورود اطلاعات"
|
||||
},
|
||||
|
||||
{
|
||||
"key": "manageFiles",
|
||||
"icon": "i-lucide-upload",
|
||||
"title": "بارگذاری فایل"
|
||||
},
|
||||
{
|
||||
"key": "hasMedia",
|
||||
"icon": "lucide:list-checks",
|
||||
"title": ""
|
||||
}
|
||||
],
|
||||
"tableColumns": [
|
||||
{
|
||||
"key": "branch",
|
||||
"title": "دوره",
|
||||
"isLink": true,
|
||||
"width": "18%",
|
||||
"contextmenu": true
|
||||
},
|
||||
{ "key": "title", "title": "عنوان", "width": "15%", "contextmenu": true },
|
||||
{ "key": "subtitle", "title": "عنوان محتوایی", "width": "25%" },
|
||||
{ "key": "meet_code", "title": "کد جلسه", "width": "12%" },
|
||||
{
|
||||
"key": "begin_date",
|
||||
"title": "تاریخ",
|
||||
"isLink": true,
|
||||
"width": "12%",
|
||||
"contextmenu": true
|
||||
},
|
||||
{ "key": "video_count", "title": "ت فیلم", "width": "5%" },
|
||||
{ "key": "photo_count", "title": "ت تصویر", "width": "5%" },
|
||||
{ "key": "sound_count", "title": "ت صوت", "width": "5%" },
|
||||
{ "key": "file_count", "title": "ت فایل", "width": "5%" }
|
||||
],
|
||||
"menuItems": [
|
||||
[
|
||||
{
|
||||
"label": "ویرایش سریع",
|
||||
"key": "quick-edit",
|
||||
"icon": "heroicons:pencil-square"
|
||||
},
|
||||
{
|
||||
"label": "کپی مقدار",
|
||||
"key": "copy",
|
||||
"icon": "heroicons:clipboard-document"
|
||||
},
|
||||
{
|
||||
"label": "جزئیات",
|
||||
"key": "details",
|
||||
"icon": "heroicons:information-circle"
|
||||
}
|
||||
]
|
||||
],
|
||||
"schemaItems": {
|
||||
"collapse_items": {
|
||||
"key": "collapse",
|
||||
"items": [
|
||||
{
|
||||
"key": "title",
|
||||
"items": [
|
||||
{
|
||||
"key": "branch",
|
||||
"source_key": "branch",
|
||||
"label": "دوره :",
|
||||
"style": "search-label",
|
||||
"hilight_key": "branch",
|
||||
"link_route": {
|
||||
"id": "_id",
|
||||
"name": "navigationView",
|
||||
"key": "sanad",
|
||||
"query": "listkey=branch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "author",
|
||||
"source_key": "author",
|
||||
"label": "",
|
||||
"style": "search-label"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "inner_hits",
|
||||
"array_key": "inner_hits.by_collapse.hits.hits",
|
||||
"items": [
|
||||
{
|
||||
"key": "format",
|
||||
"source_key": "format",
|
||||
"label": "",
|
||||
"style": "search-tag"
|
||||
},
|
||||
{
|
||||
"key": "meet_lid",
|
||||
"source_key": "meet_lid",
|
||||
"label": "کدداخلی :",
|
||||
"style": "search-label"
|
||||
},
|
||||
{
|
||||
"key": "meet_code",
|
||||
"source_key": "meet_code",
|
||||
"label": "کد جلسه :",
|
||||
"style": "search-label"
|
||||
},
|
||||
{
|
||||
"key": "begin_date",
|
||||
"source_key": "begin_date",
|
||||
"label": "تاریخ :",
|
||||
"style": "search-label",
|
||||
"process": "convert_date"
|
||||
},
|
||||
{
|
||||
"key": "title",
|
||||
"source_key": "title,subtitle",
|
||||
"label": "",
|
||||
"style": "search-title",
|
||||
"link_route": {
|
||||
"id": "_id",
|
||||
"name": "navigationView",
|
||||
"key": "sanad"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"key": "title",
|
||||
"items": [
|
||||
{
|
||||
"key": "format",
|
||||
"source_key": "format",
|
||||
"label": "",
|
||||
"style": "search-tag"
|
||||
},
|
||||
{
|
||||
"key": "title",
|
||||
"source_key": "title,subtitle",
|
||||
"label": "",
|
||||
"style": "search-title",
|
||||
"link_route": {
|
||||
"id": "_id",
|
||||
"name": "navigationView",
|
||||
"key": "sanad"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "subtitle",
|
||||
"items": [
|
||||
{
|
||||
"key": "meet_lid",
|
||||
"source_key": "meet_lid",
|
||||
"label": "کدداخلی :",
|
||||
"style": "search-label"
|
||||
},
|
||||
{
|
||||
"key": "meet_code",
|
||||
"source_key": "meet_code",
|
||||
"label": "کد جلسه :",
|
||||
"style": "search-label"
|
||||
},
|
||||
{
|
||||
"key": "research_code",
|
||||
"source_key": "research_code",
|
||||
"label": "کد دوره :",
|
||||
"style": "search-label"
|
||||
},
|
||||
{
|
||||
"key": "id",
|
||||
"source_key": "id",
|
||||
"label": "کد پورتال :",
|
||||
"style": "search-label"
|
||||
},
|
||||
{
|
||||
"key": "begin_date",
|
||||
"source_key": "begin_date",
|
||||
"label": "تاریخ :",
|
||||
"style": "search-label",
|
||||
"process": "convert_date"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "subtitle2",
|
||||
"items": [
|
||||
{
|
||||
"key": "branch",
|
||||
"source_key": "branch",
|
||||
"label": "دوره :",
|
||||
"style": "search-label",
|
||||
"hilight_key": "branch",
|
||||
"link_route": {
|
||||
"id": "_id",
|
||||
"name": "navigationView",
|
||||
"key": "sanad",
|
||||
"query": "listkey=branch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "author",
|
||||
"source_key": "author",
|
||||
"label": "",
|
||||
"style": "search-label"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "body",
|
||||
"items": [
|
||||
{
|
||||
"key": "content",
|
||||
"source_key": "content,mindex,mintro",
|
||||
"label": "",
|
||||
"hilight_key": "content",
|
||||
"style": "search-body"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "keywords",
|
||||
"items": [
|
||||
{
|
||||
"key": "keywords",
|
||||
"source_key": "keywords",
|
||||
"label": "واژهگان :",
|
||||
"style": "search-label"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "media",
|
||||
"items": [
|
||||
{
|
||||
"key": "films",
|
||||
"source_key": "films",
|
||||
"label": "فیلم :",
|
||||
"style": "search-label"
|
||||
},
|
||||
{
|
||||
"key": "voices",
|
||||
"source_key": "voices",
|
||||
"label": "صوت :",
|
||||
"style": "search-label"
|
||||
},
|
||||
{
|
||||
"key": "photos",
|
||||
"source_key": "photos",
|
||||
"label": "تصاویر :",
|
||||
"style": "search-label"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "subject",
|
||||
"items": [
|
||||
{
|
||||
"key": "subject",
|
||||
"isArray": 1,
|
||||
"source_key": "subject",
|
||||
"label": "موضوع :",
|
||||
"style": "search-label"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"title": "علاقه مندی ها",
|
||||
"key": "tbookmark",
|
||||
"can": "favorite_create",
|
||||
"type": "button",
|
||||
"api_items": {
|
||||
"data_type": "bookmark",
|
||||
"ref_key": "sanad",
|
||||
"id": "_id",
|
||||
"title": "_source.title"
|
||||
},
|
||||
|
||||
"toggle_icons": {
|
||||
"icon1": "i-heroicons-bookmark-solid",
|
||||
"icon2": "i-heroicons-bookmark"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"icon": "i-heroicons-information-circle",
|
||||
"title": "مشخصات",
|
||||
"key": "summary",
|
||||
"can": "search_summary",
|
||||
"type": "button"
|
||||
},
|
||||
|
||||
{
|
||||
"icon": "i-mdi-content-copy",
|
||||
"title": "کپی لینک",
|
||||
"key": "copy",
|
||||
"type": "button",
|
||||
"link_route": { "id": "_id", "name": "navigationView", "key": "sanad" }
|
||||
}
|
||||
]
|
||||
},
|
||||
"pagination": {
|
||||
"pages": 0,
|
||||
"total": 0,
|
||||
"page": 1,
|
||||
"offset": 0,
|
||||
"limit": 10
|
||||
},
|
||||
"switchSchema": [
|
||||
{ "key": "list", "label": "نمایش جدول", "icon": "i-ri:table-fill" },
|
||||
{
|
||||
"key": "table",
|
||||
"label": "نمایش لیستی",
|
||||
"icon": "i-lsicon:list-outline"
|
||||
}
|
||||
],
|
||||
"viewMode": "table",
|
||||
"contentKey": "sanad",
|
||||
"textSearch": "",
|
||||
"isSearchingState": true,
|
||||
"height": "calc(-20em + 100vh)"
|
||||
}
|
||||
23
app/json/tab-bar/data-entry/treeList.json
Executable file
23
app/json/tab-bar/data-entry/treeList.json
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"tabs": [
|
||||
{
|
||||
"id": "monir",
|
||||
"key": "monirContent",
|
||||
"label": "استاد حسینی",
|
||||
"icon": "emojione-closed-book",
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"id": "mirbagheri",
|
||||
"key": "mirbagheriContent",
|
||||
"icon": "emojione-orange-book",
|
||||
"label": "استاد میرباقری"
|
||||
},
|
||||
{
|
||||
"id": "farhangestan",
|
||||
"label": "فرهنگستان",
|
||||
"key": "farhangestanContent",
|
||||
"icon": "i-mdi-library-outline"
|
||||
}
|
||||
]
|
||||
}
|
||||
64
app/layouts/dashboardLayout.vue
Executable file
64
app/layouts/dashboardLayout.vue
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
<!-- app/layouts/dashboard.vue -->
|
||||
<template>
|
||||
<div class="flex bg-light-primary dark:bg-dark-primary">
|
||||
<Sidebar :sidebar-items="defaultSidebar" />
|
||||
<div class="flex-1 flex flex-col">
|
||||
<main
|
||||
class="flex-1 bg-light-primary dark:bg-dark-primary"
|
||||
>
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { watch } from "vue";
|
||||
import { useRoute } from "#imports";
|
||||
|
||||
// JSON فایلها
|
||||
import defaultSidebar from "@/json/sidebar/dashboard.json";
|
||||
|
||||
// import tabBarData from "@/json/tab-bar/dashboard/dashboard.json"
|
||||
|
||||
const route = useRoute();
|
||||
const onUserMenu = (action) => {
|
||||
switch (action.key) {
|
||||
case 'profile':
|
||||
navigateTo('/profile')
|
||||
break
|
||||
case 'settings':
|
||||
navigateTo('/settings')
|
||||
break
|
||||
case 'developer':
|
||||
navigateTo('/developer')
|
||||
break
|
||||
case 'admin':
|
||||
navigateTo('/admin')
|
||||
break
|
||||
break
|
||||
|
||||
case 'logout':
|
||||
console.log('logout from parent')
|
||||
break
|
||||
}
|
||||
}
|
||||
// دادههای سایدبار
|
||||
const sidebarData = route.meta.sidebarItems || defaultSidebar;
|
||||
const headerSchema = ref({});
|
||||
headerSchema.value = {
|
||||
breadcrumb: false,
|
||||
logo: true,
|
||||
};
|
||||
// اضافه کردن Route Meta برای سایدبار
|
||||
watch(
|
||||
() => route,
|
||||
(newRoute) => {
|
||||
sidebarData.value = newRoute.meta.sidebarItems || defaultSidebar;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
10
app/layouts/default.vue
Executable file
10
app/layouts/default.vue
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
<!-- app/layouts/default.vue -->
|
||||
<template>
|
||||
<div class="bg-light-primary dark:bg-dark-primary">
|
||||
|
||||
<main class="">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
34
app/middleware/route.global.ts
Executable file
34
app/middleware/route.global.ts
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
import { getUserPermission } from "@/stores/permissionStore";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
|
||||
let toRoute = useStorage("to_route", to.path);
|
||||
const userPermissionStore = getUserPermission();
|
||||
// let localStorageToRoute = toRoute.value;
|
||||
userPermissionStore.fetchUserPermissions();
|
||||
|
||||
const publicRoutes = ["/403", "/login", from.path];
|
||||
if (publicRoutes.includes(to.path)) return true;
|
||||
|
||||
if (
|
||||
!userPermissionStore.permissions ||
|
||||
!userPermissionStore.permissions.length
|
||||
) {
|
||||
try {
|
||||
await userPermissionStore.fetchUserPermissions();
|
||||
} catch (err) {
|
||||
console.error("Error fetching permissions:", err);
|
||||
return navigateTo("/403");
|
||||
}
|
||||
}
|
||||
const canAccess = userPermissionStore.hasPagePermission(to.fullPath);
|
||||
|
||||
// console.log("Checking permission for route:", to.fullPath, canAccess);
|
||||
|
||||
if (!canAccess) {
|
||||
return navigateTo("/403");
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
33
app/middleware/sidebar-items.js
Executable file
33
app/middleware/sidebar-items.js
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
// تعریف سایدبار پیشفرض
|
||||
const defaultSidebar = {
|
||||
topMenu: [
|
||||
{
|
||||
label: "پیشخوان",
|
||||
icon: "i-lucide-home",
|
||||
to: "/dashboard/base",
|
||||
active: true,
|
||||
},
|
||||
{ label: "اعلانات", icon: "i-lucide-bell", to: "/", badge: "۳" },
|
||||
{ label: "علاقهمندیها", icon: "i-lucide-bookmark", to: "/" },
|
||||
{ label: "تقویم", icon: "i-lucide-calendar", to: "/" },
|
||||
],
|
||||
bottomMenu: [
|
||||
{ label: "تم", icon: "i-lucide-palette", to: "/" },
|
||||
{ label: "زبان", icon: "i-lucide-languages", to: "/" },
|
||||
{ label: "حساب کاربری", icon: "i-lucide-user", to: "/" },
|
||||
{ label: "راهنما", icon: "i-lucide-help-circle", to: "/" },
|
||||
],
|
||||
};
|
||||
|
||||
// بررسی آیا route فعلی sidebarItems دارد یا نه
|
||||
const routeSidebar = to.meta?.sidebarItems;
|
||||
|
||||
if (routeSidebar) {
|
||||
// استفاده از سایدبار تعریف شده در route
|
||||
useState("sidebar", () => routeSidebar);
|
||||
} else {
|
||||
// استفاده از پیشفرض
|
||||
useState("sidebar", () => defaultSidebar);
|
||||
}
|
||||
});
|
||||
124
app/pages/403.vue
Executable file
124
app/pages/403.vue
Executable file
|
|
@ -0,0 +1,124 @@
|
|||
<template>
|
||||
<div
|
||||
class="min-h-[calc(100dvh-4em)] flex items-center justify-center p-4 bg-gray-100 dark:bg-dark-primary"
|
||||
>
|
||||
<div
|
||||
class="max-w-md w-full bg-white dark:bg-dark-primary-800 rounded-2xl shadow-xl p-8 text-center border border-gray-200 dark:border-dark-primary-600"
|
||||
>
|
||||
<!-- آیکون بزرگ -->
|
||||
<div class="mb-10">
|
||||
<div
|
||||
class="w-40 h-40 mx-auto bg-red-50 dark:bg-dark-primary-700 rounded-full flex items-center justify-center shadow-lg"
|
||||
>
|
||||
<svg
|
||||
class="w-32 h-32 text-red-600 dark:text-red-500"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- عنوان -->
|
||||
<h2
|
||||
class="text-2xl font-semibold text-gray-800 dark:text-dark-primary-100 mb-2"
|
||||
>
|
||||
دسترسی غیرمجاز
|
||||
</h2>
|
||||
|
||||
<!-- متن -->
|
||||
<div class="mb-6">
|
||||
<p class="text-gray-600 dark:text-dark-primary-300 text-lg mb-2">
|
||||
شما مجوز دسترسی به این صفحه را ندارید
|
||||
</p>
|
||||
<p class="text-gray-700 dark:text-dark-primary-200 font-medium">
|
||||
لطفا برای ادامه، ارتقاء دسترسی دهید
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- دکمه اصلی -->
|
||||
<!-- <div class="mb-8">
|
||||
<button
|
||||
@click="goToDashboard"
|
||||
class="w-full bg-primary-600 hover:bg-primary-700 text-white font-medium py-4 rounded-xl shadow-lg hover:shadow-xl transition-all duration-200"
|
||||
>
|
||||
بازگشت به داشبورد
|
||||
</button>
|
||||
</div> -->
|
||||
<div class="mb-8">
|
||||
<button
|
||||
@click="goBack"
|
||||
class="w-full bg-primary-600 hover:bg-primary-700 text-white font-medium py-4 rounded-xl shadow-lg hover:shadow-xl transition-all duration-200"
|
||||
>
|
||||
بازگشت به صفحه قبل
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- لینکهای دیگر -->
|
||||
<div class="space-y-3">
|
||||
<!-- <a
|
||||
href="/login"
|
||||
class="block text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300 hover:bg-primary-50 dark:hover:bg-dark-primary-700 py-3 rounded-lg transition duration-200 font-medium"
|
||||
>
|
||||
ثبتنام
|
||||
</a> -->
|
||||
<a
|
||||
@click.prevent="goToLogin"
|
||||
class="block text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300 hover:bg-primary-50 dark:hover:bg-dark-primary-700 py-3 rounded-lg transition duration-200 font-medium"
|
||||
>
|
||||
ورود ، ثبتنام
|
||||
</a>
|
||||
<a
|
||||
href="/contact"
|
||||
class="block text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300 hover:bg-primary-50 dark:hover:bg-dark-primary-700 py-3 rounded-lg transition duration-200 font-medium"
|
||||
>
|
||||
تماس با ما
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/about"
|
||||
class="block text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300 hover:bg-primary-50 dark:hover:bg-dark-primary-700 py-3 rounded-lg transition duration-200 font-medium"
|
||||
>
|
||||
درباره ما
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- پیام پایین -->
|
||||
<div
|
||||
class="mt-6 pt-6 border-t border-gray-200 dark:border-dark-primary-600"
|
||||
>
|
||||
<p class="text-gray-500 dark:text-dark-primary-400 text-sm">
|
||||
کد خطا: 403 • دسترسی ممنوع
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({ layout: "login-layout" });
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const goBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
// const goToDashboard = () => {
|
||||
// navigateTo({
|
||||
// name: "DashboardBasePage",
|
||||
// });
|
||||
// };
|
||||
const goToLogin = () => {
|
||||
navigateTo("/login");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* بدون استایل اضافی */
|
||||
</style>
|
||||
85
app/pages/index.vue
Executable file
85
app/pages/index.vue
Executable file
|
|
@ -0,0 +1,85 @@
|
|||
<template>
|
||||
<div>
|
||||
<Header
|
||||
:tabs="headerTabs"
|
||||
v-model:active-tab="activeTabKey"
|
||||
:is-sidebar-collapsed="isSidebarCollapsed"
|
||||
:unread-notifications="unreadNotifications"
|
||||
@tab-change="handleTabChange"
|
||||
:headerSchema="headerSchema"
|
||||
/>
|
||||
|
||||
<component
|
||||
:is="currentComponent"
|
||||
v-if="currentComponent"
|
||||
:activeTabKey="activeTabKey"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
name: "DashboardBasePage",
|
||||
layout: "dashboard-layout",
|
||||
});
|
||||
import { ref, onMounted } from "vue";
|
||||
import tabBarData from "@/json/tab-bar/data-entry/dataEntry.json";
|
||||
import { defineAsyncComponent } from "vue";
|
||||
const route = useRoute();
|
||||
|
||||
// Stateهای Header
|
||||
const isSidebarCollapsed = ref(false);
|
||||
const unreadNotifications = ref(5);
|
||||
const headerTabs = ref([]);
|
||||
headerTabs.value = tabBarData.tabs;
|
||||
const activeTabKey = ref(tabBarData.tabs[0]?.key || "");
|
||||
// const contentSchema = computed(() => ({
|
||||
// items: items.value,
|
||||
// }));
|
||||
const tabComponents = {
|
||||
MainList: defineAsyncComponent(
|
||||
() => import("~/components/lazy-load/data-entry/MainList.vue"),
|
||||
),
|
||||
RelationEdit: defineAsyncComponent(
|
||||
() => import("~/components/lazy-load/data-entry/RelationEdit.vue"),
|
||||
),
|
||||
RuleEdit: defineAsyncComponent(
|
||||
() => import("~/components/lazy-load/data-entry/RuleEdit.vue"),
|
||||
),
|
||||
// "DataEntryFarhangestan": defineAsyncComponent(() => import("~/components/lazy-load/data-entry/DataEntryFarhangestan.vue")),
|
||||
// "DataEntryMirbagheri": defineAsyncComponent(() => import("~/components/lazy-load/data-entry/DataEntryMirbagheri.vue")),
|
||||
// ...
|
||||
};
|
||||
const currentComponent = computed(() => {
|
||||
if (activeTabKey.value == "MainList") {
|
||||
return tabComponents.MainList;
|
||||
} else if (activeTabKey.value === "RelationEdit") {
|
||||
return tabComponents.RelationEdit;
|
||||
} else if (activeTabKey.value === "RuleEdit") {
|
||||
return tabComponents.RuleEdit;
|
||||
}
|
||||
});
|
||||
// تابع مدیریت تغییر تب
|
||||
const handleTabChange = (tab) => {
|
||||
// منطق تغییر مسیر یا لود داده
|
||||
switch (tab.id) {
|
||||
case "dashboard":
|
||||
break;
|
||||
case "analytics":
|
||||
break;
|
||||
case "users":
|
||||
break;
|
||||
case "settings":
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
const headerSchema = ref({});
|
||||
headerSchema.value = {
|
||||
breadcrumb: true,
|
||||
logo: false,
|
||||
};
|
||||
onMounted(() => {
|
||||
activeTabKey.value = headerTabs.value[0]?.id || "";
|
||||
});
|
||||
</script>
|
||||
69
app/pages/login.vue
Executable file
69
app/pages/login.vue
Executable file
|
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<div
|
||||
class="min-h-[calc(100dvh-4em)] flex items-center justify-center p-4 bg-gray-100 dark:bg-dark-primary"
|
||||
>
|
||||
<div
|
||||
class="max-w-md w-full bg-white dark:bg-dark-primary-800 rounded-2xl shadow-xl p-8 text-center border border-gray-200 dark:border-dark-primary-600"
|
||||
>
|
||||
<!-- Tabs -->
|
||||
<ul class="flex border-b border-gray-200">
|
||||
<li class="flex-1">
|
||||
<button
|
||||
class="w-full py-3.5 font-medium transition-all duration-200 cursor-pointer"
|
||||
:class="tabClass('login')"
|
||||
@click="setActive('login')"
|
||||
>
|
||||
ورود
|
||||
</button>
|
||||
</li>
|
||||
<li class="flex-1">
|
||||
<button
|
||||
class="w-full py-3.5 font-medium transition-all duration-200 cursor-pointer"
|
||||
:class="tabClass('register')"
|
||||
@click="setActive('register')"
|
||||
>
|
||||
ثبتنام
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="mt-8">
|
||||
<component :is="currentComponent" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
definePageMeta({ layout: "login-layout" });
|
||||
|
||||
// --- Lazy load components ---
|
||||
import LoginForm from "~/components/lazy-load/auth/LoginForm.vue";
|
||||
import RegisterForm from "~/components/lazy-load/auth/RegisterForm.vue";
|
||||
|
||||
// --- State ---
|
||||
const activeTab = ref("login");
|
||||
const currentComponent = ref(LoginForm);
|
||||
const componentsMap = {
|
||||
login: LoginForm,
|
||||
register: RegisterForm,
|
||||
};
|
||||
|
||||
// --- Methods ---
|
||||
function setActive(tab) {
|
||||
activeTab.value = tab;
|
||||
currentComponent.value = componentsMap[tab];
|
||||
}
|
||||
|
||||
// --- Compute class for tabs ---
|
||||
function tabClass(tab) {
|
||||
if (activeTab.value === tab) {
|
||||
return "text-primary border-b-1 border-primary font-semibold";
|
||||
} else {
|
||||
return "text-gray-500 hover:text-primary";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style></style>
|
||||
103
app/plugins/httpService.ts
Executable file
103
app/plugins/httpService.ts
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
import { useStorage } from "@vueuse/core";
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
// ======================
|
||||
// Runtime Config (Public)
|
||||
// ======================
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
const BASE_URL = config.public.NUXT_PUBLIC_BASE_URL || "";
|
||||
const API_NAME = config.public.NUXT_PUBLIC_API_NAME || "";
|
||||
|
||||
// ======================
|
||||
// Safe URL Joiner
|
||||
// ======================
|
||||
const joinURL = (base: string, path: string) => {
|
||||
return `${base.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
||||
};
|
||||
|
||||
const baseURL = joinURL(BASE_URL, API_NAME);
|
||||
|
||||
// ======================
|
||||
// Reactive Token
|
||||
// ======================
|
||||
const token = useStorage<string>("id_token", "GuestAccess");
|
||||
|
||||
// ======================
|
||||
// Fetch Instance
|
||||
// ======================
|
||||
const api = $fetch.create({
|
||||
baseURL,
|
||||
|
||||
// ======================
|
||||
// Request Interceptor
|
||||
// ======================
|
||||
onRequest({ options }) {
|
||||
const auth = token.value;
|
||||
if (!auth) return;
|
||||
const headers = (options.headers ||= {});
|
||||
if (headers instanceof Headers) {
|
||||
headers.set("Authorization", auth);
|
||||
} else if (Array.isArray(headers)) {
|
||||
headers.push(["Authorization", auth]);
|
||||
} else {
|
||||
headers.Authorization = auth;
|
||||
}
|
||||
},
|
||||
|
||||
// ======================
|
||||
// Response Handler
|
||||
// ======================
|
||||
onResponse({ response }) {
|
||||
return response._data;
|
||||
},
|
||||
|
||||
// ======================
|
||||
// Error Handler
|
||||
// ======================
|
||||
async onResponseError({ response }) {
|
||||
const status = response?.status;
|
||||
|
||||
if (status === 401) {
|
||||
token.value = null;
|
||||
await nuxtApp.runWithContext(() => navigateTo("/login"));
|
||||
}
|
||||
|
||||
throw {
|
||||
status,
|
||||
message:
|
||||
response?._data?.message || response?.statusText || "خطای نامشخص",
|
||||
data: response?._data || null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// ======================
|
||||
// HTTP Service (Public API)
|
||||
// ======================
|
||||
const http = {
|
||||
getRequest: (url: string, options: any = {}) =>
|
||||
api(url, { method: "GET", ...options }),
|
||||
|
||||
postRequest: (url: string, body: any, options: any = {}) =>
|
||||
api(url, { method: "POST", body, ...options }),
|
||||
|
||||
putRequest: (url: string, body: any, options: any = {}) =>
|
||||
api(url, { method: "PUT", body, ...options }),
|
||||
|
||||
patchRequest: (url: string, body: any, options: any = {}) =>
|
||||
api(url, { method: "PATCH", body, ...options }),
|
||||
|
||||
deleteRequest: (url: string, options: any = {}) =>
|
||||
api(url, { method: "DELETE", ...options }),
|
||||
};
|
||||
|
||||
// ======================
|
||||
// Provide to Nuxt
|
||||
// ======================
|
||||
return {
|
||||
provide: {
|
||||
http,
|
||||
},
|
||||
};
|
||||
});
|
||||
5
app/plugins/system-theme.client.ts
Executable file
5
app/plugins/system-theme.client.ts
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
export default defineNuxtPlugin(async () => {
|
||||
const { applyTheme } = composSystemTheme();
|
||||
|
||||
await applyTheme();
|
||||
});
|
||||
60
app/plugins/themeLoader.client.ts
Executable file
60
app/plugins/themeLoader.client.ts
Executable file
|
|
@ -0,0 +1,60 @@
|
|||
// themeLoader.client.ts
|
||||
import majles from '~/assets/majles/theme.json'
|
||||
import monir from '~/assets/monir/theme.json'
|
||||
|
||||
export const themes = {
|
||||
majles,
|
||||
monir
|
||||
}
|
||||
|
||||
type ThemeKey = keyof typeof themes
|
||||
type ThemeConfig = typeof themes[ThemeKey]
|
||||
|
||||
// تابع برای تبدیل HEX به RGB (بدون پرانتز و کاما)
|
||||
function hexToRgb(hex: string): string {
|
||||
// حذف # از ابتدا
|
||||
hex = hex.replace('#', '');
|
||||
|
||||
// اگر کوتاه باشد (مثل #fff)
|
||||
if (hex.length === 3) {
|
||||
hex = hex.split('').map(char => char + char).join('');
|
||||
}
|
||||
|
||||
// تبدیل به RGB
|
||||
const r = parseInt(hex.substring(0, 2), 16);
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
|
||||
return `${r} ${g} ${b}`;
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
const host = window.location.hostname.replace("www.", "");
|
||||
|
||||
let key: ThemeKey = "monir"; // default
|
||||
|
||||
if (host.includes("monir")) key = "monir";
|
||||
else if (host.includes("majles")) key = "majles";
|
||||
|
||||
const theme: ThemeConfig = themes[key];
|
||||
|
||||
// ست کردن CSS Variables اصلی
|
||||
Object.entries(theme).forEach(([key, value]) => {
|
||||
if (typeof value === "string") {
|
||||
document.documentElement.style.setProperty(`--theme-${key}`, value);
|
||||
|
||||
// تبدیل رنگها به RGB برای Tailwind
|
||||
if (['primary', 'secondary', 'accent', 'background', 'text'].includes(key)) {
|
||||
const rgb = hexToRgb(value);
|
||||
document.documentElement.style.setProperty(`--color-${key}`, rgb);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
provide: {
|
||||
themeKey: key,
|
||||
theme,
|
||||
},
|
||||
};
|
||||
});
|
||||
139
app/server/api/ai/index.post.js
Executable file
139
app/server/api/ai/index.post.js
Executable file
|
|
@ -0,0 +1,139 @@
|
|||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const body = await readBody(event);
|
||||
const { action, text, language = "fa" } = body;
|
||||
|
||||
// بررسی پارامترهای ورودی
|
||||
if (!action || !text) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "پارامترهای action و text الزامی هستند",
|
||||
});
|
||||
}
|
||||
|
||||
// API keyها از .env
|
||||
const config = useRuntimeConfig();
|
||||
const OPENAI_API_KEY = config.OPENAI_API_KEY;
|
||||
const GEMINI_API_KEY = config.GEMINI_API_KEY;
|
||||
|
||||
// انتخاب سرویس AI
|
||||
const useOpenAI = !!OPENAI_API_KEY;
|
||||
const useGemini = !!GEMINI_API_KEY && !useOpenAI;
|
||||
|
||||
let aiResult = "";
|
||||
|
||||
if (useOpenAI) {
|
||||
// OpenAI API
|
||||
const response = await fetch(
|
||||
"https://api.openai.com/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${OPENAI_API_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "gpt-3.5-turbo",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"تو یک دستیار فارسیزبان هستی که به کاربران در نوشتن کمک میکنی.",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: getPrompt(action, text, language),
|
||||
},
|
||||
],
|
||||
temperature: 0.7,
|
||||
max_tokens: 500,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`OpenAI API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
aiResult = data.choices[0]?.message?.content || "خطا در پردازش";
|
||||
} else if (useGemini) {
|
||||
// Google Gemini API
|
||||
const response = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${GEMINI_API_KEY}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
contents: [
|
||||
{
|
||||
parts: [
|
||||
{
|
||||
text: getPrompt(action, text, language),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Gemini API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
aiResult =
|
||||
data.candidates?.[0]?.content?.parts?.[0]?.text || "خطا در پردازش";
|
||||
} else {
|
||||
// پاسخ تستی (اگر API key ندارید)
|
||||
aiResult = getMockResponse(action, text, language);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action,
|
||||
result: aiResult,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("AI API Error:", error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
mockResponse: getMockResponse(action, text, language),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// تولید prompt بر اساس action
|
||||
function getPrompt(action, text, language) {
|
||||
const prompts = {
|
||||
summarize: `لطفاً این متن را به فارسی خلاصه کن و نکات اصلی آن را استخراج کن. متن:\n\n${text}\n\nخلاصه:`,
|
||||
improve: `این متن را از نظر دستوری، نگارشی و سبک نوشتار بهبود بده و آن را حرفهایتر کن. متن:\n\n${text}\n\nمتن بهبود یافته:`,
|
||||
translate: `این متن فارسی را به انگلیسی ترجمه کن. متن:\n\n${text}\n\nترجمه:`,
|
||||
explain: `این متن را به زبان ساده توضیح بده و مفاهیم آن را شفاف سازی کن. متن:\n\n${text}\n\nتوضیح:`,
|
||||
continue: `بر اساس این متن، آن را ادامه بده و محتوای مرتبط اضافه کن. متن:\n\n${text}\n\nادامه متن:`,
|
||||
};
|
||||
|
||||
return prompts[action] || `در مورد این متن نظر بده:\n\n${text}\n\nنظر:`;
|
||||
}
|
||||
|
||||
// پاسخهای تستی
|
||||
function getMockResponse(action, text, language) {
|
||||
const responses = {
|
||||
summarize: `📝 خلاصه متن: این یک خلاصه آزمایشی از متن شماست که نکات کلیدی را پوشش میدهد. متن اصلی حدود ${text.length} کاراکتر داشت و شامل موضوعاتی مانند نمونهگیری و تست است.`,
|
||||
improve: `✨ متن بهبود یافته: این نسخه اصلاح شده متن شماست با رعایت اصول نگارشی و ساختار بهتر. جملات روانتر شده و از واژگان مناسبتری استفاده شده است.`,
|
||||
translate: `🌍 Translation: This is a sample translation of your Persian text into English. The original text was about testing and demonstration purposes.`,
|
||||
explain: `💡 توضیح: این مفهوم به زبان ساده توضیح داده میشود تا درک آن آسانتر شود. منظور از این متن نمایش قابلیتهای سیستم هوش مصنوعی است.`,
|
||||
continue: `↪️ ادامه متن: این بخش ادامه منطقی متن شماست که ایدههای مطرح شده را توسعه میدهد. با توجه به محتوای قبلی، میتوان به موضوعات مرتبط دیگری نیز پرداخت.`,
|
||||
};
|
||||
|
||||
return (
|
||||
responses[action] ||
|
||||
"پردازش AI انجام شد. برای استفاده از قابلیتهای واقعی، لطفاً API key مناسب را تنظیم کنید."
|
||||
);
|
||||
}
|
||||
134
app/stores/authStore.ts
Executable file
134
app/stores/authStore.ts
Executable file
|
|
@ -0,0 +1,134 @@
|
|||
// stores/authStore.ts
|
||||
import { defineStore } from "pinia";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
|
||||
export const useAuthStore = defineStore("authStore", () => {
|
||||
// ===== state =====
|
||||
const user = ref({});
|
||||
const isAuthenticated = ref(false);
|
||||
const isRealUser = ref(false);
|
||||
const errors = ref(null);
|
||||
|
||||
// ===== actions =====
|
||||
function setUser(response: any) {
|
||||
const payload = response?.data ?? {}; // data اصلی از API
|
||||
|
||||
// userData از payload.user_data و user_id ساخته میشه
|
||||
const userData = {
|
||||
id: payload.user_id ?? null,
|
||||
level: payload.user_level ?? null,
|
||||
token: payload.token ?? null,
|
||||
expire: payload.expire ?? null,
|
||||
refresh_token: payload.refresh_token ?? null,
|
||||
...payload.user_data, // first_name, last_name, avatar, username
|
||||
};
|
||||
|
||||
isAuthenticated.value = true;
|
||||
isRealUser.value = true;
|
||||
user.value = userData;
|
||||
errors.value = null;
|
||||
|
||||
// ذخیره توکن در localStorage — امن بنویس (هرگز null ننویس)
|
||||
try {
|
||||
const id_token = useStorage("id_token", "");
|
||||
id_token.value = userData.token ?? "";
|
||||
} catch (e) {
|
||||
console.warn("Could not write id_token to storage:", e);
|
||||
}
|
||||
|
||||
// ذخیره کامل user — مطمئن شو شیء است
|
||||
try {
|
||||
const userStorage = useStorage("user", {});
|
||||
userStorage.value = userData ?? {};
|
||||
} catch (e) {
|
||||
console.warn("Could not write user to storage:", e);
|
||||
}
|
||||
|
||||
// ذخیره user_id جداگانه برای دسترسی سریع
|
||||
try {
|
||||
const userIdStorage = useStorage("user_id", "");
|
||||
userIdStorage.value = String(userData.id ?? "");
|
||||
} catch (e) {
|
||||
console.warn("Could not write user_id to storage:", e);
|
||||
}
|
||||
|
||||
// --- محافظت از سایر storage هایی که ممکنه در برنامه استفاده شده باشند ---
|
||||
// مثال: اگر یه key برای UI مثل 'sidebar' انتظار فیلد collapsed داره،
|
||||
// اطمینان بده که مقدار آن null نیست:
|
||||
try {
|
||||
const maybeSidebar = useStorage("sidebar", { collapsed: false });
|
||||
if (maybeSidebar.value == null) {
|
||||
// اگر در localStorage قبلاً null ذخیره شده بود، مقدار پیشفرض را بازنویسی کن
|
||||
maybeSidebar.value = { collapsed: false };
|
||||
}
|
||||
} catch (e) {
|
||||
// اگر کلیدهای دیگری داری که انتظار فیلد collapsed دارند، مشابهشان را اضافه کن
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function getCaptcha() {
|
||||
try {
|
||||
const nuxtApp = useNuxtApp();
|
||||
const baseUrl = import.meta.env.VITE_AUTH_BASE_URL;
|
||||
const { $http: httpService } = nuxtApp;
|
||||
|
||||
const data: string = await httpService.getRequest("/auth/captcha", {
|
||||
baseURL: baseUrl,
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Captcha Error:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function register(credentials: any) {
|
||||
try {
|
||||
const nuxtApp = useNuxtApp();
|
||||
const baseUrl = import.meta.env.VITE_AUTH_BASE_URL;
|
||||
const { $http: httpService } = nuxtApp;
|
||||
|
||||
const response = await httpService.postRequest(
|
||||
"/auth/register",
|
||||
credentials,
|
||||
{
|
||||
baseURL: baseUrl,
|
||||
},
|
||||
);
|
||||
|
||||
setUser(response.data);
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
errors.value = error?.response?.data?.message || error.message;
|
||||
return error;
|
||||
}
|
||||
}
|
||||
function userReset() {
|
||||
user.value = {};
|
||||
isAuthenticated.value = false;
|
||||
isRealUser.value = false;
|
||||
errors.value = null;
|
||||
|
||||
// پاک کردن localStorage / sessionStorage
|
||||
const id_token = useStorage("id_token", "");
|
||||
id_token.value = "";
|
||||
|
||||
const userStorage = useStorage("user", {});
|
||||
userStorage.value = {};
|
||||
}
|
||||
return {
|
||||
// state
|
||||
user,
|
||||
isAuthenticated,
|
||||
isRealUser,
|
||||
errors,
|
||||
|
||||
// actions
|
||||
setUser,
|
||||
getCaptcha,
|
||||
register,
|
||||
userReset,
|
||||
};
|
||||
});
|
||||
13
app/stores/commonStore.ts
Executable file
13
app/stores/commonStore.ts
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
export const useCommonStore = defineStore("commonStore", () => {
|
||||
let sidebarOpen = ref(false);
|
||||
const isSidebarOpen = () => {
|
||||
sidebarOpen.value = !sidebarOpen.value;
|
||||
};
|
||||
|
||||
return {
|
||||
isSidebarOpen,
|
||||
sidebarOpen,
|
||||
};
|
||||
});
|
||||
255
app/stores/notionStore.js
Executable file
255
app/stores/notionStore.js
Executable file
|
|
@ -0,0 +1,255 @@
|
|||
// stores/notionStore.js
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useNotionStore = defineStore("notion", {
|
||||
state: () => ({
|
||||
blocks: [],
|
||||
selectedBlockId: null,
|
||||
draggedBlockId: null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getBlockById: (state) => (id) => {
|
||||
return state.blocks.find((block) => block.id === id);
|
||||
},
|
||||
|
||||
getChildBlocks: (state) => (parentId) => {
|
||||
return state.blocks.filter((block) => block.parentId === parentId);
|
||||
},
|
||||
|
||||
selectedBlock: (state) => {
|
||||
return state.blocks.find((block) => block.id === state.selectedBlockId);
|
||||
},
|
||||
|
||||
// بلوکهای سطح بالا (بدون والد)
|
||||
topLevelBlocks: (state) => {
|
||||
return state.blocks.filter((block) => !block.parentId);
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
// ایجاد ID یکتا
|
||||
generateId() {
|
||||
return (
|
||||
"block_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9)
|
||||
);
|
||||
},
|
||||
|
||||
// ایجاد بلوک جدید
|
||||
addBlock({
|
||||
type = "paragraph",
|
||||
content = "",
|
||||
parentId = null,
|
||||
position = null,
|
||||
}) {
|
||||
const newBlock = {
|
||||
id: this.generateId(),
|
||||
type,
|
||||
content,
|
||||
parentId,
|
||||
children: [],
|
||||
properties: {},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
collapsed: false,
|
||||
aiProcessing: false,
|
||||
color: "default",
|
||||
};
|
||||
|
||||
// اضافه کردن بلوک در موقعیت مشخص
|
||||
if (position !== null && position >= 0 && position < this.blocks.length) {
|
||||
this.blocks.splice(position, 0, newBlock);
|
||||
} else {
|
||||
this.blocks.push(newBlock);
|
||||
}
|
||||
|
||||
// اگر والد دارد، به لیست فرزندانش اضافه شود
|
||||
if (parentId) {
|
||||
const parent = this.getBlockById(parentId);
|
||||
if (parent) {
|
||||
parent.children.push(newBlock.id);
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedBlockId = newBlock.id;
|
||||
this.saveToLocalStorage();
|
||||
return newBlock;
|
||||
},
|
||||
|
||||
// بهروزرسانی بلوک
|
||||
updateBlock(blockId, updates) {
|
||||
const index = this.blocks.findIndex((block) => block.id === blockId);
|
||||
if (index !== -1) {
|
||||
this.blocks[index] = {
|
||||
...this.blocks[index],
|
||||
...updates,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
this.saveToLocalStorage();
|
||||
}
|
||||
},
|
||||
|
||||
// حذف بلوک
|
||||
deleteBlock(blockId) {
|
||||
const block = this.getBlockById(blockId);
|
||||
|
||||
// حذف فرزندان بلوک
|
||||
if (block && block.children && block.children.length > 0) {
|
||||
block.children.forEach((childId) => this.deleteBlock(childId));
|
||||
}
|
||||
|
||||
// حذف از لیست فرزندان والد
|
||||
if (block && block.parentId) {
|
||||
const parent = this.getBlockById(block.parentId);
|
||||
if (parent) {
|
||||
parent.children = parent.children.filter((id) => id !== blockId);
|
||||
}
|
||||
}
|
||||
|
||||
// حذف بلوک اصلی
|
||||
this.blocks = this.blocks.filter((block) => block.id !== blockId);
|
||||
this.saveToLocalStorage();
|
||||
},
|
||||
|
||||
// انتخاب بلوک
|
||||
selectBlock(blockId) {
|
||||
this.selectedBlockId = blockId;
|
||||
},
|
||||
|
||||
// جابجایی بلوک
|
||||
moveBlock(blockId, newParentId = null, newIndex = null) {
|
||||
const block = this.getBlockById(blockId);
|
||||
if (!block) return;
|
||||
|
||||
// حذف از والد قبلی
|
||||
if (block.parentId) {
|
||||
const oldParent = this.getBlockById(block.parentId);
|
||||
if (oldParent) {
|
||||
oldParent.children = oldParent.children.filter(
|
||||
(id) => id !== blockId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// آپدیت والد جدید
|
||||
block.parentId = newParentId;
|
||||
|
||||
// اضافه به لیست فرزندان والد جدید
|
||||
if (newParentId) {
|
||||
const newParent = this.getBlockById(newParentId);
|
||||
if (newParent) {
|
||||
if (newIndex !== null) {
|
||||
newParent.children.splice(newIndex, 0, blockId);
|
||||
} else {
|
||||
newParent.children.push(blockId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// جابجایی در آرایه اصلی اگر index مشخص شده
|
||||
if (newIndex !== null) {
|
||||
const currentIndex = this.blocks.findIndex((b) => b.id === blockId);
|
||||
if (currentIndex !== -1) {
|
||||
const [block] = this.blocks.splice(currentIndex, 1);
|
||||
this.blocks.splice(newIndex, 0, block);
|
||||
}
|
||||
}
|
||||
|
||||
this.saveToLocalStorage();
|
||||
},
|
||||
|
||||
// ذخیره در localStorage
|
||||
saveToLocalStorage() {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(
|
||||
"notion-blocks",
|
||||
JSON.stringify({
|
||||
blocks: this.blocks,
|
||||
version: "1.0.0",
|
||||
lastSaved: new Date().toISOString(),
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// بارگذاری از localStorage
|
||||
loadFromLocalStorage() {
|
||||
if (typeof window !== "undefined") {
|
||||
const saved = localStorage.getItem("notion-blocks");
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
this.blocks = data.blocks || [];
|
||||
} else {
|
||||
// ایجاد بلوکهای پیشفرض
|
||||
this.initializeDefaultBlocks();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// بلوکهای پیشفرض
|
||||
initializeDefaultBlocks() {
|
||||
this.blocks = [
|
||||
{
|
||||
id: this.generateId(),
|
||||
type: "heading1",
|
||||
content: "خوش آمدید به نوشن فارسی! 🎉",
|
||||
parentId: null,
|
||||
children: [],
|
||||
properties: {},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
collapsed: false,
|
||||
color: "default",
|
||||
},
|
||||
{
|
||||
id: this.generateId(),
|
||||
type: "paragraph",
|
||||
content:
|
||||
"روی هر بلوک کلیک کنید یا علامت + را بزنید. برای منوی بیشتر راست کلیک کنید.",
|
||||
parentId: null,
|
||||
children: [],
|
||||
properties: {},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
collapsed: false,
|
||||
color: "default",
|
||||
},
|
||||
{
|
||||
id: this.generateId(),
|
||||
type: "todo",
|
||||
content: "اولین کار را انجام دهید",
|
||||
parentId: null,
|
||||
children: [],
|
||||
properties: { checked: false },
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
collapsed: false,
|
||||
color: "default",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
// پیدا کردن موقعیت بلوک
|
||||
findBlockPosition(blockId) {
|
||||
return this.blocks.findIndex((block) => block.id === blockId);
|
||||
},
|
||||
|
||||
// پیدا کردن بلوک بعدی
|
||||
findNextBlock(blockId) {
|
||||
const index = this.findBlockPosition(blockId);
|
||||
if (index < this.blocks.length - 1) {
|
||||
return this.blocks[index + 1];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// پیدا کردن بلوک قبلی
|
||||
findPreviousBlock(blockId) {
|
||||
const index = this.findBlockPosition(blockId);
|
||||
if (index > 0) {
|
||||
return this.blocks[index - 1];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
134
app/stores/page.js
Executable file
134
app/stores/page.js
Executable file
|
|
@ -0,0 +1,134 @@
|
|||
// stores/page.js
|
||||
import { defineStore } from "pinia";
|
||||
import { tiptapToBlocks, blocksToTiptap } from "@/utils/tiptapMapper";
|
||||
|
||||
export const usePageStore = defineStore("page", {
|
||||
state: () => ({
|
||||
blocks: [],
|
||||
currentPageId: null,
|
||||
pages: [],
|
||||
_hydrated: false,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getBlocks: (state) => state.blocks,
|
||||
getPageById: (state) => (id) => {
|
||||
return state.pages.find((page) => page.id === id);
|
||||
},
|
||||
isHydrated: (state) => state._hydrated,
|
||||
},
|
||||
|
||||
actions: {
|
||||
updateFromEditor(editor) {
|
||||
this.blocks = tiptapToBlocks(editor.getJSON());
|
||||
this.saveToLocalStorage();
|
||||
},
|
||||
|
||||
setBlocks(blocks) {
|
||||
this.blocks = blocks;
|
||||
},
|
||||
|
||||
addBlock(block) {
|
||||
this.blocks.push({
|
||||
...block,
|
||||
id: Date.now().toString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
this.saveToLocalStorage();
|
||||
},
|
||||
|
||||
removeBlock(blockId) {
|
||||
this.blocks = this.blocks.filter((block) => block.id !== blockId);
|
||||
this.saveToLocalStorage();
|
||||
},
|
||||
|
||||
updateBlock(blockId, updates) {
|
||||
const index = this.blocks.findIndex((block) => block.id === blockId);
|
||||
if (index !== -1) {
|
||||
this.blocks[index] = { ...this.blocks[index], ...updates };
|
||||
this.saveToLocalStorage();
|
||||
}
|
||||
},
|
||||
|
||||
saveToLocalStorage() {
|
||||
// فقط در کلاینت اجرا شود
|
||||
if (typeof window !== "undefined" && window.localStorage) {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
"notion-pages",
|
||||
JSON.stringify({
|
||||
blocks: this.blocks,
|
||||
pages: this.pages,
|
||||
currentPageId: this.currentPageId,
|
||||
lastSaved: new Date().toISOString(),
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn("خطا در ذخیره localStorage:", error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
loadFromLocalStorage() {
|
||||
// فقط در کلاینت اجرا شود
|
||||
if (typeof window !== "undefined" && window.localStorage) {
|
||||
try {
|
||||
const saved = localStorage.getItem("notion-pages");
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
this.blocks = data.blocks || [];
|
||||
this.pages = data.pages || [];
|
||||
this.currentPageId = data.currentPageId || null;
|
||||
this._hydrated = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("خطا در بارگذاری localStorage:", error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
createPage(title) {
|
||||
const newPage = {
|
||||
id: Date.now().toString(),
|
||||
title: title || "صفحه جدید",
|
||||
blocks: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
this.pages.push(newPage);
|
||||
this.saveToLocalStorage();
|
||||
return newPage;
|
||||
},
|
||||
|
||||
switchPage(pageId) {
|
||||
const page = this.getPageById(pageId);
|
||||
if (page) {
|
||||
this.currentPageId = pageId;
|
||||
this.blocks = page.blocks;
|
||||
this.saveToLocalStorage();
|
||||
}
|
||||
},
|
||||
|
||||
saveCurrentPage() {
|
||||
if (this.currentPageId) {
|
||||
const pageIndex = this.pages.findIndex(
|
||||
(p) => p.id === this.currentPageId
|
||||
);
|
||||
if (pageIndex !== -1) {
|
||||
this.pages[pageIndex].blocks = this.blocks;
|
||||
this.pages[pageIndex].updatedAt = new Date().toISOString();
|
||||
this.saveToLocalStorage();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
clearAllData() {
|
||||
this.blocks = [];
|
||||
this.pages = [];
|
||||
this.currentPageId = null;
|
||||
if (typeof window !== "undefined" && window.localStorage) {
|
||||
localStorage.removeItem("notion-pages");
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
89
app/stores/permissionStore.ts
Executable file
89
app/stores/permissionStore.ts
Executable file
|
|
@ -0,0 +1,89 @@
|
|||
// stores/permissionStore.ts
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { useAuthStore } from "./authStore";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
|
||||
export const getUserPermission = defineStore("permissionStore", () => {
|
||||
// State
|
||||
let permissions = ref<any[]>([]);
|
||||
|
||||
const hasPagePermission = (to_fullPath: string) => {
|
||||
// fullPath: "/data-entry/content"
|
||||
// fullPath: "/data-entry/content?page=2"
|
||||
let index = to_fullPath.indexOf("?");
|
||||
if (index != -1) to_fullPath = to_fullPath.substring(0, index);
|
||||
|
||||
let items = to_fullPath.split("/");
|
||||
let page_key = items[1] + "_" + items[2];
|
||||
// console.log('hasPagePermission page_key ', to_fullPath, page_key);
|
||||
|
||||
return permissions.value.some((p) => p.section_tag === page_key);
|
||||
};
|
||||
|
||||
const hasActionPermission = (to_fullPath: string, action_tag: string) => {
|
||||
// fullPath: "/data-entry/content?page=2"
|
||||
let index = to_fullPath.indexOf("?");
|
||||
if (index != -1) to_fullPath = to_fullPath.substring(0, index);
|
||||
|
||||
let items = to_fullPath.split("/");
|
||||
let page_key = items[1] + "_" + items[2];
|
||||
return permissions.value.some(
|
||||
(p) => p.section_tag === page_key && p.action_tag === action_tag
|
||||
);
|
||||
};
|
||||
|
||||
// const hasPermission = (action: string) => {
|
||||
// return permissions.value.some((p) => p.action_tag === action);
|
||||
// };
|
||||
|
||||
// const canAccessPage = (pageTag: string) => {
|
||||
// return permissions.value.some((p) => p.section_tag === pageTag);
|
||||
// };
|
||||
|
||||
// Actions
|
||||
const fetchUserPermissions = async (projectId?: number) => {
|
||||
const { $http: httpService } = useNuxtApp();
|
||||
const authStore = useAuthStore();
|
||||
if (!authStore.user && !localStorage.getItem("user").length) {
|
||||
console.log("No authenticated user found");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = {
|
||||
project_id: 50,
|
||||
project_only: 1,
|
||||
};
|
||||
|
||||
const response = await httpService.postRequest(
|
||||
// permitApi.permissions.userPermissionTags,
|
||||
data
|
||||
);
|
||||
|
||||
permissions.value = response.data || [];
|
||||
|
||||
useStorage("permit", permissions.value);
|
||||
|
||||
// console.log(" fetchUserPermissions ", permissions.value);
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to fetch user permissions:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
permissions.value = [];
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
permissions,
|
||||
|
||||
// Getters
|
||||
hasPagePermission,
|
||||
hasActionPermission,
|
||||
|
||||
// Actions
|
||||
fetchUserPermissions,
|
||||
reset,
|
||||
};
|
||||
});
|
||||
34
app/types/blocks.js
Executable file
34
app/types/blocks.js
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* @typedef {Object} BaseBlock
|
||||
* @property {string} id
|
||||
* @property {string} type
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseBlock & {
|
||||
* type: 'paragraph',
|
||||
* text: string
|
||||
* }} ParagraphBlock
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseBlock & {
|
||||
* type: 'heading',
|
||||
* level: 1|2|3,
|
||||
* text: string
|
||||
* }} HeadingBlock
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseBlock & {
|
||||
* type: 'todo',
|
||||
* text: string,
|
||||
* checked: boolean
|
||||
* }} TodoBlock
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {ParagraphBlock | HeadingBlock | TodoBlock} Block
|
||||
*/
|
||||
|
||||
export {};
|
||||
15
app/types/nuxt.d.ts
vendored
Executable file
15
app/types/nuxt.d.ts
vendored
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
import 'vue-i18n'
|
||||
|
||||
declare module '#app' {
|
||||
interface NuxtApp {
|
||||
$t: typeof import('vue-i18n')['createI18n']['prototype']['t']
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
$t: typeof import('vue-i18n')['createI18n']['prototype']['t']
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
196
app/utils/searchUtil.js
Executable file
196
app/utils/searchUtil.js
Executable file
|
|
@ -0,0 +1,196 @@
|
|||
/* =====================================================
|
||||
Helpers (ملحقات لازم)
|
||||
===================================================== */
|
||||
|
||||
/**
|
||||
* جایگزین globalMixin.myEncodeQuery
|
||||
*/
|
||||
// export function myEncodeQuery(text) {
|
||||
// if (!text) return "";
|
||||
// return encodeURIComponent(text);
|
||||
// }
|
||||
|
||||
/**
|
||||
* پاکسازی کاراکترهای غیرمجاز
|
||||
*/
|
||||
export function cleanTextUnpermittedChars(text) {
|
||||
if (!text) return "";
|
||||
|
||||
// همان منطق قبلی (اصلاحشده برای JS صحیح)
|
||||
text = text.replace(/([\x00-\x1F]|\x7F)/g, "");
|
||||
return text;
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
Main: utilGetSearchRequest
|
||||
===================================================== */
|
||||
|
||||
export function utilGetSearchRequest(
|
||||
index_key,
|
||||
textSearch,
|
||||
searchTypeKey,
|
||||
mode_url = "normal",
|
||||
filterPanel = "",
|
||||
filterExtended = "",
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
field_collapse = "normal",
|
||||
sortKey = "normal",
|
||||
domainKey = "all",
|
||||
domainSchema = {},
|
||||
user_synonyms = {},
|
||||
mirror_type = ""
|
||||
) {
|
||||
let _payload = {};
|
||||
let domain_label = "";
|
||||
let query_string = cleanTextUnpermittedChars(textSearch);
|
||||
|
||||
/* ===== Base URL ===== */
|
||||
let baseUrl = repoApi.search.queryNormal;
|
||||
|
||||
if (mode_url === "elp") {
|
||||
baseUrl = elpApi.search.elp_base_search;
|
||||
baseUrl = baseUrl.replace("{{index_key}}", index_key);
|
||||
}
|
||||
else if ( mode_url === "elp_db") {
|
||||
baseUrl = elpApi.search.elp_db_search;
|
||||
baseUrl = baseUrl.replace("{{index_key}}", index_key);
|
||||
}
|
||||
else if (mode_url === "elp_voice") {
|
||||
baseUrl = elpApi.search.elp_voice_search;
|
||||
baseUrl = baseUrl.replace("{{index_key}}", index_key);
|
||||
} else if (mode_url === "mirror") {
|
||||
baseUrl = repoApi.search.mirror_search;
|
||||
baseUrl = baseUrl.replace("{{mirror_type}}", mirror_type);
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
Synonym / Vector / Normal
|
||||
===================================================== */
|
||||
|
||||
let sysnonymSearchQuery = "";
|
||||
|
||||
if (!searchTypeKey) searchTypeKey = "normal";
|
||||
|
||||
if (searchTypeKey === "synonym") {
|
||||
let newSynonym = {};
|
||||
|
||||
if (user_synonyms) {
|
||||
Object.keys(user_synonyms).forEach((key) => {
|
||||
if (
|
||||
!("isStopWord" in user_synonyms[key] && user_synonyms[key].isStopWord)
|
||||
) {
|
||||
newSynonym[key] = user_synonyms[key].value;
|
||||
sysnonymSearchQuery += " " + key;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (textSearch === "" || textSearch === undefined)
|
||||
textSearch = sysnonymSearchQuery;
|
||||
|
||||
if (Object.keys(newSynonym).length) {
|
||||
_payload = {
|
||||
synonym: newSynonym,
|
||||
};
|
||||
}
|
||||
} else if (searchTypeKey === "vector") {
|
||||
/* ===== Vector ===== */
|
||||
_payload = {
|
||||
text: query_string,
|
||||
};
|
||||
} else if (!(textSearch === "" || textSearch === undefined)) {
|
||||
/* ===== Normal ===== */
|
||||
query_string = query_string;
|
||||
|
||||
if (!domainKey) domainKey = "all";
|
||||
|
||||
if (domainKey === "all") {
|
||||
if (domainSchema?.type_action) {
|
||||
let isNumberInput = /^\d+$/.test(query_string);
|
||||
|
||||
if (isNumberInput && domainSchema.type_action.number) {
|
||||
let tag = domainSchema.type_action.number;
|
||||
query_string = encodeURIComponent("#") + tag + " " + query_string;
|
||||
}
|
||||
}
|
||||
} else if (domainKey !== "advance") {
|
||||
let domainItem = domainSchema?.domain?.find(
|
||||
(item) => item.key === domainKey
|
||||
);
|
||||
|
||||
if (domainItem) {
|
||||
domain_label = domainItem.label;
|
||||
query_string =
|
||||
encodeURIComponent("#") + domainItem.tag + " " + query_string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
Filters
|
||||
===================================================== */
|
||||
|
||||
let filterFull = "";
|
||||
|
||||
if (searchTypeKey !== "vector" && query_string) {
|
||||
filterFull += "q=" + query_string;
|
||||
}
|
||||
|
||||
filterFull += filterPanel + filterExtended;
|
||||
|
||||
if (!filterFull) filterFull = "none";
|
||||
|
||||
/* =====================================================
|
||||
URL Replace
|
||||
===================================================== */
|
||||
|
||||
let url = baseUrl;
|
||||
const buildName = import.meta.env.VITE_BUILD_NAME;
|
||||
|
||||
url = url.replace("{{appname}}", buildName);
|
||||
url = url.replace("{{index_key}}", index_key);
|
||||
url = url.replace("{{search_type}}", searchTypeKey);
|
||||
url = url.replace("{{sortKey}}", sortKey);
|
||||
url = url.replace("{{field_collapse}}", field_collapse);
|
||||
url = url.replace("{{offset}}", offset);
|
||||
url = url.replace("{{limit}}", limit);
|
||||
url = url.replace("{{filter}}", filterFull);
|
||||
|
||||
/* =====================================================
|
||||
Payload
|
||||
===================================================== */
|
||||
|
||||
let filters = (filterPanel + filterExtended)
|
||||
.split("&")
|
||||
.filter((e) => e.length);
|
||||
|
||||
let filters_obj = {};
|
||||
filters.forEach((fl) => {
|
||||
let fl_items = fl.split("=");
|
||||
if (fl_items.length === 2) filters_obj[fl_items[0]] = fl_items[1];
|
||||
});
|
||||
|
||||
let payload_full = {
|
||||
track_total_hits: true,
|
||||
query: query_string,
|
||||
search_type: searchTypeKey,
|
||||
filters: filters_obj,
|
||||
sort: [sortKey],
|
||||
from_: offset,
|
||||
size: limit,
|
||||
collapse_field: field_collapse,
|
||||
};
|
||||
|
||||
if (_payload.synonym) {
|
||||
payload_full.user_synonyms = _payload.synonym;
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
payload_full,
|
||||
payload: _payload,
|
||||
sysnonymSearchQuery,
|
||||
domainLabel: domain_label,
|
||||
};
|
||||
}
|
||||
141
app/utils/slashExtension.js
Executable file
141
app/utils/slashExtension.js
Executable file
|
|
@ -0,0 +1,141 @@
|
|||
import { Extension } from "@tiptap/core";
|
||||
import Suggestion from "@tiptap/suggestion";
|
||||
import { ref, h } from "vue";
|
||||
|
||||
export const SlashCommand = Extension.create({
|
||||
name: "slash-command",
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
suggestion: {
|
||||
char: "/",
|
||||
startOfLine: true,
|
||||
command: ({ editor, range, props }) => {
|
||||
// props.type => 'paragraph' | 'heading' | 'todo'
|
||||
if (props.type === "paragraph") {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.insertContent({
|
||||
type: "paragraph",
|
||||
content: [{ type: "text", text: "" }],
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
if (props.type === "heading") {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setNode("heading", { level: 2 })
|
||||
.run();
|
||||
}
|
||||
|
||||
if (props.type === "todo") {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.insertContent({
|
||||
type: "paragraph",
|
||||
content: [{ type: "text", text: "☑️ Todo " }],
|
||||
})
|
||||
.run();
|
||||
}
|
||||
},
|
||||
items: ({ query }) => {
|
||||
const all = [
|
||||
{ title: "Text", type: "paragraph" },
|
||||
{ title: "Heading", type: "heading" },
|
||||
{ title: "Todo", type: "todo" },
|
||||
];
|
||||
return all.filter((item) =>
|
||||
item.title.toLowerCase().startsWith(query.toLowerCase())
|
||||
);
|
||||
},
|
||||
render: () => {
|
||||
let component;
|
||||
let popup;
|
||||
|
||||
return {
|
||||
onStart: (props) => {
|
||||
component = ref({
|
||||
items: props.items,
|
||||
command: props.command,
|
||||
selected: 0,
|
||||
});
|
||||
popup = document.createElement("div");
|
||||
popup.style.position = "absolute";
|
||||
popup.style.background = "white";
|
||||
popup.style.border = "1px solid #ddd";
|
||||
popup.style.borderRadius = "6px";
|
||||
popup.style.padding = "4px 0";
|
||||
popup.style.zIndex = 100;
|
||||
document.body.appendChild(popup);
|
||||
update();
|
||||
function update() {
|
||||
popup.innerHTML = "";
|
||||
component.value.items.forEach((item, i) => {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = item.title;
|
||||
div.style.padding = "4px 12px";
|
||||
div.style.cursor = "pointer";
|
||||
div.style.background =
|
||||
i === component.value.selected ? "#f0f0f0" : "white";
|
||||
div.onclick = () =>
|
||||
component.value.command({
|
||||
editor: props.editor,
|
||||
range: props.range,
|
||||
props: item,
|
||||
});
|
||||
popup.appendChild(div);
|
||||
});
|
||||
}
|
||||
component.value.update = update;
|
||||
},
|
||||
onUpdate: (props) => {
|
||||
component.value.items = props.items;
|
||||
component.value.update();
|
||||
},
|
||||
onKeyDown: (props) => {
|
||||
const event = props.event;
|
||||
if (event.key === "ArrowDown") {
|
||||
component.value.selected =
|
||||
(component.value.selected + 1) % component.value.items.length;
|
||||
component.value.update();
|
||||
return true;
|
||||
}
|
||||
if (event.key === "ArrowUp") {
|
||||
component.value.selected =
|
||||
(component.value.selected -
|
||||
1 +
|
||||
component.value.items.length) %
|
||||
component.value.items.length;
|
||||
component.value.update();
|
||||
return true;
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
component.value.command({
|
||||
editor: props.editor,
|
||||
range: props.range,
|
||||
props: component.value.items[component.value.selected],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onExit: () => {
|
||||
popup.remove();
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [Suggestion(this.options.suggestion)];
|
||||
},
|
||||
});
|
||||
363
app/utils/tiptapMapper.js
Executable file
363
app/utils/tiptapMapper.js
Executable file
|
|
@ -0,0 +1,363 @@
|
|||
// utils/tiptapMapper.js
|
||||
|
||||
export function tiptapToBlocks(tiptapJSON) {
|
||||
const blocks = [];
|
||||
|
||||
function processNode(node, parentId = null) {
|
||||
if (!node) return null;
|
||||
|
||||
const blockId = generateId();
|
||||
|
||||
switch (node.type) {
|
||||
case "doc":
|
||||
if (node.content) {
|
||||
node.content.forEach((child) => processNode(child, null));
|
||||
}
|
||||
return null;
|
||||
|
||||
case "paragraph":
|
||||
const paragraphBlock = {
|
||||
id: blockId,
|
||||
type: "paragraph",
|
||||
content: extractTextContent(node),
|
||||
parentId: parentId,
|
||||
createdAt: new Date().toISOString(),
|
||||
children: [],
|
||||
};
|
||||
|
||||
blocks.push(paragraphBlock);
|
||||
|
||||
// پردازش فرزندان
|
||||
if (node.content) {
|
||||
node.content.forEach((child) => {
|
||||
if (child.type !== "text") {
|
||||
const childBlock = processNode(child, blockId);
|
||||
if (childBlock) {
|
||||
paragraphBlock.children.push(childBlock.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return paragraphBlock;
|
||||
|
||||
case "heading":
|
||||
const headingBlock = {
|
||||
id: blockId,
|
||||
type: "heading",
|
||||
level: node.attrs?.level || 1,
|
||||
content: extractTextContent(node),
|
||||
parentId: parentId,
|
||||
createdAt: new Date().toISOString(),
|
||||
children: [],
|
||||
};
|
||||
|
||||
blocks.push(headingBlock);
|
||||
|
||||
// پردازش فرمتهای درون عنوان
|
||||
if (node.content) {
|
||||
node.content.forEach((child) => {
|
||||
if (child.type !== "text") {
|
||||
const childBlock = processNode(child, blockId);
|
||||
if (childBlock) {
|
||||
headingBlock.children.push(childBlock.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return headingBlock;
|
||||
|
||||
case "bulletList":
|
||||
case "orderedList":
|
||||
const isOrdered = node.type === "orderedList";
|
||||
const listBlock = {
|
||||
id: blockId,
|
||||
type: isOrdered ? "numbered_list" : "bulleted_list",
|
||||
content: "",
|
||||
parentId: parentId,
|
||||
createdAt: new Date().toISOString(),
|
||||
children: [],
|
||||
};
|
||||
|
||||
blocks.push(listBlock);
|
||||
|
||||
// پردازش آیتمهای لیست
|
||||
if (node.content) {
|
||||
node.content.forEach((listItem, index) => {
|
||||
if (listItem.type === "listItem") {
|
||||
const itemContent = extractTextContent(listItem);
|
||||
const itemBlock = {
|
||||
id: generateId(),
|
||||
type: "list_item",
|
||||
content: itemContent,
|
||||
parentId: blockId,
|
||||
number: isOrdered ? index + 1 : null,
|
||||
createdAt: new Date().toISOString(),
|
||||
children: [],
|
||||
};
|
||||
|
||||
blocks.push(itemBlock);
|
||||
listBlock.children.push(itemBlock.id);
|
||||
|
||||
// پردازش محتوای درون آیتم لیست
|
||||
if (listItem.content) {
|
||||
listItem.content.forEach((child) => {
|
||||
if (child.type !== "paragraph" && child.type !== "text") {
|
||||
const childBlock = processNode(child, itemBlock.id);
|
||||
if (childBlock) {
|
||||
itemBlock.children.push(childBlock.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return listBlock;
|
||||
|
||||
case "codeBlock":
|
||||
const codeBlock = {
|
||||
id: blockId,
|
||||
type: "code",
|
||||
language: node.attrs?.language || "javascript",
|
||||
content: extractTextContent(node),
|
||||
parentId: parentId,
|
||||
createdAt: new Date().toISOString(),
|
||||
children: [],
|
||||
};
|
||||
|
||||
blocks.push(codeBlock);
|
||||
return codeBlock;
|
||||
|
||||
case "blockquote":
|
||||
const quoteBlock = {
|
||||
id: blockId,
|
||||
type: "quote",
|
||||
content: extractTextContent(node),
|
||||
parentId: parentId,
|
||||
createdAt: new Date().toISOString(),
|
||||
children: [],
|
||||
};
|
||||
|
||||
blocks.push(quoteBlock);
|
||||
return quoteBlock;
|
||||
|
||||
case "text":
|
||||
// متن ساده را به عنوان یک بلوک متنی برمیگردانیم
|
||||
const textBlock = {
|
||||
id: blockId,
|
||||
type: "text",
|
||||
content: node.text || "",
|
||||
marks: node.marks || [],
|
||||
parentId: parentId,
|
||||
createdAt: new Date().toISOString(),
|
||||
children: [],
|
||||
};
|
||||
|
||||
// فقط اگر درون یک بلوک دیگر نباشد، آن را به عنوان بلوک مستقل اضافه میکنیم
|
||||
if (!parentId) {
|
||||
blocks.push(textBlock);
|
||||
}
|
||||
|
||||
return textBlock;
|
||||
|
||||
case "hardBreak":
|
||||
const breakBlock = {
|
||||
id: blockId,
|
||||
type: "break",
|
||||
content: "\n",
|
||||
parentId: parentId,
|
||||
createdAt: new Date().toISOString(),
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (!parentId) {
|
||||
blocks.push(breakBlock);
|
||||
}
|
||||
|
||||
return breakBlock;
|
||||
|
||||
default:
|
||||
// برای انواع ناشناخته
|
||||
if (node.content) {
|
||||
const unknownBlock = {
|
||||
id: blockId,
|
||||
type: "unknown",
|
||||
content: extractTextContent(node),
|
||||
originalType: node.type,
|
||||
parentId: parentId,
|
||||
createdAt: new Date().toISOString(),
|
||||
children: [],
|
||||
};
|
||||
|
||||
blocks.push(unknownBlock);
|
||||
|
||||
// بازگشتی پردازش فرزندان
|
||||
node.content.forEach((child) => {
|
||||
if (child.type !== "text") {
|
||||
const childBlock = processNode(child, blockId);
|
||||
if (childBlock) {
|
||||
unknownBlock.children.push(childBlock.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return unknownBlock;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function extractTextContent(node) {
|
||||
if (!node.content) return node.text || "";
|
||||
|
||||
let text = "";
|
||||
node.content.forEach((child) => {
|
||||
if (child.type === "text") {
|
||||
text += child.text;
|
||||
} else if (child.content || child.text) {
|
||||
text += extractTextContent(child);
|
||||
}
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
function generateId() {
|
||||
return (
|
||||
"block_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9)
|
||||
);
|
||||
}
|
||||
|
||||
// شروع پردازش
|
||||
if (tiptapJSON && tiptapJSON.content) {
|
||||
tiptapJSON.content.forEach((child) => processNode(child, null));
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// تابع معکوس برای تست (اختیاری)
|
||||
export function blocksToTiptap(blocks) {
|
||||
const content = [];
|
||||
const blockMap = new Map();
|
||||
|
||||
// ایجاد نقشه برای دسترسی سریع
|
||||
blocks.forEach((block) => {
|
||||
blockMap.set(block.id, block);
|
||||
});
|
||||
|
||||
// تابع بازگشتی برای ایجاد گرهها
|
||||
function createNode(block) {
|
||||
switch (block.type) {
|
||||
case "paragraph":
|
||||
return {
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: block.content || "",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
case "heading":
|
||||
return {
|
||||
type: "heading",
|
||||
attrs: { level: block.level || 1 },
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: block.content || "",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
case "bulleted_list":
|
||||
case "numbered_list":
|
||||
const listItems = [];
|
||||
if (block.children && block.children.length > 0) {
|
||||
block.children.forEach((childId) => {
|
||||
const childBlock = blockMap.get(childId);
|
||||
if (childBlock && childBlock.type === "list_item") {
|
||||
listItems.push({
|
||||
type: "listItem",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: childBlock.content || "",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: block.type === "numbered_list" ? "orderedList" : "bulletList",
|
||||
content: listItems,
|
||||
};
|
||||
|
||||
case "code":
|
||||
return {
|
||||
type: "codeBlock",
|
||||
attrs: { language: block.language || "javascript" },
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: block.content || "",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
case "quote":
|
||||
return {
|
||||
type: "blockquote",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: block.content || "",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: block.content || "",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// فقط بلوکهایی که والد ندارند (بلوکهای سطح بالا)
|
||||
const topLevelBlocks = blocks.filter((block) => !block.parentId);
|
||||
|
||||
topLevelBlocks.forEach((block) => {
|
||||
const node = createNode(block);
|
||||
if (node) {
|
||||
content.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
type: "doc",
|
||||
content: content,
|
||||
};
|
||||
}
|
||||
78
nuxt.config.ts
Executable file
78
nuxt.config.ts
Executable file
|
|
@ -0,0 +1,78 @@
|
|||
// nuxt.config.ts
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: "2025-07-15",
|
||||
devtools: { enabled: true },
|
||||
modules: ["@nuxt/ui", "@pinia/nuxt", "@nuxt/icon"],
|
||||
css: ["~/assets/css/main.css"],
|
||||
imports: {
|
||||
dirs: ["stores", "composables", "apis"],
|
||||
},
|
||||
|
||||
icon: {
|
||||
serverBundle: {
|
||||
collections: ["lucide", "system-uicons", "meteor-icons"],
|
||||
},
|
||||
},
|
||||
|
||||
ui: {
|
||||
icons: {
|
||||
collections: ["lucide", "system-uicons", "meteor-icons"],
|
||||
},
|
||||
},
|
||||
components: [
|
||||
{
|
||||
path: "~/components/auto-import",
|
||||
extensions: ["vue"],
|
||||
pathPrefix: false,
|
||||
},
|
||||
],
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
...import.meta.env,
|
||||
system: import.meta.env.NUXT_PUBLIC_SYSTEM,
|
||||
},
|
||||
},
|
||||
alias: {
|
||||
// مسیرهای اصلی با @
|
||||
"@": fileURLToPath(new URL("./app", import.meta.url)), // srcDir
|
||||
"@@": fileURLToPath(new URL(".", import.meta.url)), // rootDir
|
||||
|
||||
// پوشههای داخل app/
|
||||
"@/components": fileURLToPath(new URL("./app/components", import.meta.url)),
|
||||
"@/composables": fileURLToPath(
|
||||
new URL("./app/composables", import.meta.url)
|
||||
),
|
||||
"@/layouts": fileURLToPath(new URL("./app/layouts", import.meta.url)),
|
||||
"@/pages": fileURLToPath(new URL("./app/pages", import.meta.url)),
|
||||
"@/types": fileURLToPath(new URL("./app/types", import.meta.url)),
|
||||
"@/plugins": fileURLToPath(new URL("./app/plugins", import.meta.url)),
|
||||
"@/middleware": fileURLToPath(new URL("./app/middleware", import.meta.url)),
|
||||
"@/modules": fileURLToPath(new URL("./app/modules", import.meta.url)),
|
||||
"@/json": fileURLToPath(new URL("./app/json", import.meta.url)),
|
||||
"@/manuals": fileURLToPath(new URL("./app/manuals", import.meta.url)),
|
||||
"@/stores": fileURLToPath(new URL("./app/stores", import.meta.url)),
|
||||
|
||||
// assets
|
||||
"@/assets": fileURLToPath(new URL("./app/assets", import.meta.url)),
|
||||
"@/images": fileURLToPath(new URL("./app/assets/images", import.meta.url)),
|
||||
"@/styles": fileURLToPath(new URL("./app/assets/styles", import.meta.url)), // یا style
|
||||
"@/fonts": fileURLToPath(new URL("./app/assets/fonts", import.meta.url)),
|
||||
|
||||
// public (در rootDir)
|
||||
"@/public": fileURLToPath(new URL("./public", import.meta.url)),
|
||||
|
||||
// .nuxt (داخل rootDir)
|
||||
"@/build": fileURLToPath(new URL("./.nuxt", import.meta.url)),
|
||||
"@/internal/nuxt/paths": fileURLToPath(
|
||||
new URL("./.nuxt/paths.mjs", import.meta.url)
|
||||
),
|
||||
|
||||
// shared (اگر وجود داشته باشد — در rootDir)
|
||||
"@/shared": fileURLToPath(new URL("./shared", import.meta.url)),
|
||||
},
|
||||
fonts: {
|
||||
providers: false,
|
||||
},
|
||||
});
|
||||
16456
package-lock.json
generated
Executable file
16456
package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load Diff
40
package.json
Executable file
40
package.json
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "hamfahmi-front2",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev-tavasi": "env-cmd -f .env.tavasi nuxt dev --host --port 3007 --inspect",
|
||||
"dev": "nuxt dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/meteor-icons": "^1.2.1",
|
||||
"@nuxt/icon": "^2.2.1",
|
||||
"@nuxt/ui": "^4.3.0",
|
||||
"@nuxtjs/i18n": "^10.2.1",
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"@tiptap/extension-color": "^3.19.0",
|
||||
"@tiptap/extension-drag-handle": "^3.19.0",
|
||||
"@tiptap/extension-horizontal-rule": "^3.19.0",
|
||||
"@tiptap/extension-link": "^3.19.0",
|
||||
"@tiptap/extension-placeholder": "^3.19.0",
|
||||
"@tiptap/extension-task-item": "^3.19.0",
|
||||
"@tiptap/extension-task-list": "^3.19.0",
|
||||
"@tiptap/extension-text-align": "^3.19.0",
|
||||
"@tiptap/extension-text-style": "^3.19.0",
|
||||
"@tiptap/starter-kit": "^3.18.0",
|
||||
"@tiptap/vue-3": "^3.18.0",
|
||||
"@vueuse/integrations": "^14.1.0",
|
||||
"lodash-es": "^4.17.22",
|
||||
"nuxt": "^4.2.1",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.25",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/lucide": "^1.2.86",
|
||||
"env-cmd": "^11.0.0",
|
||||
"sass": "^1.95.0",
|
||||
"vue-draggable-next": "^2.3.0"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Executable file
BIN
public/favicon.ico
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/fonts/sahel/Sahel-Black.eot
Executable file
BIN
public/fonts/sahel/Sahel-Black.eot
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-Black.ttf
Executable file
BIN
public/fonts/sahel/Sahel-Black.ttf
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-Black.woff
Executable file
BIN
public/fonts/sahel/Sahel-Black.woff
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-Black.woff2
Executable file
BIN
public/fonts/sahel/Sahel-Black.woff2
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-Bold.eot
Executable file
BIN
public/fonts/sahel/Sahel-Bold.eot
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-Bold.ttf
Executable file
BIN
public/fonts/sahel/Sahel-Bold.ttf
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-Bold.woff
Executable file
BIN
public/fonts/sahel/Sahel-Bold.woff
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-Bold.woff2
Executable file
BIN
public/fonts/sahel/Sahel-Bold.woff2
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-Light.eot
Executable file
BIN
public/fonts/sahel/Sahel-Light.eot
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-Light.ttf
Executable file
BIN
public/fonts/sahel/Sahel-Light.ttf
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-Light.woff
Executable file
BIN
public/fonts/sahel/Sahel-Light.woff
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-Light.woff2
Executable file
BIN
public/fonts/sahel/Sahel-Light.woff2
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-SemiBold.eot
Executable file
BIN
public/fonts/sahel/Sahel-SemiBold.eot
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-SemiBold.ttf
Executable file
BIN
public/fonts/sahel/Sahel-SemiBold.ttf
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-SemiBold.woff
Executable file
BIN
public/fonts/sahel/Sahel-SemiBold.woff
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-SemiBold.woff2
Executable file
BIN
public/fonts/sahel/Sahel-SemiBold.woff2
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-VF.ttf
Executable file
BIN
public/fonts/sahel/Sahel-VF.ttf
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel-VF.woff2
Executable file
BIN
public/fonts/sahel/Sahel-VF.woff2
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel.eot
Executable file
BIN
public/fonts/sahel/Sahel.eot
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel.ttf
Executable file
BIN
public/fonts/sahel/Sahel.ttf
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel.woff
Executable file
BIN
public/fonts/sahel/Sahel.woff
Executable file
Binary file not shown.
BIN
public/fonts/sahel/Sahel.woff2
Executable file
BIN
public/fonts/sahel/Sahel.woff2
Executable file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-Black.woff2
Executable file
BIN
public/fonts/vazir/Vazirmatn-Black.woff2
Executable file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-Bold.woff2
Executable file
BIN
public/fonts/vazir/Vazirmatn-Bold.woff2
Executable file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-ExtraBold.woff2
Executable file
BIN
public/fonts/vazir/Vazirmatn-ExtraBold.woff2
Executable file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-ExtraLight.woff2
Executable file
BIN
public/fonts/vazir/Vazirmatn-ExtraLight.woff2
Executable file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-Light.woff2
Executable file
BIN
public/fonts/vazir/Vazirmatn-Light.woff2
Executable file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-Medium.woff2
Executable file
BIN
public/fonts/vazir/Vazirmatn-Medium.woff2
Executable file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-Regular.woff2
Executable file
BIN
public/fonts/vazir/Vazirmatn-Regular.woff2
Executable file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-SemiBold.woff2
Executable file
BIN
public/fonts/vazir/Vazirmatn-SemiBold.woff2
Executable file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-Thin.woff2
Executable file
BIN
public/fonts/vazir/Vazirmatn-Thin.woff2
Executable file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn[wght].woff2
Executable file
BIN
public/fonts/vazir/Vazirmatn[wght].woff2
Executable file
Binary file not shown.
BIN
public/logo/majles/dark_logo.png
Executable file
BIN
public/logo/majles/dark_logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/logo/majles/light_logo.png
Executable file
BIN
public/logo/majles/light_logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
public/logo/monir/logo.png
Executable file
BIN
public/logo/monir/logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user