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