Add files via upload
This commit is contained in:
@@ -1 +1,29 @@
|
||||
# Dlearn_panel
|
||||
# vue-project
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
Vendored
+69
@@ -0,0 +1,69 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Avatar: typeof import('primevue/avatar')['default']
|
||||
Button: typeof import('primevue/button')['default']
|
||||
Card: typeof import('primevue/card')['default']
|
||||
CategoryForm: typeof import('./src/components/CategoryForm.vue')['default']
|
||||
Checkbox: typeof import('primevue/checkbox')['default']
|
||||
Column: typeof import('primevue/column')['default']
|
||||
copy: typeof import('./src/components/ProductDetails copy.vue')['default']
|
||||
CouponDetails: typeof import('./src/components/CouponDetails.vue')['default']
|
||||
DataTable: typeof import('primevue/datatable')['default']
|
||||
Divider: typeof import('primevue/divider')['default']
|
||||
Dropdown: typeof import('primevue/dropdown')['default']
|
||||
Editor: typeof import('primevue/editor')['default']
|
||||
FileUpload: typeof import('primevue/fileupload')['default']
|
||||
FirstStep: typeof import('./src/components/ProductFormSteps/FirstStep.vue')['default']
|
||||
FloatLabel: typeof import('primevue/floatlabel')['default']
|
||||
Galleria: typeof import('primevue/galleria')['default']
|
||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
||||
IconCommunity: typeof import('./src/components/icons/IconCommunity.vue')['default']
|
||||
IconDocumentation: typeof import('./src/components/icons/IconDocumentation.vue')['default']
|
||||
IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default']
|
||||
IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default']
|
||||
IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default']
|
||||
Image: typeof import('primevue/image')['default']
|
||||
InputGroup: typeof import('primevue/inputgroup')['default']
|
||||
InputGroupAddon: typeof import('primevue/inputgroupaddon')['default']
|
||||
InputNumber: typeof import('primevue/inputnumber')['default']
|
||||
InputSwitch: typeof import('primevue/inputswitch')['default']
|
||||
InputText: typeof import('primevue/inputtext')['default']
|
||||
Listbox: typeof import('primevue/listbox')['default']
|
||||
Menu: typeof import('primevue/menu')['default']
|
||||
OverlayPanel: typeof import('primevue/overlaypanel')['default']
|
||||
Pagination: typeof import('./src/components/Pagination.vue')['default']
|
||||
Paginator: typeof import('primevue/paginator')['default']
|
||||
Panel: typeof import('primevue/panel')['default']
|
||||
PanelMenu: typeof import('primevue/panelmenu')['default']
|
||||
Password: typeof import('primevue/password')['default']
|
||||
ProductDetails: typeof import('./src/components/ProductDetails.vue')['default']
|
||||
ProductForm: typeof import('./src/components/ProductForm.vue')['default']
|
||||
Rating: typeof import('primevue/rating')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SecondStep: typeof import('./src/components/ProductFormSteps/SecondStep.vue')['default']
|
||||
SelectButton: typeof import('primevue/selectbutton')['default']
|
||||
Sidebar: typeof import('primevue/sidebar')['default']
|
||||
Slider: typeof import('primevue/slider')['default']
|
||||
Stepper: typeof import('primevue/stepper')['default']
|
||||
StepperPanel: typeof import('primevue/stepperpanel')['default']
|
||||
TabPanel: typeof import('primevue/tabpanel')['default']
|
||||
Tag: typeof import('primevue/tag')['default']
|
||||
Textarea: typeof import('primevue/textarea')['default']
|
||||
TheWelcome: typeof import('./src/components/TheWelcome.vue')['default']
|
||||
ThirdStep: typeof import('./src/components/ProductFormSteps/ThirdStep.vue')['default']
|
||||
ToggleButton: typeof import('primevue/togglebutton')['default']
|
||||
Toolbar: typeof import('primevue/toolbar')['default']
|
||||
TreeSelect: typeof import('primevue/treeselect')['default']
|
||||
TreeTable: typeof import('primevue/treetable')['default']
|
||||
UserDetails: typeof import('./src/components/UserDetails.vue')['default']
|
||||
WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default']
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>پنل مدیریت</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Generated
+4565
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "vue-project",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.8",
|
||||
"pinia": "^2.1.7",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^3.51.0",
|
||||
"quill": "^1.3.7",
|
||||
"unplugin-vue-components": "^0.27.0",
|
||||
"vue": "^3.4.21",
|
||||
"vue-i18n": "^9.8.0",
|
||||
"vue-persian-datetime-picker": "^2.10.4",
|
||||
"vue-router": "^4.3.0",
|
||||
"vue3-persian-datetime-picker": "^1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"vite": "^5.2.8",
|
||||
"vite-plugin-vue-devtools": "^7.2.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
+45
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
|
||||
<RouterView />
|
||||
<DynamicDialog />
|
||||
<ConfirmDialog />
|
||||
<Toast position="bottom-right" />
|
||||
<Menu ref="menuRef" :model="menu.items" popup />
|
||||
<OverlayPanel ref="popup">
|
||||
<component :is="popup?.is" />
|
||||
</OverlayPanel>
|
||||
<Sidebar
|
||||
v-model:visible="sidebar.visible"
|
||||
position="right"
|
||||
class="ml-auto"
|
||||
:showCloseIcon="false"
|
||||
>
|
||||
<component :is="sidebar.is" v-bind="sidebar.props" />
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import DynamicDialog from "primevue/dynamicdialog";
|
||||
import ConfirmDialog from "primevue/confirmdialog";
|
||||
import Toast from "primevue/toast";
|
||||
import { provide, reactive, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { useDialog } from "primevue/usedialog";
|
||||
import { useConfirm } from "primevue/useconfirm";
|
||||
|
||||
const { t } = useI18n();
|
||||
const popup = ref();
|
||||
const menuRef = ref();
|
||||
const menu = reactive({ ref: menuRef, items: [] });
|
||||
const toast = useToast();
|
||||
const dialog = useDialog();
|
||||
const sidebar = reactive({});
|
||||
const confirm = useConfirm();
|
||||
|
||||
const service = { confirm, popup, menu, toast, dialog, sidebar, t };
|
||||
|
||||
provide("service", service);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,38 @@
|
||||
@layer tailwind-base, primevue, tailwind-utilities;
|
||||
|
||||
@layer tailwind-base {
|
||||
@tailwind base;
|
||||
}
|
||||
|
||||
@layer tailwind-utilities {
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.ltr {
|
||||
direction: ltr;
|
||||
}
|
||||
.rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-neutral-50 flex flex-col gap-8 min-h-screen font-['vazir'];
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
@apply w-2 bg-gray-50;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
@apply bg-gray-200 rounded-full bg-opacity-0;
|
||||
}
|
||||
|
||||
*:hover::-webkit-scrollbar-thumb {
|
||||
@apply bg-opacity-100;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,71 @@
|
||||
@font-face {
|
||||
font-family: 'vazir';
|
||||
font-style: thin;
|
||||
font-weight: 100;
|
||||
src: url('@/assets/fonts/vazir/Vazir-Thin-FD.eot');
|
||||
src:
|
||||
url('@/assets/fonts/vazir/Vazir-Thin-FD.eot') format('embedded-opentype'),
|
||||
url('@/assets/fonts/vazir/Vazir-Thin-FD.ttf') format('truetype'),
|
||||
url('@/assets/fonts/vazir/Vazir-Thin-FD.woff') format('woff'),
|
||||
url('@/assets/fonts/vazir/Vazir-Thin-FD.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'vazir';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url('@/assets/fonts/vazir/Vazir-Light-FD.eot');
|
||||
src:
|
||||
url('@/assets/fonts/vazir/Vazir-Light-FD.eot') format('embedded-opentype'),
|
||||
url('@/assets/fonts/vazir/Vazir-Light-FD.ttf') format('truetype'),
|
||||
url('@/assets/fonts/vazir/Vazir-Light-FD.woff') format('woff'),
|
||||
url('@/assets/fonts/vazir/Vazir-Light-FD.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'vazir';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('@/assets/fonts/vazir/Vazir-Regular-FD.eot');
|
||||
src:
|
||||
url('@/assets/fonts/vazir/Vazir-Regular-FD.eot') format('embedded-opentype'),
|
||||
url('@/assets/fonts/vazir/Vazir-Regular-FD.ttf') format('truetype'),
|
||||
url('@/assets/fonts/vazir/Vazir-Regular-FD.woff') format('woff'),
|
||||
url('@/assets/fonts/vazir/Vazir-Regular-FD.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'vazir';
|
||||
font-style: medium;
|
||||
font-weight: 500;
|
||||
src: url('@/assets/fonts/vazir/Vazir-Medium-FD.eot');
|
||||
src:
|
||||
url('@/assets/fonts/vazir/Vazir-Medium-FD.eot') format('embedded-opentype'),
|
||||
url('@/assets/fonts/vazir/Vazir-Medium-FD.ttf') format('truetype'),
|
||||
url('@/assets/fonts/vazir/Vazir-Medium-FD.woff') format('woff'),
|
||||
url('@/assets/fonts/vazir/Vazir-Medium-FD.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'vazir';
|
||||
font-style: bold;
|
||||
font-weight: 700;
|
||||
src: url('@/assets/fonts/vazir/Vazir-Bold-FD.eot');
|
||||
src:
|
||||
url('@/assets/fonts/vazir/Vazir-Bold-FD.eot') format('embedded-opentype'),
|
||||
url('@/assets/fonts/vazir/Vazir-Bold-FD.ttf') format('truetype'),
|
||||
url('@/assets/fonts/vazir/Vazir-Bold-FD.woff') format('woff'),
|
||||
url('@/assets/fonts/vazir/Vazir-Bold-FD.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'vazir';
|
||||
font-style: black;
|
||||
font-weight: 900;
|
||||
src: url('@/assets/fonts/vazir/Vazir-Black-FD.eot');
|
||||
src:
|
||||
url('@/assets/fonts/vazir/Vazir-Black-FD.eot') format('embedded-opentype'),
|
||||
url('@/assets/fonts/vazir/Vazir-Black-FD.ttf') format('truetype'),
|
||||
url('@/assets/fonts/vazir/Vazir-Black-FD.woff') format('woff'),
|
||||
url('@/assets/fonts/vazir/Vazir-Black-FD.woff2') format('woff2');
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<section class="flex flex-col gap-7 pt-5 w-96">
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText v-model="name" class="w-full" />
|
||||
<label>{{ $t('name') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.name">{{ errors.name }}</small>
|
||||
</div>
|
||||
<div class="w-full flex gap-2 mt-4">
|
||||
<Button
|
||||
:label="$t('back')"
|
||||
outlined
|
||||
severity="secondary"
|
||||
class="ml-auto"
|
||||
@click="dialog.close()"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('save')"
|
||||
icon="pi pi-save"
|
||||
severity="success"
|
||||
@click="save()"
|
||||
:loading="saving"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useCategoriesStore } from '@/stores/categoreis'
|
||||
// import { useUploaderStore } from '@/stores/uploader';
|
||||
import { computed, inject, reactive, ref, watch } from 'vue'
|
||||
const name = ref('')
|
||||
const dialog = inject('dialogRef')
|
||||
|
||||
const { toast, t } = inject('service')
|
||||
|
||||
const { category } = dialog.value.data
|
||||
|
||||
const form = reactive(Object.assign({}, category || {}))
|
||||
|
||||
const errors = reactive({})
|
||||
|
||||
const file = ref({})
|
||||
|
||||
const saving = ref(false)
|
||||
|
||||
const store = useCategoriesStore()
|
||||
|
||||
const save = async () => {
|
||||
saving.value = true
|
||||
const { status, data } = await store.create(name)
|
||||
console.log('status',status);
|
||||
save.value = false
|
||||
|
||||
if (status === 201 || status === 200)
|
||||
toast.add({
|
||||
life: 2000,
|
||||
severity: 'success',
|
||||
summary: t('successful'),
|
||||
detail: t('دسته بندی جدید ایجاد شد')
|
||||
})
|
||||
dialog.value.close()
|
||||
|
||||
// form.headerImage = data.url
|
||||
// else
|
||||
// return toast.add({
|
||||
// life: 2000, severity: 'error', summary: t('error'), detail: data.msg
|
||||
// })
|
||||
|
||||
}
|
||||
// //Create or Update
|
||||
// const { status, data } =
|
||||
// !!category ? await store.update(category._id, form) : await store.store(form)
|
||||
|
||||
// saving.value = false
|
||||
|
||||
// if (status === 200 || status === 201) {
|
||||
// toast.add({
|
||||
// life: 2000,
|
||||
// severity: 'success',
|
||||
// summary: t('successful'),
|
||||
// detail: t(!!category ? 'updateSuccessfully' : 'storeSuccessfully')
|
||||
// })
|
||||
// dialog.value.close()
|
||||
// } else if (status === 422)
|
||||
// Object.assign(errors, data)
|
||||
// else
|
||||
// toast.add({
|
||||
// severity: 'error', life: 2000, detail: data.msg, summary: t('error')
|
||||
// })
|
||||
|
||||
// }
|
||||
|
||||
// watch(computed(() => Object.assign({}, form)), (value, old) => {
|
||||
// Object.keys(form).forEach((key) => {
|
||||
// if (value[key] != old[key]) delete errors[key]
|
||||
// })
|
||||
// })
|
||||
|
||||
// const parentCategoryID = computed({
|
||||
// get: () => form.parentCategoryID ? ({ [form.parentCategoryID]: true }) : null,
|
||||
// set: (v) => form.parentCategoryID = Object.keys(v)[0]
|
||||
// })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<section class="flex flex-col gap-6 w-96">
|
||||
<ul class="flex flex-col gap-5">
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('کد تخفیف') }}</strong>
|
||||
<div class="flex items-center gap-2 w-max">
|
||||
<!-- <i v-if="user.confirmNumber" class="pi pi-verified text-green-600"></i> -->
|
||||
<span>{{ coupon.coupon.code }}</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('id') }}</strong>
|
||||
<span>{{ coupon.coupon.id }}</span>
|
||||
</li>
|
||||
|
||||
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('createdAt') }}</strong>
|
||||
<span>{{ jDate(coupon.coupon.createdAt) }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('تاریخ شروع') }}</strong>
|
||||
<span>{{ jDate(coupon.coupon.startDate) }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('تاریخ انقضا') }}</strong>
|
||||
<span>{{ jDate(coupon.coupon.endDate) }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('نوع تخفیف') }}</strong>
|
||||
<span>{{ coupon.coupon.status === "Active" ? 'فعال' : 'غیرفعال' }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('ایجاد شده توسط') }}</strong>
|
||||
<span>{{ coupon.coupon.admin.name }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('تغییر وضعیت') }}</strong>
|
||||
<ToggleButton v-model="checked" onLabel="فعال" offLabel="غیرفعال" />
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, reactive } from 'vue';
|
||||
import { jDate } from '@/utils/jDate';
|
||||
import { numberFormat } from '@/utils/numberFormat';
|
||||
import { ref } from 'vue';
|
||||
const checked = ref(false);
|
||||
const tabs = ref([
|
||||
{ title: 'Tab 1', content: 'Tab 1 Content', value: '0' },
|
||||
{ title: 'Tab 2', content: 'Tab 2 Content', value: '1' },
|
||||
{ title: 'Tab 3', content: 'Tab 3 Content', value: '2' }
|
||||
]);
|
||||
const dialog = inject('dialogRef')
|
||||
|
||||
const coupon = reactive(Object.assign({}, dialog.value.data))
|
||||
console.log('der', coupon.coupon);
|
||||
const confirmToSales = (value) => {
|
||||
dialog.value.data.confirm(user, value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
msg: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="greetings">
|
||||
<h1 class="green">{{ msg }}</h1>
|
||||
<h3>
|
||||
You’ve successfully created a project with
|
||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
font-size: 2.6rem;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<Paginator :rows="10" :first="first" :pageSize="+pageSize" :totalRecords="total"
|
||||
:currentPageReportTemplate="$t('currentPageReportTemplate')"
|
||||
template="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
|
||||
:alwaysShow="true" @page="paginate" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
const props = defineProps(['total'])
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const { page = 1, pageSize = 10 } = route.query
|
||||
|
||||
const first = computed(() => (+page - 1) * +pageSize)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const paginate = (event) =>
|
||||
router.replace({ query: { page: event.page + 1, pageSize: event.pageSize } })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<section class="flex flex-col gap-6 w-96">
|
||||
<div class="flex flex-col gap-2 items-center self-center">
|
||||
<Avatar :image="user.product.image" class="w-full h-40" />
|
||||
<h2 class="text-xl">{{ user.product.name }}</h2>
|
||||
<!-- <Tag :value="$t(user.accountType.toLowerCase())" severity="warning" /> -->
|
||||
</div>
|
||||
<ul class="flex flex-col gap-5">
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('category') }}</strong>
|
||||
<div class="flex items-center gap-2 w-max">
|
||||
<!-- <i v-if="user.confirmNumber" class="pi pi-verified text-green-600"></i> -->
|
||||
<span>{{ user.product.categories[0].name }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('id') }}</strong>
|
||||
<div class="flex items-center gap-2 w-max">
|
||||
<!-- <i v-if="user.confirmEmail" class="pi pi-verified text-green-600"></i> -->
|
||||
<span>{{ user.product._id }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('تعداد ویدیو ها') }}</strong>
|
||||
<span>{{ user.product.countOfVideos }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('createdAt') }}</strong>
|
||||
<span>{{ jDate(user.product.createdAt) }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('updatedAt') }}</strong>
|
||||
<span>{{ jDate(user.product.updatedAt) }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('زمان دوره') }}</strong>
|
||||
<span>{{ user.product.duration }} ساعت</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('زبان') }}</strong>
|
||||
<span>{{ user.product.language }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('price') }}</strong>
|
||||
<span>{{ numberFormat(user.product.price) }} تومان</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('تعداد دانشجو ها') }}</strong>
|
||||
<span>{{ user.product.studentsCount }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('مدرس دوره') }}</strong>
|
||||
<span>{{ user.product.teacher.name }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('حجم دوره') }}</strong>
|
||||
<span>{{ user.product.totalFileSize }} گیگابایت</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('امتیاز دانشجویان') }}</strong>
|
||||
<span>{{ user.product.averageRating }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, reactive } from 'vue';
|
||||
import { jDate } from '@/utils/jDate';
|
||||
import { numberFormat } from '@/utils/numberFormat';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const tabs = ref([
|
||||
{ title: 'Tab 1', content: 'Tab 1 Content', value: '0' },
|
||||
{ title: 'Tab 2', content: 'Tab 2 Content', value: '1' },
|
||||
{ title: 'Tab 3', content: 'Tab 3 Content', value: '2' }
|
||||
]);
|
||||
const dialog = inject('dialogRef')
|
||||
|
||||
const user = reactive(Object.assign({}, dialog.value.data))
|
||||
console.log('der', user.product);
|
||||
const confirmToSales = (value) => {
|
||||
dialog.value.data.confirm(user, value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<section class="container w-full">
|
||||
<Stepper>
|
||||
<StepperPanel :header="$t('information')">
|
||||
<template #content="{ nextCallback }">
|
||||
<FirstStep @close="dialog.close()" @next="nextCallback" />
|
||||
</template>
|
||||
</StepperPanel>
|
||||
<StepperPanel :header="$t('details')">
|
||||
<template #content="{ nextCallback, prevCallback }">
|
||||
<SecondStep @prev="prevCallback" @next="nextCallback" />
|
||||
</template>
|
||||
</StepperPanel>
|
||||
<StepperPanel :header="$t('images')">
|
||||
<template #content="{ prevCallback }">
|
||||
<ThirdStep @prev="prevCallback" @save="save()" />
|
||||
</template>
|
||||
</StepperPanel>
|
||||
</Stepper>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUploaderStore } from '@/stores/uploader';
|
||||
import FirstStep from './ProductFormSteps/FirstStep.vue';
|
||||
import SecondStep from './ProductFormSteps/SecondStep.vue';
|
||||
import ThirdStep from './ProductFormSteps/ThirdStep.vue';
|
||||
|
||||
import { inject, provide, reactive, ref } from 'vue';
|
||||
import { useProductsStore } from '@/stores/products';
|
||||
|
||||
const dialog = inject('dialogRef')
|
||||
const { toast, t } = inject('service')
|
||||
const { product } = dialog.value.data
|
||||
|
||||
const form = reactive(Object.assign({}, product || {}))
|
||||
console.log(form)
|
||||
const errors = reactive({})
|
||||
|
||||
const saving = ref(false)
|
||||
|
||||
provide('data', { form, errors, saving })
|
||||
|
||||
const store = useProductsStore()
|
||||
|
||||
const uploader = useUploaderStore()
|
||||
|
||||
const save = async () => {
|
||||
saving.value = true
|
||||
|
||||
console.log(form);
|
||||
|
||||
//Upload File
|
||||
if (typeof form.coverImage === 'object') {
|
||||
const { status, data } = await uploader.single(form.coverImage)
|
||||
|
||||
if (status === 201)
|
||||
form.coverImage = data.url
|
||||
else
|
||||
return toast.add({
|
||||
life: 2000, severity: 'error', summary: t('error'), detail: data.msg
|
||||
})
|
||||
}
|
||||
|
||||
//upload images
|
||||
const files = form.images.filter((image) => typeof image === 'object')
|
||||
if (files.length > 0) {
|
||||
const { status, data } = await uploader.bulk(files)
|
||||
|
||||
if (status === 201)
|
||||
form.images = form.images.map((image) => {
|
||||
if (typeof image === 'object')
|
||||
return data.urls.shift();
|
||||
return image;
|
||||
})
|
||||
else
|
||||
return toast.add({
|
||||
life: 2000, severity: 'error', summary: t('error'), detail: data.msg
|
||||
})
|
||||
}
|
||||
|
||||
//Create or Update
|
||||
const { status, data } =
|
||||
!!product ? await store.update(product._id, form) : await store.store(form)
|
||||
|
||||
saving.value = false
|
||||
|
||||
if (status === 200 || status === 201) {
|
||||
toast.add({
|
||||
life: 2000,
|
||||
severity: 'success',
|
||||
summary: t('successful'),
|
||||
detail: t(!!category ? 'updateSuccessfully' : 'storeSuccessfully')
|
||||
})
|
||||
dialog.value.close()
|
||||
} else if (status === 422)
|
||||
Object.assign(errors, data)
|
||||
else
|
||||
toast.add({
|
||||
severity: 'error', life: 2000, detail: data.msg, summary: t('error')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<section class="flex flex-col gap-4 ">
|
||||
<div class="flex flex-col gap-1">
|
||||
<FileUpload mode="advanced" accept="image/*" multiple :maxFileSize="10490000" @select="chooseImages"
|
||||
@remove="chooseImages" :invalidFileSizeMessage="$t('invalidFileSizeMessage')">
|
||||
<template #header="{ chooseCallback }">
|
||||
<div class="flex justify-between w-full items-center">
|
||||
<h2 class="font-bold text-xl ">{{ $t('imagesGallery') }}</h2>
|
||||
<Button :label="$t('chooseImage')" severity="secondary" icon="pi pi-images" text
|
||||
@click="chooseCallback()" />
|
||||
</div>
|
||||
</template>
|
||||
<template #content="{ files, removeFileCallback }">
|
||||
<div v-if="files.length > 0" class="flex gap-4 justify-between ">
|
||||
<div class="flex gap-4 w-96 h-96 relative m-4 rounded-lg overflow-hidden bg-gray-50 shrink-0">
|
||||
<img :src="form.coverImage?.objectURL || form.coverImage" class="object-cover">
|
||||
<div class=" bg-black/60 absolute bottom-2 right-2 flex rounded-full px-4 py-2">
|
||||
<span class="text-white text-sm m-auto"> {{ $t('mainImage') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Listbox v-model="form.coverImage" :options="files" @change="changeCoverImage(files)"
|
||||
class="border-none" listStyle="max-height:26rem">
|
||||
<template #option="{ option, index }">
|
||||
<div class="flex flex-row-reverse gap-4 items-center w-96 rounded">
|
||||
<img :src="option.objectURL || option"
|
||||
class="w-24 h-20 object-cover object-center rounded-lg">
|
||||
<div class="flex items-end justify-center h-full flex-col gap-4 ">
|
||||
<h3 class="font-bold truncate w-40 text-left">{{ option.name }}</h3>
|
||||
<span dir="ltr" class="text-sm">{{ formatSize(option.size) }}</span>
|
||||
</div>
|
||||
<Button icon="pi pi-times" size="small" text rounded severity="danger"
|
||||
class="ml-auto p-0 w-6 h-6" @click="removeFileCallback(index)" />
|
||||
</div>
|
||||
</template>
|
||||
</Listbox>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<template #empty>
|
||||
<div class="flex items-center justify-center flex-col gap-8 py-12 h-96">
|
||||
<i class="pi pi-images border-2 rounded-full p-5 text-8xl text-400 border-400" />
|
||||
<p>{{ $t('Drag and drop images to here to upload.') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</FileUpload>
|
||||
<small v-if="errors.coverImage || errors.images" class="p-error">
|
||||
{{ errors.coverImage || errors.images }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="flex pt-4 justify-content-end">
|
||||
<Button :label="$t('back')" outlined severity="secondary" class="ml-auto" @click="$emit('close')" />
|
||||
<Button :label="$t('next')" icon="pi pi-arrow-left" iconPos="right" severity="success"
|
||||
@click="$emit('next')" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
const dialogRef = inject('dialogRef')
|
||||
|
||||
const { form, errors } = inject('data')
|
||||
console.log('ax',form);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<section class="flex flex-col gap-4 ">
|
||||
مرحله دوم
|
||||
<!-- <div class="flex gap-7 bg-gray-50/50 p-7 pt-7 border rounded-lg">
|
||||
<div class="flex flex-col gap-7">
|
||||
<div class="flex flex-col gap-1 w-96">
|
||||
<FloatLabel>
|
||||
<InputText v-model="form.title" class="w-full" />
|
||||
<label>{{ $t('title') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.title">{{ errors.title }}</small>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText v-model="form.titleEN" class="w-full ltr" />
|
||||
<label>{{ $t('titleEN') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.titleEN">{{ errors.titleEN }}</small>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText v-model="form.link" class="w-full ltr" />
|
||||
<label>{{ $t('link') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.link">{{ errors.link }}</small>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText v-model="form.metaTitle" class="w-full" />
|
||||
<label>{{ $t('metaTitle') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.metaTitle">{{ errors.metaTitle }}</small>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<Textarea v-model="form.metaDescription" rows="5" class="w-full" />
|
||||
<label>{{ $t('metaDescription') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.metaDescription">{{ errors.metaDescription }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 gorw">
|
||||
<Editor v-model="form.description" :placeholder="$t('description')" class="h-full"
|
||||
editorStyle="max-height: calc(100% -46px)" />
|
||||
<small v-if="errors.description">{{ errors.description }}</small>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
<div class="flex pt-4 justify-content-end">
|
||||
<Button :label="$t('back')" outlined severity="secondary" class="ml-auto" @click="$emit('close')" />
|
||||
<Button :label="$t('next')" icon="pi pi-arrow-left" iconPos="right" severity="success"
|
||||
@click="$emit('next')" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
const dialogRef = inject('dialogRef')
|
||||
|
||||
const { form, errors } = inject('data')
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<section class="flex flex-col gap-4 ">
|
||||
<Panel :header="$t('اطلاعات دوره')">
|
||||
<div class="grid grid-cols-3 gap-7">
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t('title') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.title">{{ errors.title }}</small>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t('مدت زمان دوره') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.title">{{ errors.title }}</small>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t('دسته بندی') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.title">{{ errors.title }}</small>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t('قیمت') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.title">{{ errors.title }}</small>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t('مدرس') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.title">{{ errors.title }}</small>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t('درصد تکمیل شده') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.title">{{ errors.title }}</small>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t('سطح') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.title">{{ errors.title }}</small>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t('زبان') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.title">{{ errors.title }}</small>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t('تعداد ویدیو') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.title">{{ errors.title }}</small>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t('حجم کل دوره') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.title">{{ errors.title }}</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 mt-8">
|
||||
<Editor :placeholder="$t('description')" class="h-full"
|
||||
editorStyle="min-height: 200px" />
|
||||
<small v-if="errors.description">{{ errors.description }}</small>
|
||||
</div>
|
||||
</Panel>
|
||||
<div class="flex pt-4 justify-content-end">
|
||||
<Button :label="$t('prev')" outlined severity="secondary" icon="pi pi-arrow-right" class="ml-auto"
|
||||
@click="$emit('prev')" />
|
||||
<Button :label="$t('save')" icon="pi pi-save" severity="success" :loading="saving" @click="$emit('save')" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { formatSize } from '@/utils/formatSize'
|
||||
import { inject } from 'vue';
|
||||
|
||||
const { form, errors, saving } = inject('data')
|
||||
|
||||
const chooseImages = ({ files }) => {
|
||||
if (!form.coverImage) form.coverImage = files?.[0] || null
|
||||
changeCoverImage(files)
|
||||
}
|
||||
|
||||
const changeCoverImage = (files) => {
|
||||
form.images = files.filter((img) => img != form.coverImage)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup>
|
||||
import WelcomeItem from './WelcomeItem.vue'
|
||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
||||
import ToolingIcon from './icons/IconTooling.vue'
|
||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
||||
import CommunityIcon from './icons/IconCommunity.vue'
|
||||
import SupportIcon from './icons/IconSupport.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<DocumentationIcon />
|
||||
</template>
|
||||
<template #heading>Documentation</template>
|
||||
|
||||
Vue’s
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
||||
provides you with all information you need to get started.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<ToolingIcon />
|
||||
</template>
|
||||
<template #heading>Tooling</template>
|
||||
|
||||
This project is served and bundled with
|
||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
||||
recommended IDE setup is
|
||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
||||
you need to test your components and web pages, check out
|
||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
||||
<a href="https://on.cypress.io/component" target="_blank" rel="noopener"
|
||||
>Cypress Component Testing</a
|
||||
>.
|
||||
|
||||
<br />
|
||||
|
||||
More instructions are available in <code>README.md</code>.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<EcosystemIcon />
|
||||
</template>
|
||||
<template #heading>Ecosystem</template>
|
||||
|
||||
Get official tools and libraries for your project:
|
||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
||||
you need more resources, we suggest paying
|
||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||
a visit.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<CommunityIcon />
|
||||
</template>
|
||||
<template #heading>Community</template>
|
||||
|
||||
Got stuck? Ask your question on
|
||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
||||
Discord server, or
|
||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||
>StackOverflow</a
|
||||
>. You should also subscribe to
|
||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
||||
the official
|
||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||
twitter account for latest news in the Vue world.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<SupportIcon />
|
||||
</template>
|
||||
<template #heading>Support Vue</template>
|
||||
|
||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
||||
us by
|
||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||
</WelcomeItem>
|
||||
</template>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<section class="flex flex-col gap-6 w-96">
|
||||
<div class="flex flex-col gap-2 items-center self-center">
|
||||
<Avatar :image="user.avatarMedia" shape="circle" class="w-24 h-24" />
|
||||
<h2 class="text-xl">{{ user.name }} {{ user.lastName }}</h2>
|
||||
<!-- <Tag :value="$t(user.accountType.toLowerCase())" severity="warning" /> -->
|
||||
</div>
|
||||
<ul class="flex flex-col gap-5">
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('phoneNumber') }}</strong>
|
||||
<div class="flex items-center gap-2 w-max">
|
||||
<!-- <i v-if="user.confirmNumber" class="pi pi-verified text-green-600"></i> -->
|
||||
<span>{{ user.phone }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('id') }}</strong>
|
||||
<div class="flex items-center gap-2 w-max">
|
||||
<!-- <i v-if="user.confirmEmail" class="pi pi-verified text-green-600"></i> -->
|
||||
<span>{{ user.id }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<strong>{{ $t('status') }}</strong>
|
||||
<span>{{ user.statusText }}</span>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { jDate } from '@/utils/jDate';
|
||||
|
||||
import { inject, reactive } from 'vue';
|
||||
|
||||
const dialog = inject('dialogRef')
|
||||
|
||||
const user = reactive(Object.assign({}, dialog.value.data.user))
|
||||
|
||||
const confirmToSales = (value) => {
|
||||
dialog.value.data.confirm(user, value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="item">
|
||||
<i>
|
||||
<slot name="icon"></slot>
|
||||
</i>
|
||||
<div class="details">
|
||||
<h3>
|
||||
<slot name="heading"></slot>
|
||||
</h3>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
i {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.item {
|
||||
margin-top: 0;
|
||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
i {
|
||||
top: calc(50% - 25px);
|
||||
left: -26px;
|
||||
position: absolute;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.item:before {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:after {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:first-of-type:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item:last-of-type:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<Toolbar>
|
||||
<template #start>
|
||||
<h1 class="text-2xl font-bold">{{ $t('asanMarketAdmin') }}</h1>
|
||||
</template>
|
||||
<template #end>
|
||||
<Button :label="$t('logout')" outlined severity="danger" icon="pi pi-sign-out rotate-180" iconPos="right"
|
||||
@click="logout()" />
|
||||
</template>
|
||||
</Toolbar>
|
||||
|
||||
|
||||
<main class="container flex gap-8">
|
||||
<PanelMenu :model="items" class="w-72 shrink-0" />
|
||||
<RouterView class="grow" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getActivePinia } from 'pinia';
|
||||
import { inject, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const { t, confirm, toast } = inject('service')
|
||||
const router = useRouter()
|
||||
|
||||
const items = ref([
|
||||
|
||||
// {
|
||||
// label: t('blog'),
|
||||
// icon: 'pi pi-sitemap',
|
||||
// items: [
|
||||
// {
|
||||
// label: t('posts'),
|
||||
// icon: 'pi pi-sitemap',
|
||||
// command: () => router.push('/blog/posts')
|
||||
// },
|
||||
// {
|
||||
// label: t('categories'),
|
||||
// icon: 'pi pi-sitemap',
|
||||
// command: () => router.push('/blog/categories')
|
||||
// },
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// label: t('news'),
|
||||
// icon: 'pi pi-sitemap',
|
||||
// command: () => router.push('/news')
|
||||
// },
|
||||
// {
|
||||
// label: t('emails'),
|
||||
// icon: 'pi pi-sitemap',
|
||||
// command: () => router.push('/emails')
|
||||
// },
|
||||
// {
|
||||
// label: t('sms'),
|
||||
// icon: 'pi pi-sitemap',
|
||||
// command: () => router.push('/sms')
|
||||
// },
|
||||
// {
|
||||
// label: t('moeens'),
|
||||
// icon: 'pi pi-sitemap',
|
||||
// command: () => router.push('/moeens')
|
||||
// },
|
||||
|
||||
{
|
||||
label: t('pages'),
|
||||
icon: 'pi pi-sitemap',
|
||||
items: [
|
||||
{
|
||||
label: t('header'),
|
||||
icon: 'pi pi-desktop',
|
||||
command: () => router.push('/pages/header-menu')
|
||||
},
|
||||
{
|
||||
label: t('aboutUs'),
|
||||
icon: 'pi pi-exclamation-circle',
|
||||
command: () => router.push('/pages/about-us')
|
||||
},
|
||||
// {
|
||||
// label: t('contactUs'),
|
||||
// icon: 'pi pi-sitemap',
|
||||
// command: () => router.push('/pages/contact-us')
|
||||
// }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: t('courses'),
|
||||
icon: 'pi pi-video',
|
||||
items: [
|
||||
{
|
||||
label: t('allCourses'),
|
||||
icon: 'pi pi-angle-double-down',
|
||||
command: () => router.push('/courses')
|
||||
},
|
||||
{
|
||||
label: t('createCourse'),
|
||||
icon: 'pi pi-file-plus',
|
||||
command: () => router.push('/create')
|
||||
},
|
||||
{
|
||||
label: t('addVideo'),
|
||||
icon: 'pi pi-file-edit',
|
||||
command: () => router.push('/courses/update')
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
label: t('users'),
|
||||
icon: 'pi pi-users',
|
||||
command: () => router.push('/users')
|
||||
},
|
||||
{
|
||||
label: t('categories'),
|
||||
icon: 'pi pi-th-large',
|
||||
command: () => router.push('/categories')
|
||||
},
|
||||
{
|
||||
label: t('coupons'),
|
||||
icon: 'pi pi-barcode',
|
||||
items: [
|
||||
{
|
||||
label: t('ایجاد تخفیف'),
|
||||
icon: 'pi pi-barcode',
|
||||
command: () => router.push('/createCoupon')
|
||||
},
|
||||
{
|
||||
label: t('همه ی تخفیف ها'),
|
||||
icon: 'pi pi-barcode',
|
||||
command: () => router.push('/Coupons')
|
||||
},
|
||||
{
|
||||
label: t('تاریخچه ی تخفیف ها'),
|
||||
icon: 'pi pi-barcode',
|
||||
command: () => router.push('/Coupons')
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('admins'),
|
||||
icon: 'pi pi-sitemap',
|
||||
command: () => router.push('/admins')
|
||||
},
|
||||
{
|
||||
label: t('comments'),
|
||||
icon: 'pi pi-sitemap',
|
||||
command: () => router.push('/comments')
|
||||
},
|
||||
{
|
||||
label: t('orders'),
|
||||
icon: 'pi pi-sitemap',
|
||||
command: () => router.push('/orders')
|
||||
},
|
||||
{
|
||||
label: t('products'),
|
||||
icon: 'pi pi-sitemap',
|
||||
command: () => router.push('/products')
|
||||
},
|
||||
// {
|
||||
// label: t('brands'),
|
||||
// icon: 'pi pi-apple',
|
||||
// command: () => router.push('/brands')
|
||||
// },
|
||||
|
||||
]);
|
||||
|
||||
const logout = () => {
|
||||
confirm.require({
|
||||
message: t('Are you sure you want to proceed?'),
|
||||
header: t('Danger Zone'),
|
||||
icon: 'pi pi-info-circle',
|
||||
acceptClass: 'p-button-danger p-button-sm',
|
||||
rejectClass: 'p-button-secondary p-button-outlined p-button-sm',
|
||||
rejectLabel: t('cancel'),
|
||||
acceptLabel: t('forceLogout'),
|
||||
defaultFocus: 'reject',
|
||||
accept: () => {
|
||||
getActivePinia()._s.forEach(store => store.$reset());
|
||||
localStorage.removeItem('state')
|
||||
router.replace({ name: "Login" })
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"header": "صفحه اصلی",
|
||||
"asanMarketAdmin": "مدیریت دی لرن ",
|
||||
"login": "ورود",
|
||||
"otp": "کد ورود",
|
||||
"sendOtp": "ارسال کد",
|
||||
"addVideo": "افزودن ویدیو",
|
||||
"coupons": "تخفیف ها",
|
||||
"token": "توکن",
|
||||
"courses": "دوره ها",
|
||||
"create": "ایجاد",
|
||||
"email": "ایمیل",
|
||||
"password": "رمزعبور",
|
||||
"name": "نام",
|
||||
"image": "تصویر",
|
||||
"link": "لینک",
|
||||
"description": "توضیحات",
|
||||
"categories": "دستهبندیها",
|
||||
"add": "افزودن",
|
||||
"edit": "ویرایش",
|
||||
"back": "بازگشت",
|
||||
"cancel": "انصراف",
|
||||
"save": "ذخیره",
|
||||
"update": "بروزرسانی",
|
||||
"createNewCategory": "ایجاد دستهبندی جدید",
|
||||
"editCategory": "ویرایش دستهبندی",
|
||||
"parentCategory": "دستهبندی والد",
|
||||
"chooseImage": "انتخاب تصویر",
|
||||
"confirmation": "تائیدیه",
|
||||
"Danger Zone": "منطقه خطر",
|
||||
"beOmitted": "حذف شود",
|
||||
"Are you sure you want to proceed?": "آیا مطمئن هستید که می خواهید ادامه دهید؟",
|
||||
"products": "محصولات",
|
||||
"currentPageReportTemplate": "نمایش {'{first}'} تا {'{last}'} از {'{totalRecords}'} مورد",
|
||||
"brand": "برند",
|
||||
"brands": "برندها",
|
||||
"title": "عنوان",
|
||||
"titleEN": "عنوان لاتین",
|
||||
"logo": "لوگو",
|
||||
"createNewBrand": "ایجاد برند جدید",
|
||||
"editBrand": "ویرایش برند",
|
||||
"category": "دستهبندی",
|
||||
"images": "تصاویر",
|
||||
"price": "قیمت",
|
||||
"reviews": "بررسیها",
|
||||
"status": "وضعیت",
|
||||
"loading": "درحال بارگذاری ...",
|
||||
"createNewProduct": "ایجاد محصول جدید",
|
||||
"editProduct": "ویرایش محصول",
|
||||
"productDetails": "جزئیات محصول",
|
||||
"information": "اطلاعات",
|
||||
"specifications": "مشخصات",
|
||||
"seo": "سئو",
|
||||
"metaTitle": "عنوان متا",
|
||||
"metaDescription": "توضیحات متا",
|
||||
"warranty": "گارانتی",
|
||||
"toman": "تومان",
|
||||
"orderRangeMin": "حداقل: {0}",
|
||||
"orderRange": "محدوده سفارش",
|
||||
"orderRangeMax": "حداکثر: {1}",
|
||||
"stockQuantity": "مقدار موجودی",
|
||||
"boxSeller": "فروش کارتونی",
|
||||
"itemInBox": "تعداد در هر کارتون",
|
||||
"value": "مقدار",
|
||||
"Drag and drop images to here to upload.": "برای آپلود تصویر را به اینجا بکشید و رها کنید.",
|
||||
"mainImage": "تصویر اصلی",
|
||||
"imagesGallery": "بنر دوره",
|
||||
"invalidFileSizeMessage": "اندازه فایل نامعتبر است، اندازه فایل باید کوچکتر از {'{1}'} باشد. {'{0}'}",
|
||||
"next": "بعدی",
|
||||
"id": "آیدی",
|
||||
"prev": "قبلی",
|
||||
"details": "جزئیات",
|
||||
"shops": "فروشگاهها",
|
||||
"options": "گزینه ها",
|
||||
"blog": "بلاگ",
|
||||
"posts": "مطالب",
|
||||
"news": "اخبار",
|
||||
"emails": "ایمیلها",
|
||||
"moeens": "فاکتورهای معین",
|
||||
"pages": "صفحات",
|
||||
"headerMenu": "منوی هدر",
|
||||
"slides": "اسلایدها",
|
||||
"contactUs": "ارتباط با ما",
|
||||
"aboutUs": "درباره ما",
|
||||
"admins": "مدیران",
|
||||
"fees": "نرخها",
|
||||
"users": "کاربران",
|
||||
"comments": "نظرات",
|
||||
"orders": "سفارشات",
|
||||
"content": "محتوا",
|
||||
"createdAt": "تاریخ ایجاد",
|
||||
"updatedAt": "آخرین ویرایش",
|
||||
"fullName": "نام و نام خانوادگی",
|
||||
"phoneNumber": "شماره تماس",
|
||||
"shopName": "نام فروشگاه",
|
||||
"active": "فعال",
|
||||
"inactive": "غیرفعال",
|
||||
"editShop": "ویرایش فروشگاه",
|
||||
"createNews": "ایجاد خبر جدید",
|
||||
"pending": "درانتظار",
|
||||
"paid": "پرداخت شده",
|
||||
"sent": "ارسال شده",
|
||||
"notSent": "ارسال نشده",
|
||||
"send": "ارسال",
|
||||
"icon": "ایکن",
|
||||
"docId": "شماره سند",
|
||||
"date": "تاریخ",
|
||||
"createNewEmail": "ایجاد ایمیل جدید",
|
||||
"editEmail": "ویرایش ایمیل",
|
||||
"createNewMoeen": "ایجاد فاکتور جدید",
|
||||
"editMoeen": "ویرایش فاکتور",
|
||||
"bankId": "شناسنه بانک",
|
||||
"companyName": "نام شرکت",
|
||||
"debtor": "بدهکار",
|
||||
"creditor": "بستانکار",
|
||||
"profilePhoto": "تصویر پروفایل",
|
||||
"firstName": "نام",
|
||||
"lastName": "نام خانوادگی",
|
||||
"username": "نام کاربری",
|
||||
"createNewAdmin": "ایجاد مدیر جدید",
|
||||
"editAdmin": "ویرایش مدیر",
|
||||
"permissions": "دسترسیها",
|
||||
"product": "محصول",
|
||||
"admin": "مدیر",
|
||||
"fee": "نرخ",
|
||||
"order": "سفارش",
|
||||
"discount": "تخفیف",
|
||||
"moeen": "فاکتور معین",
|
||||
"shop": "فروشکاه",
|
||||
"item": "مورد",
|
||||
"user": "کاربر",
|
||||
"site": "سایت",
|
||||
"contentus": "درباره ما",
|
||||
"customers": "خریداران",
|
||||
"sellers": "فروشندگان",
|
||||
"userDetails": "جزئیات کاربر",
|
||||
"retailer": "خرده فروش",
|
||||
"dateOfBirth": "تاریخ تولد",
|
||||
"gender": "جنسیت",
|
||||
"male": "آقا",
|
||||
"female": "خانم",
|
||||
"governmentImage": "تصویر کارت ملی",
|
||||
"confirmToSales": "تایید فروش",
|
||||
"confirmed": "تایید شده",
|
||||
"notconfirmed": "تاید نشده",
|
||||
"point": "امتیاز",
|
||||
"commentDetails": "جزئیات نظر",
|
||||
"positive": "نقاط قوت",
|
||||
"negative": "نقاط ضعف",
|
||||
"coverImage": "تصویر محصول",
|
||||
"confirm": "تایید",
|
||||
"invoiceNumber": "شماره فاکتور",
|
||||
"totalAmount": "قیمت کل",
|
||||
"priceWithDiscount": "قیمت کل با تخفیف",
|
||||
"orderDate": "زمان سفارش",
|
||||
"productsCount": "تعداد محصولات",
|
||||
"address": "آدرس",
|
||||
"error": "خطا",
|
||||
"successful": "موفق",
|
||||
"sendOtpSuccessfully": "کد ورود برای شما ارسال شد.",
|
||||
"loginSuccessfully": "با موفقیت وارد شدید.",
|
||||
"logout": "خروج از حساب",
|
||||
"forceLogout": "خارج شو",
|
||||
"emptyTable": "موردی برای نمایش وجود ندارد!",
|
||||
"storeSuccessfully": "با موفقیت ذخیره شد.",
|
||||
"updateSuccessfully": "با موفقیت بروزرسانی شد.",
|
||||
"destroySuccessfully": "با موفقیت حذف شد.",
|
||||
"quantity": "تعداد",
|
||||
"resetPassword": "تنظیم رمزعبور",
|
||||
"newPassword": "رمزعبور جدید",
|
||||
"reNewPassword": "تکرار رمزعبور جدید",
|
||||
"createNewAboutUs": "ایجاد درباره ما جدید",
|
||||
"editAboutUs": "ویرایش درباره ما",
|
||||
"payment": "در انتظار پرداخت",
|
||||
"processing": "درحال آماده سازی",
|
||||
"shipped": "ارسال شده",
|
||||
"delivered": "دریافت شده",
|
||||
"cancelled": "لغو شده",
|
||||
"rejected": "رد شده",
|
||||
"accountId": " شماره حساب",
|
||||
"accountName": " نام حساب",
|
||||
"desc": "پیامک ها",
|
||||
"sms": "پیامک ها",
|
||||
"priority":"اولویت",
|
||||
"createCourse": "ایجاد دوره جدید",
|
||||
"allCourses": "همه ی دوره ها",
|
||||
"updateCourse": "بروزرسانی دوره"
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import './assets/fonts/vazir/style.css'
|
||||
import './assets/css/style.css'
|
||||
import './assets/css/theme.css'
|
||||
import 'primeicons/primeicons.css'
|
||||
|
||||
import { createApp, watch } from 'vue'
|
||||
import PrimeVue from 'primevue/config';
|
||||
import Ripple from 'primevue/ripple'
|
||||
import Tooltip from 'primevue/tooltip';
|
||||
import Badge from 'primevue/badgedirective'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import DialogService from 'primevue/dialogservice'
|
||||
import ConfirmationService from 'primevue/confirmationservice';
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import i18n from './plugins/i18n'
|
||||
import Vue3PersianDatetimePicker from 'vue3-persian-datetime-picker'
|
||||
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import FloatLabel from 'primevue/floatlabel';
|
||||
import InputGroup from 'primevue/inputgroup';
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon';
|
||||
import Pagination from '@/components/Pagination.vue'
|
||||
import Stepper from 'primevue/stepper';
|
||||
import StepperPanel from 'primevue/stepperpanel';
|
||||
import Calendar from 'primevue/calendar';
|
||||
//import Select from 'primevue/select';
|
||||
|
||||
const pinia = createPinia()
|
||||
const app = createApp(App)
|
||||
app.use(i18n)
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
app.use(PrimeVue, { ripple: true, pt: {} });
|
||||
app.use(ConfirmationService);
|
||||
app.use(DialogService)
|
||||
app.use(ToastService)
|
||||
|
||||
app.directive('badge', Badge)
|
||||
app.directive('ripple', Ripple)
|
||||
app.directive('tooltip', Tooltip);
|
||||
|
||||
app.component('FloatLabel', FloatLabel)
|
||||
app.component('InputGroup', InputGroup)
|
||||
app.component('InputGroupAddon', InputGroupAddon)
|
||||
app.component('Stepper', Stepper)
|
||||
app.component('StepperPanel', StepperPanel)
|
||||
app.component('Calendar', Calendar)
|
||||
// app.component('Select', Select)
|
||||
app.component('Pagination', Pagination)
|
||||
app.component('DatePicker', Vue3PersianDatetimePicker)
|
||||
//app.component('date-picker', VuePersianDatetimePicker);
|
||||
|
||||
app.mount('body')
|
||||
|
||||
watch(pinia.state, (state) => localStorage.setItem('state', JSON.stringify(state)), { deep: true })
|
||||
if (localStorage.getItem('state'))
|
||||
pinia.state.value = JSON.parse(localStorage.getItem('state') || '{}')
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useAdminsStore } from '@/stores/admins'
|
||||
import axioss from 'axios'
|
||||
|
||||
const baseURL = 'https://dlearn.iran.liara.run'
|
||||
|
||||
const axios = axioss.create({ baseURL })
|
||||
|
||||
axios.interceptors.request.use((config) => {
|
||||
const admin = useAdminsStore()
|
||||
config.headers.Authorization = `Bearer ${admin.token}`
|
||||
return config
|
||||
})
|
||||
|
||||
// axios.interceptors.response.use(
|
||||
// async (res) => {
|
||||
// if(!res.data?.url || !res.data?.urls){
|
||||
// res.data = JSON.parse(JSON.stringify(res.data).replaceAll('/uploads', baseURL + '/uploads'))
|
||||
// return res
|
||||
// }
|
||||
// },
|
||||
// async (error) => {
|
||||
// const status = error?.response?.status
|
||||
|
||||
// if (status === 401)
|
||||
// return window.location.href = '/login?to=' + window.location.href;
|
||||
|
||||
// return error?.response
|
||||
// }
|
||||
// )
|
||||
|
||||
export default axios
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import fa from '../locales/fa.json'
|
||||
|
||||
export default createI18n({
|
||||
legacy: false,
|
||||
messages: { fa },
|
||||
locale: 'fa',
|
||||
})
|
||||
@@ -0,0 +1,121 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import Login from '@/views/Login.vue'
|
||||
import Main from '../layouts/Main.vue'
|
||||
import { useAdminsStore } from '@/stores/admins'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'Main',
|
||||
//redirect: '/products',
|
||||
component: Main,
|
||||
children: [
|
||||
{
|
||||
path: '/categories',
|
||||
name: 'Categories',
|
||||
component: () => import('@/views/Categories.vue'),
|
||||
},
|
||||
{
|
||||
path: '/news',
|
||||
name: 'News',
|
||||
component: () => import('@/views/News.vue'),
|
||||
},
|
||||
{
|
||||
path: '/coupons',
|
||||
name: 'Coupons',
|
||||
component: () => import('@/views/Coupons.vue'),
|
||||
},
|
||||
{
|
||||
path: '/courses',
|
||||
name: 'courses',
|
||||
component: () => import('@/views/Courses.vue'),
|
||||
},
|
||||
{
|
||||
path: '/create',
|
||||
name: 'create',
|
||||
component: () => import('@/views/Create.vue'),
|
||||
},
|
||||
// {
|
||||
// path: '/products',
|
||||
// name: 'Products',
|
||||
// component: () => import('@/views/Products.vue'),
|
||||
// },
|
||||
// {
|
||||
// path: '/orders',
|
||||
// name: 'Orders',
|
||||
// component: () => import('@/views/Orders.vue'),
|
||||
// },
|
||||
// {
|
||||
// path: '/comments',
|
||||
// name: 'Comments',
|
||||
// component: () => import('@/views/Comments.vue'),
|
||||
// },
|
||||
{
|
||||
path: '/users',
|
||||
name: 'Users',
|
||||
component: () => import('@/views/Users.vue'),
|
||||
},
|
||||
{
|
||||
path: '/admins',
|
||||
name: 'admins',
|
||||
component: () => import('@/views/Admins.vue'),
|
||||
},
|
||||
{
|
||||
path: '/:id',
|
||||
name: 'Edit',
|
||||
component: () => import('@/views/Edit.vue'),
|
||||
},
|
||||
{
|
||||
path: '/CreateCoupon',
|
||||
name: 'CreateCoupon',
|
||||
component: () => import('@/views/CreateCoupon.vue'),
|
||||
},
|
||||
{
|
||||
path: '/coupon/:id',
|
||||
name: 'EditCoupon',
|
||||
component: () => import('@/views/EditCoupon.vue'),
|
||||
},
|
||||
{
|
||||
path: '/pages',
|
||||
children: [
|
||||
{
|
||||
path: 'header-menu',
|
||||
name: 'HeaderMenu',
|
||||
component: () => import('@/views/HeaderMenu.vue')
|
||||
},
|
||||
{
|
||||
path: 'about-us',
|
||||
name: 'about-us',
|
||||
component: () => import('@/views/About.vue')
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
|
||||
],
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
router.beforeEach(function (to, from, next) {
|
||||
const admins = useAdminsStore()
|
||||
if (!admins.isLoggedIn && to.name != 'Login')
|
||||
next({ name: 'Login' })
|
||||
else if (admins.isLoggedIn && to.name == 'Login')
|
||||
next({ name: from.name })
|
||||
else next()
|
||||
})
|
||||
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,50 @@
|
||||
import axios from '../plugins/axios'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAdminsStore = defineStore('admins', {
|
||||
|
||||
state: () => ({
|
||||
items: [],
|
||||
fetching: true,
|
||||
total: 0,
|
||||
token: null,
|
||||
permissions: []
|
||||
}),
|
||||
getters: {
|
||||
isLoggedIn: (state) => !!state.token
|
||||
},
|
||||
actions:{
|
||||
async login(number) {
|
||||
const { status, data, error } = await axios.post('/auth/login', {
|
||||
phone: number.value,
|
||||
type: 1
|
||||
})
|
||||
console.log('state',data);
|
||||
if (status == 200) {
|
||||
// this.token = data.accessToken
|
||||
// this.permissions = data.permissions
|
||||
// console.log(data);
|
||||
|
||||
return { status, data, error }
|
||||
}
|
||||
|
||||
}, async sendOtp(number, code) {
|
||||
const { status, data, error } = await axios.post('/auth/login-step-two', {
|
||||
phone: number.value,
|
||||
code: code.value
|
||||
})
|
||||
|
||||
console.log(data);
|
||||
if (status == 200) {
|
||||
console.log('first', this.token);
|
||||
this.token = data.token
|
||||
// this.permissions = data.permissions
|
||||
console.log('two', this.token);
|
||||
return { status, data, error }
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
@@ -0,0 +1,120 @@
|
||||
|
||||
import axios from '../plugins/axios'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCategoriesStore = defineStore('categories', {
|
||||
state: () => ({
|
||||
items: [],
|
||||
fetching: true,
|
||||
total: 0
|
||||
}),
|
||||
getters: {
|
||||
// treeSelect: (state) => (id = null) => {
|
||||
// if (id) {
|
||||
// const foreach = (items) => {
|
||||
// return items.filter((item) => {
|
||||
// if (item.key != id) {
|
||||
// if (item.children)
|
||||
// item.children = foreach(item.children)
|
||||
// return true
|
||||
// } else return false
|
||||
// })
|
||||
// }
|
||||
// return foreach(state.items)
|
||||
// }
|
||||
// return state.items
|
||||
// },
|
||||
find: (state) => (id) => {
|
||||
if (id) {
|
||||
const foreach = (items) => {
|
||||
const item = items.find(({ key }) => key == id)
|
||||
if (item) return item
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
|
||||
if (item.children) {
|
||||
const temp = foreach(item.children)
|
||||
if (temp) return temp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foreach(state.items)
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async index(page, rows) {
|
||||
this.fetching = true
|
||||
|
||||
const params = { page, rows }
|
||||
|
||||
const { status, data } = await axios.get('/courseCategory', { params })
|
||||
|
||||
this.fetching = false
|
||||
|
||||
if (status === 200) {
|
||||
const map = (items, parent = null) => {
|
||||
return items.map((item) => {
|
||||
const temp = {
|
||||
key: item._id,
|
||||
label: item.name,
|
||||
data: item
|
||||
}
|
||||
if (parent)
|
||||
temp.data.parentCategoryID = { [parent]: true }
|
||||
if (item.sub) {
|
||||
temp.children = map(item.sub, temp.key)
|
||||
delete temp.data.sub
|
||||
}
|
||||
return temp;
|
||||
});
|
||||
}
|
||||
|
||||
this.items = map(data)
|
||||
|
||||
// this.items = data.items
|
||||
// this.total = data.total
|
||||
}
|
||||
},
|
||||
async create(id) {
|
||||
const result = await axios.post('/courseCategory', {
|
||||
name: id.value
|
||||
})
|
||||
|
||||
//console.log('result', result);
|
||||
return result;
|
||||
},
|
||||
|
||||
async store(form) {
|
||||
const result = await axios.post('/admin/categories', form)
|
||||
|
||||
if (result.status === 201)
|
||||
this.items.unshift(result.data)
|
||||
|
||||
return result;
|
||||
},
|
||||
async update(id, form) {
|
||||
const result = await axios.put(`/admin/categories/${id}`, form)
|
||||
|
||||
if (result.status === 200) {
|
||||
const index = this.items.findIndex(({ _id }) => _id == id)
|
||||
this.items[index] = result.data
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
async remove(id) {
|
||||
const result = await axios.delete(`/courseCategory/${id}`)
|
||||
|
||||
if (result.status === 200) {
|
||||
const index = this.items.findIndex(({ _id }) => _id == id)
|
||||
this.items.splice(index, 1)
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
import axios from '../plugins/axios'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useDiscountStore = defineStore('discounts', {
|
||||
state: () => ({
|
||||
items: [],
|
||||
brands: [],
|
||||
categories: [],
|
||||
fetching: true,
|
||||
total: 0
|
||||
}),
|
||||
actions: {
|
||||
async index() {
|
||||
this.items = [];
|
||||
this.fetching = true
|
||||
const { status, data } = await axios.get('/admins/discount')
|
||||
this.fetching = false
|
||||
return this.items = data
|
||||
|
||||
},
|
||||
async getDiscount(id) {
|
||||
this.items = [];
|
||||
this.fetching = true
|
||||
const { status, data } = await axios.get(`/admins/discount/${id}`)
|
||||
this.fetching = false
|
||||
return this.items = data
|
||||
|
||||
},
|
||||
// async create(newCourse) {
|
||||
// const { status, data } = await axios.post('/course', newCourse)
|
||||
|
||||
// return data
|
||||
|
||||
// },
|
||||
// async delete(id) {
|
||||
// const { status, data } = await axios.delete(`/course/${id}`, id)
|
||||
|
||||
// return data
|
||||
|
||||
// },
|
||||
|
||||
// async edit(id) {
|
||||
// const { status, data } = await axios.get(`/course/${id}`)
|
||||
// return data
|
||||
|
||||
// },
|
||||
|
||||
|
||||
|
||||
// async store(form) {
|
||||
// const result = await axios.post('/admin/products/add', form)
|
||||
|
||||
// if (result.status === 201)
|
||||
// this.items.unshift(result.data)
|
||||
|
||||
// return result;
|
||||
// },
|
||||
// async update(id, form) {
|
||||
// const result = await axios.put(`/admin/products/${id}`, form)
|
||||
|
||||
// if (result.status === 200) {
|
||||
// const index = this.items.findIndex(({ _id }) => _id == id)
|
||||
// this.items[index] = result.data
|
||||
// }
|
||||
|
||||
// return result;
|
||||
// },
|
||||
// async confirm(id, form) {
|
||||
// const result = await axios.post(`/admin/products/confirm/${id}`, form)
|
||||
|
||||
// if (result.status === 200) {
|
||||
// const index = this.items.findIndex(({ _id }) => _id == id)
|
||||
// this.items[index] = result.data
|
||||
// }
|
||||
|
||||
// return result;
|
||||
// },
|
||||
// async remove(id) {
|
||||
// const result = await axios.delete(`/admin/products/${id}`)
|
||||
|
||||
// if (result.status === 200) {
|
||||
// const index = this.items.findIndex(({ _id }) => _id == id)
|
||||
// this.items.splice(index, 1)
|
||||
// }
|
||||
|
||||
// return result;
|
||||
// },
|
||||
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
import axios from '../plugins/axios'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useProductsStore = defineStore('products', {
|
||||
state: () => ({
|
||||
items: [],
|
||||
brands: [],
|
||||
categories: [],
|
||||
fetching: true,
|
||||
total: 0
|
||||
}),
|
||||
actions: {
|
||||
async index(page, pageSize) {
|
||||
this.items = [];
|
||||
|
||||
this.fetching = true
|
||||
|
||||
const params = { page, pageSize }
|
||||
|
||||
const { status, data } = await axios.get('/course', { params })
|
||||
|
||||
this.fetching = false
|
||||
return this.items = data.courses
|
||||
|
||||
},
|
||||
|
||||
async create(newCourse) {
|
||||
const { status, data } = await axios.post('/course', newCourse)
|
||||
|
||||
return data
|
||||
|
||||
},
|
||||
async delete(id) {
|
||||
const { status, data } = await axios.delete(`/course/${id}`, id)
|
||||
|
||||
return data
|
||||
|
||||
},
|
||||
|
||||
async edit(id) {
|
||||
const { status, data } = await axios.get(`/course/${id}`)
|
||||
return data
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
// async store(form) {
|
||||
// const result = await axios.post('/admin/products/add', form)
|
||||
|
||||
// if (result.status === 201)
|
||||
// this.items.unshift(result.data)
|
||||
|
||||
// return result;
|
||||
// },
|
||||
// async update(id, form) {
|
||||
// const result = await axios.put(`/admin/products/${id}`, form)
|
||||
|
||||
// if (result.status === 200) {
|
||||
// const index = this.items.findIndex(({ _id }) => _id == id)
|
||||
// this.items[index] = result.data
|
||||
// }
|
||||
|
||||
// return result;
|
||||
// },
|
||||
// async confirm(id, form) {
|
||||
// const result = await axios.post(`/admin/products/confirm/${id}`, form)
|
||||
|
||||
// if (result.status === 200) {
|
||||
// const index = this.items.findIndex(({ _id }) => _id == id)
|
||||
// this.items[index] = result.data
|
||||
// }
|
||||
|
||||
// return result;
|
||||
// },
|
||||
// async remove(id) {
|
||||
// const result = await axios.delete(`/admin/products/${id}`)
|
||||
|
||||
// if (result.status === 200) {
|
||||
// const index = this.items.findIndex(({ _id }) => _id == id)
|
||||
// this.items.splice(index, 1)
|
||||
// }
|
||||
|
||||
// return result;
|
||||
// },
|
||||
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,117 @@
|
||||
|
||||
import axios from '../plugins/axios'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useStaticsStore = defineStore('statics', {
|
||||
state: () => ({
|
||||
items: [],
|
||||
fetching: true,
|
||||
total: 0
|
||||
}),
|
||||
getters: {
|
||||
// treeSelect: (state) => (id = null) => {
|
||||
// if (id) {
|
||||
// const foreach = (items) => {
|
||||
// return items.filter((item) => {
|
||||
// if (item.key != id) {
|
||||
// if (item.children)
|
||||
// item.children = foreach(item.children)
|
||||
// return true
|
||||
// } else return false
|
||||
// })
|
||||
// }
|
||||
// return foreach(state.items)
|
||||
// }
|
||||
// return state.items
|
||||
// },
|
||||
find: (state) => (id) => {
|
||||
if (id) {
|
||||
const foreach = (items) => {
|
||||
const item = items.find(({ key }) => key == id)
|
||||
if (item) return item
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
|
||||
if (item.children) {
|
||||
const temp = foreach(item.children)
|
||||
if (temp) return temp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foreach(state.items)
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async index() {
|
||||
this.fetching = true
|
||||
const { status, data } = await axios.get('/admin/settings')
|
||||
this.fetching = false
|
||||
if (status === 200) {
|
||||
this.items = data
|
||||
// this.total = data.total
|
||||
}
|
||||
},
|
||||
async edit(data) {
|
||||
this.fetching = true
|
||||
// const json = {
|
||||
// "about": "",
|
||||
// "student": 100,
|
||||
// "hours": 0,
|
||||
// "title": 0,
|
||||
// "expertise": 0,
|
||||
// "nameCompany": "string",
|
||||
// "mission": "string",
|
||||
// "start": "string",
|
||||
// "goals": "string"
|
||||
// }
|
||||
const result = await axios.patch('/admin/settings/edit', data)
|
||||
this.fetching = false
|
||||
return result
|
||||
// if (status === 200) {
|
||||
// this.items = data
|
||||
// // this.total = data.total
|
||||
// }
|
||||
},
|
||||
async create(id) {
|
||||
const result = await axios.post('/courseCategory', {
|
||||
name: id.value
|
||||
})
|
||||
|
||||
//console.log('result', result);
|
||||
return result;
|
||||
},
|
||||
|
||||
async store(form) {
|
||||
const result = await axios.post('/admin/categories', form)
|
||||
|
||||
if (result.status === 201)
|
||||
this.items.unshift(result.data)
|
||||
|
||||
return result;
|
||||
},
|
||||
async update(id, form) {
|
||||
const result = await axios.put(`/admin/categories/${id}`, form)
|
||||
|
||||
if (result.status === 200) {
|
||||
const index = this.items.findIndex(({ _id }) => _id == id)
|
||||
this.items[index] = result.data
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
async remove(id) {
|
||||
const result = await axios.delete(`/courseCategory/${id}`)
|
||||
|
||||
if (result.status === 200) {
|
||||
const index = this.items.findIndex(({ _id }) => _id == id)
|
||||
this.items.splice(index, 1)
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
import axios from '../plugins/axios'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useUploaderStore = defineStore('uploader', {
|
||||
state: () => ({
|
||||
|
||||
}),
|
||||
|
||||
|
||||
actions: {
|
||||
async uploader(file) {
|
||||
|
||||
const form = new FormData()
|
||||
|
||||
form.append('directoryName', 'directoryName')
|
||||
form.append('file', new Blob([file]))
|
||||
// for (var pair of form.entries()) {
|
||||
// console.log('pairs :',pair[0]+ ', ' + pair[1]);
|
||||
// }
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
};
|
||||
try {
|
||||
const result = await axios.post('/uploader',
|
||||
form,
|
||||
config
|
||||
);
|
||||
console.log('result', result);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.log('err', error);
|
||||
return error;
|
||||
}
|
||||
|
||||
|
||||
// const result = await axios.post('/uploader', form,
|
||||
// {
|
||||
// headers: { "Content-Type": "multipart/form-data" }
|
||||
// }
|
||||
// )
|
||||
// console.log(result);
|
||||
// return result
|
||||
// const result = await axios({
|
||||
// method: "post",
|
||||
// url: "/uploader",
|
||||
// data: form,
|
||||
// headers: { "Content-Type": "multipart/form-data" },
|
||||
// })
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// .then(function (response) {
|
||||
|
||||
// console.log(response);
|
||||
// })
|
||||
// .catch(function (response) {
|
||||
|
||||
// console.log(response);
|
||||
// });
|
||||
// console.log('result', result);
|
||||
// return result;
|
||||
},
|
||||
|
||||
|
||||
|
||||
async bulk(files) {
|
||||
const form = new FormData()
|
||||
files.forEach(file => form.append('images', file))
|
||||
|
||||
const params = { type: 1 }
|
||||
|
||||
const result = await axios.post('/images/admin/upload', form, { params })
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
import axios from '../plugins/axios'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useUsersStore = defineStore('users', {
|
||||
state: () => ({
|
||||
items: [],
|
||||
fetching: true,
|
||||
total: 0
|
||||
}),
|
||||
actions: {
|
||||
async index(page, pageSize) {
|
||||
this.items = [];
|
||||
this.fetching = true
|
||||
|
||||
const params = { page, pageSize }
|
||||
const { status, data } = await axios.get('/admin/user/list', { params })
|
||||
|
||||
this.fetching = false
|
||||
this.total = 30
|
||||
this.items = data;
|
||||
|
||||
},
|
||||
async confirm(item, value) {
|
||||
item.confirming = true
|
||||
const result = await axios.put(`/admin/users/seller/${item.id}`, { confirmToSales: value })
|
||||
item.confirming = false
|
||||
|
||||
if (result.status === 200) {
|
||||
const index = this.items.findIndex(({ _id }) => _id == item.id)
|
||||
if (index >= 0)
|
||||
this.items.splice(index, 1)
|
||||
else
|
||||
this.items.unshift(item)
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
export const formatSize = (bytes) => {
|
||||
const k = 1024;
|
||||
const dm = 1;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
if (bytes === 0) {
|
||||
return `0 ${sizes[0]}`;
|
||||
}
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
const formattedSize = parseFloat((bytes / Math.pow(k, i)).toFixed(dm));
|
||||
|
||||
return `${formattedSize} ${sizes[i]}`;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const jDate = (d) => new Date(d).toLocaleDateString('fa-IR')
|
||||
@@ -0,0 +1 @@
|
||||
export const numberFormat = (number) => new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(number)
|
||||
@@ -0,0 +1,10 @@
|
||||
export const treeTogglerRTL = (selector) => {
|
||||
try {
|
||||
document.querySelectorAll(selector).forEach(element => {
|
||||
element.style['margin-right'] = element.style['margin-left']
|
||||
element.style['margin-left'] = null
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<section class="flex flex-col gap-4">
|
||||
<Panel :header="$t('اطلاعات صفحه ی درباره ما')">
|
||||
<div class="grid grid-cols-1 gap-7">
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="data.title" />
|
||||
<label>{{ $t("متن هدر") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<Textarea v-model="data.description" rows="5" class="w-full" />
|
||||
<label>{{ $t("توضیحات هدر") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-7 mt-7">
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText v-model="data.student" class="w-full" />
|
||||
<label>{{ $t("تعداد دانشجو ها") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="data.videosCount" />
|
||||
<label>{{ $t("تعداد ویدیو های آموزشی") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="data.rates" />
|
||||
<label>{{ $t("امتیاز دانشجو ها") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-7 text-lg">درباره ما :</p>
|
||||
<div class="mt-2">
|
||||
<FloatLabel>
|
||||
<Editor v-model="data.about" editorStyle="height: 320px">
|
||||
<template v-slot:toolbar>
|
||||
<span class="ql-formats text-start justify-center w-full flex">
|
||||
<button v-tooltip.bottom="'Bold'" class="ql-bold"></button>
|
||||
<button v-tooltip.bottom="'Italic'" class="ql-italic"></button>
|
||||
<button
|
||||
v-tooltip.bottom="'Underline'"
|
||||
class="ql-underline"
|
||||
></button>
|
||||
</span>
|
||||
</template>
|
||||
</Editor>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="flex pt-4 justify-end">
|
||||
<Button
|
||||
:label="$t('edit')"
|
||||
icon="pi pi-save"
|
||||
severity="success"
|
||||
:loading="saving"
|
||||
@click="save(data)"
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { jDate } from "@/utils/jDate";
|
||||
import { treeTogglerRTL } from "@/utils/tree-toggler-rtl";
|
||||
import {
|
||||
inject,
|
||||
defineAsyncComponent,
|
||||
watchEffect,
|
||||
ref,
|
||||
watch,
|
||||
nextTick,
|
||||
reactive,
|
||||
} from "vue";
|
||||
import { useStaticsStore } from "@/stores/statics";
|
||||
import { useRoute } from "vue-router";
|
||||
const data = reactive({
|
||||
about: "string",
|
||||
student: 0,
|
||||
hours: 0,
|
||||
title: 0,
|
||||
expertise: 0,
|
||||
nameCompany: "string",
|
||||
mission: "string",
|
||||
start: "string",
|
||||
goals: "string",
|
||||
// description: "",
|
||||
// videosCount: null,
|
||||
// rates: null,
|
||||
});
|
||||
const save = async (data) => {
|
||||
console.log('form', data);
|
||||
console.log('json', JSON.stringify(data));
|
||||
const { result } = await store.edit(data);
|
||||
console.log("res", result);
|
||||
};
|
||||
const expandedKeys = ref({});
|
||||
|
||||
const CategoryForm = defineAsyncComponent(() =>
|
||||
import("../components/CategoryForm.vue")
|
||||
);
|
||||
|
||||
const { dialog, confirm, toast, t } = inject("service");
|
||||
|
||||
const store = useStaticsStore();
|
||||
|
||||
watchEffect(async () => {
|
||||
await store.index();
|
||||
// console.log(store.items);
|
||||
data.student = store.items.student;
|
||||
//data.videosCount = store.items.hours;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
|
||||
ادمین ها
|
||||
|
||||
<input id="avatar" ref="files" name="file" type="file" @change="change">
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
import { useUploaderStore } from "@/stores/uploader";
|
||||
const avatar = document.querySelector("#avatar");
|
||||
const store = useUploaderStore()
|
||||
//const files = ref(null)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const change = async (avatar) => {
|
||||
|
||||
const data = await store.uploader(avatar.target.value)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<Panel :header="$t('categories')">
|
||||
<template #icons>
|
||||
<Button icon="pi pi-plus" :label="$t('add')" severity="success" @click="create()" />
|
||||
</template>
|
||||
<TreeTable :value="store.items" v-model:expandedKeys="expandedKeys">
|
||||
|
||||
|
||||
<Column field="name" :header="$t('name')" />
|
||||
|
||||
<!-- <Column :header="$t('image')">
|
||||
<template #body="{ node: { data: { headerImage } } }">
|
||||
<Image :src="headerImage" imageClass="rounded h-10" class="align-middle" preview
|
||||
:pt="{ toolbar: 'flex-row-reverse' }" />
|
||||
</template>
|
||||
</Column> -->
|
||||
<Column field="id" :header="$t('id')" />
|
||||
|
||||
|
||||
|
||||
|
||||
<Column :field="({ createdAt }) => jDate(createdAt)" :header="$t('createdAt')" />
|
||||
|
||||
|
||||
<Column headerClass="w-32" :header="$t('options')">
|
||||
<template #body="{ node: { data } }">
|
||||
<div class="flex justify-start">
|
||||
<!-- <Button icon="pi pi-pencil" rounded text severity="secondary" @click="edit(data)" /> -->
|
||||
<Button icon="pi pi-trash" rounded text severity="danger" @click="remove(data)"
|
||||
:loading="data.removing" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<template #empty>
|
||||
<p>{{ store.fetching ? $t('loading') : $t('emptyTable') }}</p>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Pagination :total="store.total" />
|
||||
</template>
|
||||
</TreeTable>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { jDate } from '@/utils/jDate';
|
||||
import { treeTogglerRTL } from '@/utils/tree-toggler-rtl';
|
||||
import { inject, defineAsyncComponent, watchEffect, ref, watch, nextTick } from 'vue';
|
||||
import { useCategoriesStore } from '@/stores/categoreis';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const expandedKeys = ref({});
|
||||
|
||||
const CategoryForm = defineAsyncComponent(() => import('../components/CategoryForm.vue'));
|
||||
|
||||
const { dialog, confirm, toast, t } = inject('service')
|
||||
|
||||
const store = useCategoriesStore()
|
||||
|
||||
const create = () => {
|
||||
dialog.open(CategoryForm, {
|
||||
props: { modal: true, closable: false, header: t('createNewCategory') },
|
||||
data: { categories: 'store.items' }
|
||||
});
|
||||
};
|
||||
|
||||
const edit = (category) => {
|
||||
dialog.open(CategoryForm, {
|
||||
props: { modal: true, closable: false, header: t('editCategory') },
|
||||
data: { categories: store.items, category }
|
||||
});
|
||||
};
|
||||
|
||||
const remove = (category) => {
|
||||
// console.log(category.id);
|
||||
confirm.require({
|
||||
message: t('Are you sure you want to proceed?'),
|
||||
header: t('Danger Zone'),
|
||||
icon: 'pi pi-info-circle',
|
||||
acceptClass: 'p-button-danger p-button-sm',
|
||||
rejectClass: 'p-button-secondary p-button-outlined p-button-sm',
|
||||
rejectLabel: t('cancel'),
|
||||
acceptLabel: t('beOmitted'),
|
||||
defaultFocus: 'reject',
|
||||
accept: async () => {
|
||||
category.removing = true
|
||||
|
||||
const { status, data } = await store.remove(category.id);
|
||||
|
||||
category.removing = false
|
||||
|
||||
if (status === 200)
|
||||
toast.add({
|
||||
severity: 'success', summary: t('successful'), detail: t('destroySuccessfully'), life: 3000
|
||||
});
|
||||
else
|
||||
toast.add({
|
||||
severity: 'error', summary: t('error'), detail: data.msg, life: 3000
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
watchEffect(() => {
|
||||
const { page = 1, rows = 10 } = route.query
|
||||
store.index(page, rows)
|
||||
})
|
||||
watch(store , () =>{
|
||||
store.index()
|
||||
})
|
||||
|
||||
watch(expandedKeys, () => nextTick(() => treeTogglerRTL('button.p-treetable-toggler')))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<Panel :header="$t('coupons')">
|
||||
<DataTable :value="store.items">
|
||||
|
||||
<Column field="code" :header="$t('کد تخفیف')" />
|
||||
<Column field="id" :header="$t('id')" />
|
||||
<Column field="admin.name" :header="$t('ایجاد شده توسط')" />
|
||||
<Column :field="({ createdAt }) => jDate(createdAt)" :header="$t('createdAt')" />
|
||||
<Column :field="({ startDate }) => jDate(startDate)" :header="$t('تاریخ شروع')" />
|
||||
<Column :field="({ endDate }) => jDate(endDate)" :header="$t('تاریخ انقضا')" />
|
||||
<Column field="status" :header="$t('status')" />
|
||||
|
||||
|
||||
<Column headerClass="w-32">
|
||||
<template #body="{ data }">
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
icon="pi pi-pencil"
|
||||
rounded
|
||||
text
|
||||
severity="secondary"
|
||||
@click="edit(data)"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
rounded
|
||||
text
|
||||
severity="secondary"
|
||||
@click="show(data)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<template #empty>
|
||||
<p>{{ store.fetching ? $t("loading") : $t("emptyTable") }}</p>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<!--
|
||||
<Paginator :rows="store.total" :totalRecords="store.total" :rowsPerPageOptions="[5, 20, 30]"></Paginator>
|
||||
-->
|
||||
|
||||
<Pagination :total="store.total" />
|
||||
</template>
|
||||
</DataTable>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {useDiscountStore} from "../stores/discounts"
|
||||
import { jDate } from "@/utils/jDate";
|
||||
import { defineAsyncComponent, inject, onBeforeMount, ref, watchEffect } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const { toast, dialog, confirm, t } = inject('service')
|
||||
const CouponDetails = defineAsyncComponent(() => import('../components/CouponDetails.vue'));
|
||||
|
||||
const store = useDiscountStore()
|
||||
|
||||
const show = (coupon) => {
|
||||
dialog.open(CouponDetails, {
|
||||
props: { modal: true, closable: true, header: t('جزئیات تخفیف') },
|
||||
data: {
|
||||
coupon
|
||||
}
|
||||
});
|
||||
}
|
||||
const router = useRouter()
|
||||
const edit = (data) =>{
|
||||
console.log(data.id);
|
||||
router.push(`/coupon/${data.id}`)
|
||||
}
|
||||
watchEffect( async() =>{
|
||||
await store.index()
|
||||
console.log('data', data);
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<Panel :header="$t('courses')">
|
||||
<template #icons>
|
||||
<Button
|
||||
icon="pi pi-plus"
|
||||
:label="$t('add')"
|
||||
severity="success"
|
||||
@click="create()"
|
||||
/>
|
||||
</template>
|
||||
<DataTable :value="store.items">
|
||||
<Column class="w-16" :header="$t('image')">
|
||||
<template #body="{ data: { image } }">
|
||||
<Avatar
|
||||
:image="image"
|
||||
size="large"
|
||||
shape="circle"
|
||||
class="transition-transform hover:scale-150"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="name" :header="$t('name')" />
|
||||
<Column
|
||||
:field="({ categories }) => categories[0].name"
|
||||
:header="$t('category')"
|
||||
/>
|
||||
|
||||
<Column field="price" :header="$t('price')" />
|
||||
<Column
|
||||
:field="({ createdAt }) => jDate(createdAt)"
|
||||
:header="$t('createdAt')"
|
||||
class="w-28"
|
||||
/>
|
||||
<Column
|
||||
:field="({ updatedAt }) => jDate(updatedAt)"
|
||||
:header="$t('updatedAt')"
|
||||
class="w-32 text-center"
|
||||
/>
|
||||
<Column field="averageRating" :header="$t('امتیاز')" />
|
||||
|
||||
<Column :header="$t('options')" headerClass="w-32">
|
||||
<template #body="{ data }">
|
||||
<div class="flex justify-end">
|
||||
<Button icon="pi pi-eye" rounded text severity="secondary" @click="show(data)" />
|
||||
<Button
|
||||
icon="pi pi-pencil"
|
||||
rounded
|
||||
text
|
||||
severity="secondary"
|
||||
@click="edit(data)"
|
||||
/>
|
||||
|
||||
<Button
|
||||
@click="remove(data)"
|
||||
icon="pi pi-trash"
|
||||
rounded
|
||||
text
|
||||
severity="danger"
|
||||
:loading="data.removing"
|
||||
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<!-- <Column :field="({ price }) => numberFormat(price)" :header="$t('price')" />
|
||||
<Column :header="$t('category')" class="whitespace-nowrap">
|
||||
<template #body="{ data: { category } }">
|
||||
<span v-if="category.length > 0">
|
||||
{{ category.slice(-1)[0].name }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="name" :header="$t('reviews')">
|
||||
<template #body="{ data: { name } }">
|
||||
<Rating :modelValue="name" readonly :cancel="false" class="[&_svg]:text-yellow-400" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column :header="$t('status')">
|
||||
<template #body="{ data: { confirmToShow, _id } }">
|
||||
<Tag v-if="!confirmToShow" value="غیر فعال" severity="danger" @click="accepter(_id,true)"/>
|
||||
<Tag v-if="confirmToShow" value="فعال" severity="success" @click="accepter(_id,false)"/>
|
||||
</template>
|
||||
|
||||
</Column>
|
||||
<Column :field="({ createdAt }) => jDate(createdAt)" :header="$t('createdAt')" class="w-28" />
|
||||
<Column :field="({ updatedAt }) => jDate(updatedAt)" :header="$t('updatedAt')" class="w-32 text-center" />
|
||||
<Column headerClass="w-32">
|
||||
<template #body="{ data }">
|
||||
<div class="flex justify-end">
|
||||
<Button icon="pi pi-eye" rounded text severity="secondary" @click="show(data)" />
|
||||
<Button icon="pi pi-pencil" rounded text severity="secondary" @click="edit(data)" />
|
||||
<Button icon="pi pi-trash" rounded text severity="danger" :loading="data.removing"
|
||||
@click="remove(data)" />
|
||||
</div>
|
||||
</template>
|
||||
</Column> -->
|
||||
<template #empty>
|
||||
<p>{{ store.fetching ? $t("loading") : $t("emptyTable") }}</p>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Pagination :total="store.total" />
|
||||
</template>
|
||||
</DataTable>
|
||||
<Galleria
|
||||
v-model:visible="galleryVes"
|
||||
:value="gallery"
|
||||
:numVisible="5"
|
||||
containerClass="w-1/2"
|
||||
circular
|
||||
fullScreen
|
||||
showItemNavigators
|
||||
:showThumbnailNavigators="false"
|
||||
>
|
||||
<template #item="{ item: { itemImageSrc, alt } }">
|
||||
<img :src="itemImageSrc" :alt="alt" class="w-full block" />
|
||||
</template>
|
||||
</Galleria>
|
||||
<Column headerClass="w-32">
|
||||
<template #body="{ data }">
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
rounded
|
||||
text
|
||||
severity="secondary"
|
||||
@click="show(data)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useProductsStore } from '@/stores/products';
|
||||
import { jDate } from '@/utils/jDate';
|
||||
import { numberFormat } from '@/utils/numberFormat';
|
||||
import { defineAsyncComponent, inject, onBeforeMount, ref, watchEffect } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const ProductForm = defineAsyncComponent(() => import('../components/ProductForm.vue'));
|
||||
const ProductDetails = defineAsyncComponent(() => import('../components/ProductDetails.vue'));
|
||||
|
||||
const { toast, dialog, confirm, t } = inject('service')
|
||||
|
||||
var gallery = []
|
||||
const galleryVes = ref(false)
|
||||
const openGallery = (images =>{
|
||||
gallery = []
|
||||
images.map(image =>{
|
||||
gallery.push({itemImageSrc:image, alt:"product"})
|
||||
})
|
||||
galleryVes.value = true
|
||||
|
||||
})
|
||||
const show = (product) => {
|
||||
dialog.open(ProductDetails, {
|
||||
props: { modal: true, closable: true, header: t('productDetails') },
|
||||
data: {
|
||||
product
|
||||
}
|
||||
});
|
||||
}
|
||||
const UserDetails = defineAsyncComponent(() => import('../components/UserDetails.vue'));
|
||||
|
||||
const store = useProductsStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const edit = (data) =>{
|
||||
router.push(data._id)
|
||||
}
|
||||
|
||||
|
||||
const remove = (data) => {
|
||||
console.log('done', data._id)
|
||||
confirm.require({
|
||||
message: t('Are you sure you want to proceed?'),
|
||||
header: t('Danger Zone'),
|
||||
icon: 'pi pi-info-circle',
|
||||
acceptClass: 'p-button-danger p-button-sm',
|
||||
rejectClass: 'p-button-secondary p-button-outlined p-button-sm',
|
||||
rejectLabel: t('cancel'),
|
||||
acceptLabel: t('beOmitted'),
|
||||
defaultFocus: 'reject',
|
||||
accept: async () => {
|
||||
const deleted = await store.delete(data._id)
|
||||
console.log(deleted);
|
||||
await store.index()
|
||||
}
|
||||
// product.removing = true
|
||||
|
||||
// const { status, data } = await store.remove(product._id);
|
||||
|
||||
// product.removing = false
|
||||
|
||||
// if (status === 200)
|
||||
// toast.add({
|
||||
// severity: 'success', summary: t('successful'), detail: t('destroySuccessfully'), life: 3000
|
||||
// });
|
||||
// else
|
||||
// toast.add({
|
||||
// severity: 'error', summary: t('error'), detail: data.msg, life: 3000
|
||||
// });
|
||||
|
||||
// },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
watchEffect(async() => {
|
||||
const { page = 1, pageSize = 10 } = route.query
|
||||
await store.index(page, pageSize)
|
||||
console.log(store.items[2].image);
|
||||
console.log(String.fromCharCode(store.items[2].image)).join('');
|
||||
// store.items.forEach((product) => {
|
||||
// product.status = { value: t('active'), severity: 'success' };
|
||||
// })
|
||||
|
||||
})
|
||||
|
||||
const accepter = async (id,confirmToShow)=>{
|
||||
const { status, data } = await store.confirm(id, {confirmToShow});
|
||||
|
||||
|
||||
if (status === 200)
|
||||
toast.add({
|
||||
severity: 'success', summary: t('successful'), detail: t('destroySuccessfully'), life: 3000
|
||||
});
|
||||
else
|
||||
toast.add({
|
||||
severity: 'error', summary: t('error'), detail: data.msg, life: 3000
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,350 @@
|
||||
<template>
|
||||
<section class="flex flex-col gap-4">
|
||||
<Panel :header="$t('createCourse')">
|
||||
<div class="grid grid-cols-3 gap-7">
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="newCourse.name" />
|
||||
<label>{{ $t("title") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="newCourse.language" />
|
||||
<label>{{ $t("زبان آموزش") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText v-model="newCourse.duration" class="w-full" />
|
||||
<label>{{ $t("مدت زمان دوره") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<Dropdown
|
||||
:options="levels"
|
||||
:optionLabel="levels"
|
||||
:optionValue="levels"
|
||||
v-model="newCourse.level"
|
||||
class="w-full"
|
||||
/>
|
||||
<label>{{ $t("سطح دوره") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="newCourse.teacher" />
|
||||
<label>{{ $t("مدرس دوره") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="newCourse.categories" />
|
||||
<label>{{ $t("دسته بندی") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText
|
||||
class="w-full"
|
||||
v-model="newCourse.completionPercentage"
|
||||
/>
|
||||
<label>{{ $t("درصد تکمیل دوره") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="newCourse.videosCount" />
|
||||
<label>{{ $t("تعداد ویدیو ها") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="newCourse.totalFileSize" />
|
||||
<label>{{ $t("حجم کل دوره") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full mt-7">
|
||||
<div class="basis-4/6">
|
||||
<FloatLabel>
|
||||
<Editor
|
||||
v-model="newCourse.description"
|
||||
placeholder="توضیحات"
|
||||
editorStyle="height: 320px"
|
||||
/>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="basis-2/6 ms-10">
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputGroup>
|
||||
<InputText class="pointer-events-none truncate" dir="ltr" />
|
||||
<Button severity="secondary">
|
||||
<i class="pi pi-image ml-2"></i>
|
||||
<span>{{ $t("chooseImage") }}</span>
|
||||
<input
|
||||
type="file"
|
||||
class="opacity-0 absolute inset-0 cursor-pointer"
|
||||
accept="image/*"
|
||||
@input="chooseImage"
|
||||
/>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
<label>{{ $t("image") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="card flex flex-col gap-2 justify-center mt-7">
|
||||
<Listbox
|
||||
listStyle="height:240px"
|
||||
emptyMessage="دسته بندی وجود ندارد"
|
||||
emptyFilterMessage="دسته بندی وجود ندارد"
|
||||
v-model="selectedCity"
|
||||
filterPlaceholder=" دسته بندی"
|
||||
:options="cities"
|
||||
filter
|
||||
optionLabel="name"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div class="flex flex-col gap-1">
|
||||
<InputGroup>
|
||||
<FloatLabel>
|
||||
<Dropdown
|
||||
:options="cities"
|
||||
:optionLabel="cities.name"
|
||||
:optionValue="cities.name"
|
||||
class="w-full"
|
||||
>
|
||||
</Dropdown>
|
||||
<label>{{ $t("brand") }}</label>
|
||||
</FloatLabel>
|
||||
</InputGroup>
|
||||
|
||||
</div> -->
|
||||
|
||||
<div class="flex pt-4 justify-end">
|
||||
<Button
|
||||
:label="$t('create')"
|
||||
icon="pi pi-save"
|
||||
severity="success"
|
||||
:loading="saving"
|
||||
@click="create(newCourse)"
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { jDate } from "@/utils/jDate";
|
||||
import { treeTogglerRTL } from "@/utils/tree-toggler-rtl";
|
||||
|
||||
import {
|
||||
inject,
|
||||
defineAsyncComponent,
|
||||
watchEffect,
|
||||
ref,
|
||||
watch,
|
||||
nextTick,
|
||||
reactive,
|
||||
} from "vue";
|
||||
|
||||
import { useStaticsStore } from "@/stores/statics";
|
||||
import { useUploaderStore } from "@/stores/uploader";
|
||||
import { useProductsStore } from "@/stores/products";
|
||||
const courseStore = useProductsStore()
|
||||
const uploadStore = useUploaderStore();
|
||||
import { useRoute } from "vue-router";
|
||||
const selectedCity = ref();
|
||||
const levels = reactive(["آسان", "متوسط", "سخت"]);
|
||||
const newCourse = reactive({
|
||||
name: "",
|
||||
description: "",
|
||||
slug: "",
|
||||
duration: "",
|
||||
image: "",
|
||||
categories: "",
|
||||
price: 0,
|
||||
teacher: "",
|
||||
completionPercentage: 0,
|
||||
level: "",
|
||||
language: "",
|
||||
countOfVideos: 0,
|
||||
totalFileSize: "",
|
||||
});
|
||||
const chooseImage = async (el) => {
|
||||
const uploader = await uploadStore.uploader(el.target.value);
|
||||
//console.log('uploader', uploader.data.url);
|
||||
newCourse.image = uploader.data.url;
|
||||
console.log("تصویر آپلود شد");
|
||||
console.log('upldr',uploader);
|
||||
};
|
||||
const create = async (data) => {
|
||||
if (newCourse.image === "") {
|
||||
return console.log("تصویر دوره وارد نشده است");
|
||||
}
|
||||
if (
|
||||
newCourse.name === "" ||
|
||||
newCourse.description === "" ||
|
||||
// newCourse.slug === "" ||
|
||||
newCourse.duration === "" ||
|
||||
// newCourse.categories === "" ||
|
||||
// newCourse.teacher === "" ||
|
||||
newCourse.completionPercentage === 0 ||
|
||||
// newCourse.level === "" ||
|
||||
// newCourse.language === "" ||
|
||||
newCourse.countOfVideos === "" ||
|
||||
newCourse.totalFileSize === ""
|
||||
) {
|
||||
return console.log("تمام مقادیر را وارد کنید");
|
||||
}
|
||||
newCourse.name = data.name;
|
||||
newCourse.description = data.description;
|
||||
newCourse.slug = data.name;
|
||||
newCourse.duration = data.duration;
|
||||
// newCourse.categories = data.categories;
|
||||
newCourse.categories = "66587ae698d0a0db14693a5e"
|
||||
// newCourse.teacher = data.teacher;
|
||||
newCourse.teacher = "66530b0480ef484e2bd19220"
|
||||
// newCourse.completionPercentage = data.completionPercentage;
|
||||
newCourse.completionPercentage = 20;
|
||||
// newCourse.level = data.level;
|
||||
newCourse.level = "متوسط"
|
||||
// newCourse.language = data.language;
|
||||
newCourse.language = "فارسی"
|
||||
newCourse.countOfVideos = data.countOfVideos;
|
||||
newCourse.totalFileSize = data.totalFileSize;
|
||||
// console.log("newCourse: ", newCourse);
|
||||
const creat = await courseStore.create(newCourse)
|
||||
console.log('created', creat);
|
||||
};
|
||||
// console.log("form", data);
|
||||
// console.log("json", JSON.stringify(data));
|
||||
// const { result } = await store.edit(data);
|
||||
// console.log("res", result);
|
||||
// };
|
||||
const expandedKeys = ref({});
|
||||
|
||||
const CategoryForm = defineAsyncComponent(() =>
|
||||
import("../components/CategoryForm.vue")
|
||||
);
|
||||
|
||||
const { dialog, confirm, toast, t } = inject("service");
|
||||
|
||||
const store = useStaticsStore();
|
||||
|
||||
watchEffect(async () => {
|
||||
await store.index();
|
||||
// console.log(store.items);
|
||||
|
||||
//data.videosCount = store.items.hours;
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--
|
||||
<script setup>
|
||||
import { useUploaderStore } from "@/stores/uploader";
|
||||
import FirstStep from "../components/ProductFormSteps/FirstStep.vue";
|
||||
import SecondStep from "../components/ProductFormSteps/SecondStep.vue";
|
||||
import ThirdStep from "../components/ProductFormSteps/ThirdStep.vue";
|
||||
|
||||
import { inject, provide, reactive, ref } from "vue";
|
||||
import { useProductsStore } from "@/stores/products";
|
||||
|
||||
const dialog = inject("dialogRef");
|
||||
const { toast, t } = inject("service");
|
||||
// const { product } = dialog.data
|
||||
|
||||
//const form = reactive(Object.assign({}, product || {}));
|
||||
//console.log(form);
|
||||
const errors = reactive({});
|
||||
|
||||
const saving = ref(false);
|
||||
|
||||
provide("data", { errors, saving });
|
||||
|
||||
const store = useProductsStore();
|
||||
|
||||
const uploader = useUploaderStore();
|
||||
|
||||
const save = async () => {
|
||||
saving.value = true;
|
||||
|
||||
console.log(form);
|
||||
|
||||
//Upload File
|
||||
if (typeof form.coverImage === "object") {
|
||||
const { status, data } = await uploader.single(form.coverImage);
|
||||
|
||||
if (status === 201) form.coverImage = data.url;
|
||||
else
|
||||
return toast.add({
|
||||
life: 2000,
|
||||
severity: "error",
|
||||
summary: t("error"),
|
||||
detail: data.msg,
|
||||
});
|
||||
}
|
||||
|
||||
//upload images
|
||||
const files = form.images.filter((image) => typeof image === "object");
|
||||
if (files.length > 0) {
|
||||
const { status, data } = await uploader.bulk(files);
|
||||
|
||||
if (status === 201)
|
||||
form.images = form.images.map((image) => {
|
||||
if (typeof image === "object") return data.urls.shift();
|
||||
return image;
|
||||
});
|
||||
else
|
||||
return toast.add({
|
||||
life: 2000,
|
||||
severity: "error",
|
||||
summary: t("error"),
|
||||
detail: data.msg,
|
||||
});
|
||||
}
|
||||
|
||||
//Create or Update
|
||||
// const { status, data } = !!product
|
||||
// ? await store.update(product._id, form)
|
||||
// : await store.store(form);
|
||||
|
||||
// saving.value = false;
|
||||
|
||||
// if (status === 200 || status === 201) {
|
||||
// toast.add({
|
||||
// life: 2000,
|
||||
// severity: "success",
|
||||
// summary: t("successful"),
|
||||
// detail: t(!!category ? "updateSuccessfully" : "storeSuccessfully"),
|
||||
// });
|
||||
// dialog.value.close();
|
||||
// } else if (status === 422) Object.assign(errors, data);
|
||||
// else
|
||||
// toast.add({
|
||||
// severity: "error",
|
||||
// life: 2000,
|
||||
// detail: data.msg,
|
||||
// summary: t("error"),
|
||||
// });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style> -->
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<section class="flex flex-col gap-4">
|
||||
<Panel :header="$t('ایجاد تخفیف جدید')">
|
||||
<div class="grid grid-cols-3 gap-7">
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full custom-input" v-model="date" />
|
||||
<label>{{ $t("تاریخ شروع") }}</label>
|
||||
</FloatLabel>
|
||||
<date-picker
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
display-format="jYYYY-jMM-jDD HH:mm"
|
||||
:timezone="true"
|
||||
color="dimgray"
|
||||
v-model="date"
|
||||
simple
|
||||
custom-input=".custom-input"
|
||||
></date-picker>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full custom-input2" v-model="date2" />
|
||||
<label>{{ $t("تاریخ پایان") }}</label>
|
||||
</FloatLabel>
|
||||
<date-picker
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
display-format="jYYYY-jMM-jDD HH:mm"
|
||||
:timezone="true"
|
||||
color="dimgray"
|
||||
v-model="date2"
|
||||
simple
|
||||
custom-input=".custom-input2"
|
||||
></date-picker>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="course.duration" />
|
||||
<label>{{ $t("کد تخفیف") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<Dropdown
|
||||
v-model="noe"
|
||||
:options="['مبلغ', 'درصد']"
|
||||
:optionLabel="levels"
|
||||
:optionValue="levels"
|
||||
class="w-full"
|
||||
/>
|
||||
<label>{{ $t("نوع تخفیف") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div v-if="noe === 'مبلغ'" class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t("مبلغ تخفیف") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div v-if="noe === 'درصد'" class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t("درصد تخفیف") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 my-custom-container inline">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t("تخفیف برای همه کاربران") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex pt-4 justify-end">
|
||||
<Button
|
||||
:label="$t('save')"
|
||||
icon="pi pi-save"
|
||||
severity="success"
|
||||
:loading="saving"
|
||||
@click="create(course)"
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useProductsStore } from "@/stores/products";
|
||||
import { ref, watchEffect, reactive } from "vue";
|
||||
import moment from 'moment-jalaali'
|
||||
const date= ref(null)
|
||||
const date2= ref(null)
|
||||
const noe = ref('مبلغ')
|
||||
const store = useProductsStore();
|
||||
const data = ref(null);
|
||||
const course = reactive({
|
||||
name: "",
|
||||
description: "",
|
||||
slug: "",
|
||||
duration: "",
|
||||
image: "",
|
||||
categories: "",
|
||||
price: 0,
|
||||
teacher: "",
|
||||
completionPercentage: 0,
|
||||
level: "",
|
||||
language: "",
|
||||
countOfVideos: 0,
|
||||
totalFileSize: "",
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dp {
|
||||
height: 15px;
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
|
||||
<section class="flex flex-col gap-4">
|
||||
<Panel :header="$t('ویرایش دوره')">
|
||||
<div class="grid grid-cols-3 gap-7">
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="course.name" />
|
||||
<label>{{ $t("title") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="course.language" />
|
||||
<label>{{ $t("زبان آموزش") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="course.duration" />
|
||||
<label>{{ $t("مدت زمان دوره") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<Dropdown
|
||||
:options="levels"
|
||||
:optionLabel="levels"
|
||||
:optionValue="levels"
|
||||
|
||||
class="w-full"
|
||||
/>
|
||||
<label>{{ $t("سطح دوره") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t("مدرس دوره") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t("دسته بندی") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText
|
||||
class="w-full"
|
||||
|
||||
/>
|
||||
<label>{{ $t("درصد تکمیل دوره") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t("تعداد ویدیو ها") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t("حجم کل دوره") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full mt-7">
|
||||
<div class="basis-4/6">
|
||||
<FloatLabel>
|
||||
<Editor
|
||||
|
||||
placeholder="توضیحات"
|
||||
editorStyle="height: 320px"
|
||||
/>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="basis-2/6 ms-10">
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputGroup>
|
||||
<InputText class="pointer-events-none truncate" dir="ltr" />
|
||||
<Button severity="secondary">
|
||||
<i class="pi pi-image ml-2"></i>
|
||||
<span>{{ $t("chooseImage") }}</span>
|
||||
<input
|
||||
type="file"
|
||||
class="opacity-0 absolute inset-0 cursor-pointer"
|
||||
accept="image/*"
|
||||
|
||||
/>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
<label>{{ $t("image") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="card flex flex-col gap-2 justify-center mt-7">
|
||||
<Listbox
|
||||
listStyle="height:240px"
|
||||
emptyMessage="دسته بندی وجود ندارد"
|
||||
emptyFilterMessage="دسته بندی وجود ندارد"
|
||||
filterPlaceholder=" دسته بندی"
|
||||
:options="cities"
|
||||
filter
|
||||
optionLabel="name"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex pt-4 justify-end">
|
||||
<Button
|
||||
:label="$t('edit')"
|
||||
icon="pi pi-save"
|
||||
severity="success"
|
||||
:loading="saving"
|
||||
@click="create(course)"
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
</section>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useProductsStore } from '@/stores/products';
|
||||
import { ref, watchEffect, reactive } from 'vue';
|
||||
|
||||
const store = useProductsStore()
|
||||
const data = ref(null)
|
||||
const course = reactive({
|
||||
name: "",
|
||||
description: "",
|
||||
slug: "",
|
||||
duration: "",
|
||||
image: "",
|
||||
categories: "",
|
||||
price: 0,
|
||||
teacher: "",
|
||||
completionPercentage: 0,
|
||||
level: "",
|
||||
language: "",
|
||||
countOfVideos: 0,
|
||||
totalFileSize: "",
|
||||
});
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
watchEffect(async()=>{
|
||||
data.value = await store.edit(route.params.id)
|
||||
console.log('store7',data.value.course);
|
||||
course.name = data.value.course.name
|
||||
course.language = data.value.course.language
|
||||
course.duration = data.value.course.duration
|
||||
})
|
||||
|
||||
// const levels = reactive(["آسان", "متوسط", "سخت"]);
|
||||
|
||||
|
||||
const chooseImage = async (el) => {
|
||||
// const uploader = await uploadStore.uploader(el.target.value);
|
||||
// //console.log('uploader', uploader.data.url);
|
||||
// newCourse.image = uploader.data.url;
|
||||
// console.log("تصویر آپلود شد");
|
||||
// console.log('upldr',uploader);
|
||||
};
|
||||
|
||||
const create = async (data) => {
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<section class="flex flex-col gap-4">
|
||||
<Panel :header="$t('ویرایش تخفیف')">
|
||||
<div class="grid grid-cols-3 gap-7">
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full custom-input" v-model="date" />
|
||||
<label>{{ $t("تاریخ شروع") }}</label>
|
||||
</FloatLabel>
|
||||
<date-picker
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
display-format="jYYYY-jMM-jDD HH:mm"
|
||||
:timezone="true"
|
||||
color="dimgray"
|
||||
v-model="date"
|
||||
simple
|
||||
custom-input=".custom-input"
|
||||
></date-picker>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full custom-input2" v-model="date2" />
|
||||
<label>{{ $t("تاریخ پایان") }}</label>
|
||||
</FloatLabel>
|
||||
<date-picker
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
display-format="jYYYY-jMM-jDD HH:mm"
|
||||
:timezone="true"
|
||||
color="dimgray"
|
||||
v-model="date2"
|
||||
simple
|
||||
custom-input=".custom-input2"
|
||||
></date-picker>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="course.duration" />
|
||||
<label>{{ $t("کد تخفیف") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<Dropdown
|
||||
v-model="noe"
|
||||
:options="['مبلغ', 'درصد']"
|
||||
:optionLabel="levels"
|
||||
:optionValue="levels"
|
||||
class="w-full"
|
||||
/>
|
||||
<label>{{ $t("نوع تخفیف") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div v-if="noe === 'مبلغ'" class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t("مبلغ تخفیف") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div v-if="noe === 'درصد'" class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t("درصد تخفیف") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 my-custom-container inline">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" />
|
||||
<label>{{ $t("تخفیف برای همه کاربران") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex pt-4 justify-end">
|
||||
<Button
|
||||
:label="$t('save')"
|
||||
icon="pi pi-save"
|
||||
severity="success"
|
||||
:loading="saving"
|
||||
@click="create(course)"
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDiscountStore } from "@/stores/discounts";
|
||||
import { ref, watchEffect, reactive } from "vue";
|
||||
import moment from 'moment-jalaali'
|
||||
import { useRoute } from "vue-router";
|
||||
const date= ref(null)
|
||||
const date2= ref(null)
|
||||
const noe = ref('مبلغ')
|
||||
const store = useDiscountStore();
|
||||
const data = ref(null);
|
||||
const course = reactive({
|
||||
name: "",
|
||||
description: "",
|
||||
slug: "",
|
||||
duration: "",
|
||||
image: "",
|
||||
categories: "",
|
||||
price: 0,
|
||||
teacher: "",
|
||||
completionPercentage: 0,
|
||||
level: "",
|
||||
language: "",
|
||||
countOfVideos: 0,
|
||||
totalFileSize: "",
|
||||
});
|
||||
const route = useRoute()
|
||||
|
||||
watchEffect(async() =>{
|
||||
data.value = await store.getDiscount(route.params.id)
|
||||
})
|
||||
console.log('this', data);
|
||||
</script>
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<section class="flex flex-col gap-4">
|
||||
<Panel :header="$t('اطلاعات صفحه ی اصلی وبسایت')">
|
||||
<div class="grid grid-cols-1 gap-7">
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="data.title" />
|
||||
<label>{{ $t("متن هدر") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<Textarea v-model="data.description" rows="5" class="w-full" />
|
||||
<label>{{ $t("توضیحات هدر") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-7 mt-7">
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText v-model="data.student" class="w-full" />
|
||||
<label>{{ $t("تعداد دانشجو ها") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="data.videosCount" />
|
||||
<label>{{ $t("تعداد ویدیو های آموزشی") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText class="w-full" v-model="data.rates" />
|
||||
<label>{{ $t("امتیاز دانشجو ها") }}</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-7 text-lg">درباره ما :</p>
|
||||
<div class="mt-2">
|
||||
<FloatLabel>
|
||||
<Editor v-model="data.about" editorStyle="height: 320px">
|
||||
<template v-slot:toolbar>
|
||||
<span class="ql-formats text-start justify-center w-full flex">
|
||||
<button v-tooltip.bottom="'Bold'" class="ql-bold"></button>
|
||||
<button v-tooltip.bottom="'Italic'" class="ql-italic"></button>
|
||||
<button
|
||||
v-tooltip.bottom="'Underline'"
|
||||
class="ql-underline"
|
||||
></button>
|
||||
</span>
|
||||
</template>
|
||||
</Editor>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="flex pt-4 justify-end">
|
||||
<Button
|
||||
:label="$t('edit')"
|
||||
icon="pi pi-save"
|
||||
severity="success"
|
||||
:loading="saving"
|
||||
@click="save(data)"
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { jDate } from "@/utils/jDate";
|
||||
import { treeTogglerRTL } from "@/utils/tree-toggler-rtl";
|
||||
import {
|
||||
inject,
|
||||
defineAsyncComponent,
|
||||
watchEffect,
|
||||
ref,
|
||||
watch,
|
||||
nextTick,
|
||||
reactive,
|
||||
} from "vue";
|
||||
import { useStaticsStore } from "@/stores/statics";
|
||||
import { useRoute } from "vue-router";
|
||||
const data = reactive({
|
||||
about: "string",
|
||||
student: 0,
|
||||
hours: 0,
|
||||
title: 0,
|
||||
expertise: 0,
|
||||
nameCompany: "string",
|
||||
mission: "string",
|
||||
start: "string",
|
||||
goals: "string",
|
||||
// description: "",
|
||||
// videosCount: null,
|
||||
// rates: null,
|
||||
});
|
||||
const save = async (data) => {
|
||||
console.log('form', data);
|
||||
console.log('json', JSON.stringify(data));
|
||||
const { result } = await store.edit(data);
|
||||
console.log("res", result);
|
||||
};
|
||||
const expandedKeys = ref({});
|
||||
|
||||
const CategoryForm = defineAsyncComponent(() =>
|
||||
import("../components/CategoryForm.vue")
|
||||
);
|
||||
|
||||
const { dialog, confirm, toast, t } = inject("service");
|
||||
|
||||
const store = useStaticsStore();
|
||||
|
||||
watchEffect(async () => {
|
||||
await store.index();
|
||||
// console.log(store.items);
|
||||
data.student = store.items.student;
|
||||
//data.videosCount = store.items.hours;
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<Card
|
||||
v-if="!changeForm"
|
||||
class="m-auto w-96"
|
||||
:pt="{ title: 'text-center pt-4', content: 'flex flex-col gap-9 pt-8 h-30' }"
|
||||
>
|
||||
<template #title> {{ $t('login') }} </template>
|
||||
<template #content>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText v-model="number" class="w-full ltr" />
|
||||
<label>{{ $t('phoneNumber') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.length > 0">{{ errors }}</small>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Button :label="$t('sendOtp')" class="w-full" :loading="loading" @click="login()" />
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
v-else
|
||||
class="m-auto w-96"
|
||||
:pt="{ title: 'text-center pt-4', content: 'flex flex-col gap-9 pt-8 h-30' }"
|
||||
>
|
||||
<template #title> {{ $t('otp') }} </template>
|
||||
<template #content>
|
||||
<div class="flex flex-col gap-1">
|
||||
<FloatLabel>
|
||||
<InputText v-model="code" class="w-full ltr" />
|
||||
<label>{{ $t('otp') }}</label>
|
||||
</FloatLabel>
|
||||
<small v-if="errors.length > 0">{{ errors }}</small>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Button :label="$t('login')" class="w-full" :loading="loading" @click="checkOtp()" />
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
<script setup>
|
||||
import { inject, reactive, ref } from 'vue'
|
||||
import { useAdminsStore } from '@/stores/admins'
|
||||
import { useRouter } from 'vue-router'
|
||||
const { toast, t } = inject('service')
|
||||
const loading = ref(false)
|
||||
const changeForm = ref(false)
|
||||
const store = useAdminsStore()
|
||||
const errors = ref({})
|
||||
const number = ref(null)
|
||||
const code = ref(1234)
|
||||
const router = useRouter()
|
||||
const login = async () => {
|
||||
loading.value = true
|
||||
const { status, data } = await store.login(number)
|
||||
loading.value = false
|
||||
if (status === 200) {
|
||||
changeForm.value = true
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
life: 2000,
|
||||
detail: t('sendOtpSuccessfully'),
|
||||
summary: t('successful'),
|
||||
closable: false
|
||||
})
|
||||
}
|
||||
}
|
||||
const checkOtp = async () => {
|
||||
loading.value = true
|
||||
const { status, data } = await store.sendOtp(number, code)
|
||||
console.log('com data', data)
|
||||
console.log('com status', status)
|
||||
loading.value = false
|
||||
if (status === 200) {
|
||||
toast.add({
|
||||
severity: 'success', life: 2000, detail: t('loginSuccessfully'), summary: t('successful'), closable: false
|
||||
})
|
||||
setTimeout(() => router.push('/'), 300);
|
||||
}
|
||||
}
|
||||
|
||||
// // setTimeout(() => router.push('/'), 300);
|
||||
// console.log(data);
|
||||
// } else if (status === 400)
|
||||
// // console.log('400', data.message[0])
|
||||
// errors.value = data.message[0]
|
||||
// else
|
||||
// {
|
||||
// console.log(data);
|
||||
|
||||
// }
|
||||
// toast.add({
|
||||
// severity: 'error', life: 2000, detail: data.msg, summary: t('error')
|
||||
// })
|
||||
</script>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
اخبار
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<h1>محصولات</h1>
|
||||
</template>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<Panel :header="$t('users')">
|
||||
<DataTable :value="store.items">
|
||||
<Column class="w-16">
|
||||
<template #body="{ data: { avatarMedia } }">
|
||||
<Avatar
|
||||
:image="avatarMedia"
|
||||
size="large"
|
||||
shape="circle"
|
||||
class="transition-transform hover:scale-150"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="name" :header="$t('name')" />
|
||||
<Column field="id" :header="$t('id')" />
|
||||
<Column field="phone" :header="$t('phoneNumber')" />
|
||||
<Column field="statusText" :header="$t('status')" />
|
||||
<Column field="userName" :header="$t('username')" />
|
||||
<Column headerClass="w-32">
|
||||
<template #body="{ data }">
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
rounded
|
||||
text
|
||||
severity="secondary"
|
||||
@click="show(data)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<template #empty>
|
||||
<p>{{ store.fetching ? $t("loading") : $t("emptyTable") }}</p>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<!--
|
||||
<Paginator :rows="store.total" :totalRecords="store.total" :rowsPerPageOptions="[5, 20, 30]"></Paginator>
|
||||
-->
|
||||
|
||||
<Pagination :total="store.total" />
|
||||
</template>
|
||||
</DataTable>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import Pagination from "@/components/Pagination.vue";
|
||||
import { useUsersStore } from "@/stores/users";
|
||||
import { jDate } from "@/utils/jDate";
|
||||
console.log("users", useUsersStore);
|
||||
import { inject, reactive, defineAsyncComponent, ref, watchEffect } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const UserDetails = defineAsyncComponent(() =>
|
||||
import("../components/UserDetails.vue")
|
||||
);
|
||||
|
||||
const { dialog, t } = inject("service");
|
||||
|
||||
const store = useUsersStore();
|
||||
|
||||
const options = reactive([
|
||||
{ label: t("customers"), value: false },
|
||||
{ label: t("sellers"), value: true },
|
||||
]);
|
||||
|
||||
const confirmToSales = ref(false);
|
||||
|
||||
const show = (user) => {
|
||||
dialog.open(UserDetails, {
|
||||
props: { modal: true, closable: true, header: t("userDetails") },
|
||||
data: { user, confirm: store.confirm },
|
||||
});
|
||||
};
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
watchEffect(async () => {
|
||||
const { page = 1, pageSize = 10 } = route.query;
|
||||
await store.index(page, pageSize);
|
||||
const pages = await store.page();
|
||||
});
|
||||
console.log(store);
|
||||
</script>
|
||||
@@ -0,0 +1,16 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
container: {
|
||||
center: true
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
import Components from 'unplugin-vue-components/vite';
|
||||
import { PrimeVueResolver } from 'unplugin-vue-components/resolvers';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue({
|
||||
script: {
|
||||
defineModel: true
|
||||
}
|
||||
}),
|
||||
VueDevTools(),
|
||||
Components({
|
||||
resolvers: [
|
||||
PrimeVueResolver()
|
||||
]
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user