feat: add requests
This commit is contained in:
parent
6501b1d020
commit
41cc1e7c83
5
src/api/request.ts
Normal file
5
src/api/request.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export const requestApi = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||||
|
})
|
||||||
123
src/components/request/RequestForm.vue
Normal file
123
src/components/request/RequestForm.vue
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="onSubmit">
|
||||||
|
<div
|
||||||
|
class="form-control"
|
||||||
|
:class="{invalid: fError}"
|
||||||
|
>
|
||||||
|
<label for="fio">ФИО</label>
|
||||||
|
<input
|
||||||
|
id="fio"
|
||||||
|
v-model="fio"
|
||||||
|
type="text"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
@blur="fBlur"
|
||||||
|
>
|
||||||
|
<small
|
||||||
|
v-if="fError"
|
||||||
|
class="text-danger"
|
||||||
|
>
|
||||||
|
{{ fError }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="form-control"
|
||||||
|
:class="{invalid: pError}"
|
||||||
|
>
|
||||||
|
<label for="phone">Телефон</label>
|
||||||
|
<input
|
||||||
|
id="phone"
|
||||||
|
v-model="phone"
|
||||||
|
type="tel"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
@blur="pBlur"
|
||||||
|
>
|
||||||
|
<small
|
||||||
|
v-if="pError"
|
||||||
|
class="text-danger"
|
||||||
|
>
|
||||||
|
{{ pError }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="form-control"
|
||||||
|
:class="{invalid: aError}"
|
||||||
|
>
|
||||||
|
<label for="amount">Сумма</label>
|
||||||
|
<input
|
||||||
|
id="amount"
|
||||||
|
v-model="amount"
|
||||||
|
type="number"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
min="0"
|
||||||
|
@blur="aBlur"
|
||||||
|
>
|
||||||
|
<small
|
||||||
|
v-if="aError"
|
||||||
|
class="text-danger"
|
||||||
|
>
|
||||||
|
{{ aError }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label for="status">Статус</label>
|
||||||
|
<select
|
||||||
|
id="status"
|
||||||
|
v-model="status"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
<option value="done">
|
||||||
|
Завершено
|
||||||
|
</option>
|
||||||
|
<option value="cancelled">
|
||||||
|
Отменено
|
||||||
|
</option>
|
||||||
|
<option value="active">
|
||||||
|
Активен
|
||||||
|
</option>
|
||||||
|
<option value="pending">
|
||||||
|
Выполняется
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
Создать
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import useRequestForm from '@/hooks/useRequestForm.ts';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
|
const emit = defineEmits(['created'])
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const onSubmitHandler = async (values) => {
|
||||||
|
await store.dispatch('request/create', values);
|
||||||
|
emit('created', values);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
onSubmit,
|
||||||
|
isSubmitting,
|
||||||
|
status,
|
||||||
|
fio,
|
||||||
|
phone,
|
||||||
|
amount,
|
||||||
|
fError,
|
||||||
|
fBlur,
|
||||||
|
pError,
|
||||||
|
pBlur,
|
||||||
|
aError,
|
||||||
|
aBlur,
|
||||||
|
} = useRequestForm({ onSubmit: onSubmitHandler });
|
||||||
|
|
||||||
|
</script>
|
||||||
45
src/components/request/RequestTable.vue
Normal file
45
src/components/request/RequestTable.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<h4
|
||||||
|
v-if="requests.length === 0"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
Заявок нет
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<table
|
||||||
|
v-else
|
||||||
|
class="table"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>№</th>
|
||||||
|
<th>ФИО</th>
|
||||||
|
<th>Телефон</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
<th>Действие</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(request, index) in requests"
|
||||||
|
:key="request.id"
|
||||||
|
>
|
||||||
|
<td>{{ index + 1 }}</td>
|
||||||
|
<td>{{ request.fio }}</td>
|
||||||
|
<td>{{ request.phone }}</td>
|
||||||
|
<td>{{ request.status }}</td>
|
||||||
|
<td>Купить</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
requests: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
24
src/components/ui/AppModal.vue
Normal file
24
src/components/ui/AppModal.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="modal-backdrop"
|
||||||
|
@click="$emit('close')"
|
||||||
|
/>
|
||||||
|
<div class="modal">
|
||||||
|
<h3 v-if="title">
|
||||||
|
{{ title }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits(['close'])
|
||||||
|
</script>
|
||||||
25
src/components/ui/AppSpinner.vue
Normal file
25
src/components/ui/AppSpinner.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<span class="loader" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.loader {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 5px solid transparent;
|
||||||
|
border-bottom-color: #42b983;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: rotation 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotation {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
65
src/hooks/useRequestForm.ts
Normal file
65
src/hooks/useRequestForm.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { toTypedSchema } from '@vee-validate/zod';
|
||||||
|
import { type SubmissionHandler, useField, useForm } from 'vee-validate';
|
||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
fio: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, 'Обязательное поле')
|
||||||
|
.default(''),
|
||||||
|
phone: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, 'Обязательное поле')
|
||||||
|
.default(''),
|
||||||
|
amount: z
|
||||||
|
.coerce
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default('')
|
||||||
|
.transform(value => Number(value || 0)),
|
||||||
|
|
||||||
|
status: z
|
||||||
|
.enum(["done", "cancelled", "active", "pending"])
|
||||||
|
});
|
||||||
|
|
||||||
|
type Values = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
|
||||||
|
type UseRequestFormProps = {
|
||||||
|
onSubmit: SubmissionHandler<Values>
|
||||||
|
}
|
||||||
|
const useRequestForm = (props: UseRequestFormProps) => {
|
||||||
|
const {onSubmit} = props;
|
||||||
|
const {isSubmitting, handleSubmit} = useForm<Values>({
|
||||||
|
validationSchema: toTypedSchema(schema),
|
||||||
|
initialValues: {
|
||||||
|
status: 'active',
|
||||||
|
amount: 0,
|
||||||
|
phone: '',
|
||||||
|
fio: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const { value: fio, errorMessage: fError, handleBlur: fBlur } = useField('fio');
|
||||||
|
const { value: phone, errorMessage: pError, handleBlur: pBlur } = useField('phone');
|
||||||
|
const { value: amount, errorMessage: aError, handleBlur: aBlur } = useField('amount');
|
||||||
|
const { value: status } = useField('status');
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
isSubmitting,
|
||||||
|
onSubmit: handleSubmit(onSubmit),
|
||||||
|
fio,
|
||||||
|
phone,
|
||||||
|
amount,
|
||||||
|
fError,
|
||||||
|
fBlur,
|
||||||
|
pError,
|
||||||
|
pBlur,
|
||||||
|
aError,
|
||||||
|
aBlur,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useRequestForm
|
||||||
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<the-navbar />
|
<the-navbar />
|
||||||
<div class="container with-nav">
|
<div class="container with-nav">
|
||||||
|
<app-alert />
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
@ -9,8 +10,9 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import TheNavbar from '../components/TheNavbar.vue'
|
import TheNavbar from '../components/TheNavbar.vue'
|
||||||
|
import AppAlert from '@/components/ui/AppAlert.vue';
|
||||||
export default {
|
export default {
|
||||||
components: { TheNavbar }
|
components: {AppAlert, TheNavbar }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -45,6 +45,8 @@ const routes = [
|
|||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes,
|
routes,
|
||||||
|
linkActiveClass: 'active',
|
||||||
|
linkExactActiveClass: 'active',
|
||||||
});
|
});
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { createLogger, createStore } from 'vuex'
|
import { createLogger, createStore } from 'vuex'
|
||||||
import authModule from './modules/auth.module.ts';
|
import authModule from './modules/auth.module.ts';
|
||||||
import alertModule from './modules/alert.module.ts';
|
import alertModule from './modules/alert.module.ts';
|
||||||
|
import requestModule from './modules/request.module.ts';
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ const store = createStore({
|
|||||||
modules: {
|
modules: {
|
||||||
auth: authModule,
|
auth: authModule,
|
||||||
alert: alertModule,
|
alert: alertModule,
|
||||||
|
request: requestModule
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
51
src/store/modules/request.module.ts
Normal file
51
src/store/modules/request.module.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { requestApi } from '@/api/request.ts';
|
||||||
|
import type { Module } from 'vuex';
|
||||||
|
import authModule from './auth.module.ts';
|
||||||
|
|
||||||
|
type Request = string;
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
requests: Request[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type SetRequestsPayload = {
|
||||||
|
requests: State['requests']
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddRequestsPayload = {
|
||||||
|
request: Request
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestModule: Module<State, any> = {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
requests: []
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setRequests: (state, payload: SetRequestsPayload) => {
|
||||||
|
const { requests } = payload;
|
||||||
|
state.requests = requests
|
||||||
|
},
|
||||||
|
|
||||||
|
addRequest: (state, payload: AddRequestsPayload) => {
|
||||||
|
const { request } = payload;
|
||||||
|
state.requests.push(request)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions:{
|
||||||
|
create: async (injectee, payload) => {
|
||||||
|
const { dispatch, commit } = injectee;
|
||||||
|
|
||||||
|
const token = authModule.state.token;
|
||||||
|
try {
|
||||||
|
const { data } = await requestApi.post(`/requests.json?auth=${token}`, payload)
|
||||||
|
commit('addRequest', {...payload, id: data.name})
|
||||||
|
await dispatch('alert/setAlert', {message: 'Заявка успешно создана', type: 'primary', title: 'Успех', time: 5000}, {root: true})
|
||||||
|
} catch (e) {
|
||||||
|
await dispatch('alert/setAlert', {message: 'Ошибка при попытке создать заявку', type: 'danger', title: 'Ошибка', time: 5000}, {root: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default requestModule;
|
||||||
@ -1,16 +1,77 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="header">
|
||||||
<h1>Home</h1>
|
<h1 class="card-title">
|
||||||
|
Список заявок
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn primary"
|
||||||
|
@click="openModal"
|
||||||
|
>
|
||||||
|
Создать
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="loading"
|
||||||
|
style="display: flex;"
|
||||||
|
>
|
||||||
|
<app-spinner />
|
||||||
|
</div>
|
||||||
|
<request-table
|
||||||
|
v-else
|
||||||
|
:requests="requests"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<teleport to="body">
|
||||||
|
<app-modal
|
||||||
|
v-if="modal"
|
||||||
|
title="Создать заявку"
|
||||||
|
@close="closeModal"
|
||||||
|
>
|
||||||
|
<request-form @created="closeModal" />
|
||||||
|
</app-modal>
|
||||||
|
</teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
|
|
||||||
export default {
|
import { requestApi } from '@/api/request.ts';
|
||||||
|
import RequestForm from '@/components/request/RequestForm.vue';
|
||||||
|
import RequestTable from '@/components/request/RequestTable.vue';
|
||||||
|
import AppModal from '@/components/ui/AppModal.vue';
|
||||||
|
import AppSpinner from '@/components/ui/AppSpinner.vue';
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
}
|
const modal = ref(false);
|
||||||
|
const store = useStore();
|
||||||
|
const closeModal = () => modal.value = false;
|
||||||
|
const openModal = () => modal.value = true;
|
||||||
|
|
||||||
|
const loading = ref(true);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const token = store.state.auth.token;
|
||||||
|
const { data } = await requestApi.get(`/requests.json?auth=${token}`);
|
||||||
|
const requests = Object.entries(data).map(([id, value]) => ({id, ...value}))
|
||||||
|
store.commit('request/setRequests', { requests })
|
||||||
|
} catch (e) {
|
||||||
|
await store.dispatch('alert/setAlert', {message: 'Ошибка при попытке получить список заявок', type: 'danger', title: 'Ошибка', time: 5000})
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const requests = computed(() => store.state.request.requests);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style scoped>
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user