This commit is contained in:
2025-03-07 20:41:21 +07:00
parent bf7037f4e8
commit 96b814d54a
52 changed files with 6643 additions and 782 deletions

View File

@@ -1,31 +1,62 @@
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
import { ref } from 'vue';
import type { IEntity } from './types/entity.type';
import type { IService } from './types/service.type';
import type { Scheme } from './types/scheme.type';
import Table from './table/Table.vue';
import { getDefaultValues } from './utils/structs/defaults.util';
class Entity implements IEntity {
constructor(public Id: number, public Name: string, public Region: string) { }
}
class Service implements IService<Entity> {
private readonly entities = ref<Entity[]>([])
private maxId = 0
async create(data: Entity): Promise<void | never> {
this.maxId++
this.entities.value.push({ ...data, Id: this.maxId })
}
async delete(id: number): Promise<void | never> {
this.entities.value = this.entities.value.filter(el => el.Id != id)
}
async read(id: number): Promise<Entity | undefined> {
return this.entities.value.find(el => el.Id == id)
}
async readAll(): Promise<Entity[]> {
return this.entities.value
}
async update(data: Entity): Promise<void | never> {
this.entities.value = this.entities.value.map(el => el.Id == data.Id ? data : el)
}
}
const service = new Service
const scheme: Scheme<Entity> = {
Id: {
hidden: true,
russian: 'Id'
},
Name: {
type: {
primitive: 'string'
},
russian: 'Имя'
},
Region: {
type: {
nested: {
field: [],
values: ['kemerovo', 'kuzbass', 'berlin']
}
},
russian: "Место жительства"
},
}
</script>
<template>
<div class="container">
<div>
<a wml-openURL="https://wails.io">
<img src="/wails.png" class="logo" alt="Wails logo"/>
</a>
<a wml-openURL="https://vuejs.org/">
<img src="/vue.svg" class="logo vue" alt="Vue logo"/>
</a>
</div>
<HelloWorld msg="Wails + Vue" />
</div>
<Table :scheme :service :get-defaults="() => getDefaultValues(scheme)"></Table>
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
}
.logo:hover {
filter: drop-shadow(0 0 2em #e80000aa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

View File

@@ -1,42 +0,0 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { Events } from "@wailsio/runtime";
import * as PostService from "../../bindings/app/internal/services/postservice";
const name = ref("");
const result = ref("Please enter your name below 👇");
const time = ref("Listening for Time event...");
const doGreet = () => {
let localName = name.value;
if (!localName) {
localName = "anonymous";
}
};
onMounted(async () => {
console.log(await PostService.GetById(5));
});
</script>
<template>
<h1>Kuzbass</h1>
<div class="result"></div>
<div class="card">
<div class="input-box">
<input
class="input"
v-model="name"
type="text"
autocomplete="off"
/>
<button class="btn" @click="doGreet">Greet</button>
</div>
</div>
<div class="footer">
<div><p>Click on the Wails logo to learn more</p></div>
<div><p></p></div>
</div>
</template>

View File

@@ -0,0 +1,4 @@
<template>
<button class="fixed bottom-6 right-6 aspect-square h-10 text-3xl flex items-center justify-center rounded-full focus-visible:outline-none text-black"
style="background:var(--p-primary-color)"><span class="pi pi-plus"></span></button>
</template>

View File

@@ -1,4 +1,12 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { Config } from 'primevue'
import Aura from '@primevue/themes/aura'
import 'primeicons/primeicons.css'
createApp(App).mount('#app')
createApp(App).use(Config, {
theme: {
preset: Aura
}
}).mount('#app')

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import Table from '../table/Table.vue'
import { onMounted, reactive } from 'vue'
import { getDefaultValues } from '../utils/structs/defaults.util'
import S from './post.service.ts'
import type { Scheme } from '../types/scheme.type'
import { Post } from '../../bindings/app/internal/services/models.ts'
const service = new S
onMounted(async () => {
})
const scheme: Scheme<Post> = reactive({
Id:{
type: {
primitive: "number",
},
},
Text:{
type: {
primitive: "string",
},
},
CreatedAt:{
type: {
primitive: "number",
},
},
})
const getDefaults = () => getDefaultValues(scheme)
</script>
<template>
<main class="w-screen h-screen">
<Table :scheme :service :getDefaults></Table>
</main>
</template>

View File

@@ -0,0 +1,27 @@
import { GetAll, Create, Delete, GetById, Update, Count } from "../../bindings/app/internal/services/postservice.ts"
import type { Post } from "../../bindings/app/internal/services/models.ts"
import type { IService } from "../types/service.type.ts"
export default class PostService implements IService<Post> {
async read(id: number) {
return await GetById(id)
}
async readAll() {
return await GetAll()
}
async create(item: Post) {
await Create(item)
}
async delete(id: number) {
return await Delete(id)
}
async update(item: Post) {
await Update(item)
}
async count() {
return await Count()
}
}

3
frontend/src/style.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,94 @@
<script setup lang="ts" generic="T extends IEntity">
import { Button, DatePicker, Dialog, InputNumber, InputText, MultiSelect, Select, Textarea, ToggleSwitch } from 'primevue';
import type { IEntity } from '../types/entity.type';
import type { Scheme } from '../types/scheme.type';
import type { IService } from '../types/service.type';
import { manyStructsView } from '../utils/structs/structs-view.util';
import { type UnwrapRef } from 'vue';
const showCreate = defineModel<boolean>('show')
const createItem = defineModel<T>('item')
const items = defineModel<UnwrapRef<T[]>>('items')
const props = defineProps<{
scheme: Scheme<T>,
getDefaults: () => Partial<T>,
service: IService<T>,
updateMode?: boolean
}>()
type Key = keyof T
const keys = Object.keys(props.scheme) as Key[]
const emits = defineEmits<{
(e: 'onSave', data: any): void
(e: 'onSaveUpdate', data: any): void
(e: 'onSaveCreate', data: any): void
}>()
</script>
<template>
<Dialog v-model:visible="showCreate">
<div class="flex flex-col justify-center gap-5">
<div v-for="key in keys" v-show="!props.scheme[key].hidden && !props.scheme[key].readonly"
class="flex items-center gap-5">
<h1 class="w-[200px]">{{ props.scheme[key].russian }}</h1>
<div>
<div v-if="props.scheme[key]?.customWindow?.[props.updateMode ? 'update' : 'create']">
<slot :name="<string>key + (props.updateMode ? 'Update' : 'Create')"></slot>
</div>
<div v-else-if="props.scheme[key]?.customWindow?.common">
<slot :name="<string>key"></slot>
</div>
<InputNumber class="w-[300px]" v-model:model-value="<number>createItem![key]"
v-else-if="props.scheme[key]?.type?.primitive === 'number'" />
<InputText class="w-[300px]" v-model:model-value="<string>createItem![key]"
v-else-if="props.scheme[key].type?.primitive === 'string'" />
<DatePicker class="w-[300px]" v-model:model-value="<Date>createItem![key]"
v-else-if="props.scheme[key].type?.primitive === 'date'" />
<Textarea class="w-[300px]" v-model="<string>createItem![key]"
v-else-if="props.scheme[key].type?.primitive === 'multiple'" />
<ToggleSwitch class="w-[300px]" v-model:model-value="<boolean>createItem![key]"
v-else-if="props.scheme[key].type?.primitive === 'boolean'" />
<Select v-else-if="props.scheme[key].type?.nested?.values && !props.scheme[key].type?.many"
v-model:model-value="createItem![key]" :options="props.scheme[key].type.nested.values"
:placeholder="`Выберите ${props.scheme[key].russian}`" class="w-[300px]">
<template #option="{ option }">
{{ manyStructsView(option, props.scheme[key].type.nested.field) }}
</template>
<template #value="{ value }">
{{ manyStructsView(value, props.scheme[key].type.nested.field) }}
</template>
</Select>
<MultiSelect v-else-if="props.scheme[key].type?.many" v-model:model-value="createItem![key]"
:options="props.scheme[key].type?.nested?.values"
class="w-[300px] h-11"
:placeholder="`Выберите ${props.scheme[key].russian}`">
<template #option="{ option }">
{{ manyStructsView(option, props.scheme[key]?.type?.nested?.field) }}
</template>
<template #value="{ value }">
{{ manyStructsView(value, props.scheme[key]?.type?.nested?.field) }}
</template>
</MultiSelect>
</div>
</div>
</div>
<template #footer>
<Button severity="success" @click="async () => {
if (props.updateMode) {
props.service.update(createItem as T)
emits('onSaveUpdate', createItem as T)
emits('onSave', createItem as T)
} else {
props.service.create(createItem as T)
emits('onSaveCreate', createItem as T)
emits('onSave', createItem as T)
}
items = await service.readAll() as UnwrapRef<T[]>
showCreate = false
}">Сохранить</Button>
</template>
</Dialog>
</template>

View File

@@ -0,0 +1,155 @@
<script setup lang="ts" generic="T extends IEntity">
import { onMounted, ref, watch, type UnwrapRef } from "vue";
import type { TableProps } from "../types/table-props.type";
import { DataTable, Column, Button } from "primevue";
import { manyStructsView } from "../utils/structs/structs-view.util";
import type { TableEmits } from "../types/table-emits.type";
import FloatingButton from "../components/buttons/FloatingButton.vue";
import type { IEntity } from "../types/entity.type";
import DialogWindow from "./DialogWindow.vue";
const props = defineProps<TableProps<T>>();
const items = ref<T[]>([]);
onMounted(async () => {
items.value = await props.service.readAll();
});
type Key = keyof T;
const keys = Object.keys(props.scheme) as Key[];
const emits = defineEmits<TableEmits>();
const showCreate = ref(false);
const createItem = ref<null | T>(null);
const showUpdate = ref(false);
const updateItem = ref<null | T>(null);
watch(showUpdate, (value) => {
if (!value) {
updateItem.value = null;
}
});
watch(updateItem, (value) => {
if (value) {
showUpdate.value = true;
} else {
showUpdate.value = false;
}
});
watch(showCreate, (value) => {
if (value) {
createItem.value = props.getDefaults();
} else {
createItem.value = null;
}
});
watch(createItem, (value) => {
if (value) {
showCreate.value = true;
} else {
showCreate.value = false;
}
});
const handleFloatingButtonClick = () => {
emits('onCreateOpen')
emits('onOpen')
showCreate.value = true;
};
const slots = defineSlots();
const createSlotName = (key: any) => key + "Create";
const updateSlotName = (key: any) => key + "Update";
</script>
<template>
<DialogWindow
:scheme="props.scheme"
:service="props.service"
:get-defaults="props.getDefaults"
v-model:item="<T>createItem"
v-model:show="showCreate"
v-model:items="items"
@on-save="data => emits('onSave', data)"
@on-save-create="data => emits('onSaveCreate', data)"
>
<template v-for="key in keys" #[key]>
<slot :name="<string>key"></slot>
</template>
<template v-for="key in keys" #[createSlotName(key)]>
<slot :name="createSlotName(key)"></slot>
</template>
</DialogWindow>
<DialogWindow
:scheme="props.scheme"
update-mode
:service="props.service"
:get-defaults="props.getDefaults"
v-model:item="<T>updateItem"
v-model:show="showUpdate"
v-model:items="items"
@on-save="data => emits('onSave', data)"
@on-save-update="data => emits('onSaveUpdate', data)"
>
<template v-for="key in keys" #[key]>
<slot :name="<string>key"></slot>
</template>
<template v-for="key in keys" #[updateSlotName(key)]>
<slot :name="updateSlotName(key)"></slot>
</template>
</DialogWindow>
<div>
<DataTable :value="<[]>items">
<template #header v-if="props.name">
<p>{{ props.name }}</p>
</template>
<template v-for="key in keys">
<Column
:header="props.scheme[key]?.russian"
v-if="!props.scheme[key].hidden"
>
<template #body="{ data }">
<p>
{{
manyStructsView(
data[key],
props.scheme[key]?.type?.nested?.field,
)
}}
</p>
</template>
</Column>
</template>
<Column header="Действия">
<template #body="{ data }">
<div class="flex gap-2">
<Button
severity="secondary"
icon="pi pi-pencil"
@click="() => {
emits('onUpdateOpen')
emits('onOpen')
updateItem = data
}"
></Button>
<Button
severity="danger"
icon="pi pi-trash"
@click="async () => {
emits('onDelete', data)
await props.service.delete(data.Id)
items = await props.service.readAll() as UnwrapRef<T[]>
}"
></Button>
</div>
</template>
</Column>
</DataTable>
<FloatingButton @click="handleFloatingButtonClick" />
</div>
</template>

View File

@@ -0,0 +1,3 @@
export interface IEntity {
Id: number
}

View File

@@ -0,0 +1 @@
export type PrimitiveFieldType = "date" | "number" | "string" | "boolean" | "multiple"

View File

@@ -0,0 +1,21 @@
import type { IEntity } from "./entity.type"
import type { PrimitiveFieldType } from "./primitive-field-type.type"
export interface ISchemeField<T extends IEntity> {
type?: {
primitive?: PrimitiveFieldType
nested?: {
field: string[]
values: T[]
}
many?: boolean
}
russian?: string
hidden?: boolean
readonly?: boolean
customWindow?: {
common?: boolean
create?: boolean
update?: boolean
}
}

View File

@@ -0,0 +1,4 @@
import type { IEntity } from "./entity.type";
import type { ISchemeField } from "./scheme-field.type";
export type Scheme<T extends IEntity, S extends IEntity=any> = Record<keyof T, ISchemeField<S>>

View File

@@ -0,0 +1,7 @@
export interface IService<T> {
read(id: number): Promise<T | undefined>
readAll(): Promise<T[]>
create(data: T): Promise<void | never>
update(data: T): Promise<void | never>
delete(id: number): Promise<void | never>
}

View File

@@ -0,0 +1,12 @@
export type TableEmits = {
(e: 'onCreateOpen'): void
(e: 'onCreateClose', data: any): void
(e: 'onUpdateOpen'): void
(e: 'onUpdateClose', data: any): void
(e: 'onOpen'): void
(e: 'onClose', data: any): void
(e: 'onDelete', data: any): void
(e: 'onSaveUpdate', data: any): void
(e: 'onSaveCreate', data: any): void
(e: 'onSave', data: any): void
}

View File

@@ -0,0 +1,11 @@
import type { IEntity } from "./entity.type";
import type { Scheme } from "./scheme.type";
import type { IService } from "./service.type";
export interface TableProps<T extends IEntity> {
service: IService<T>
scheme: Scheme<T>
name?: string
getDefaults: () => Partial<T>
validate?: (data: T) => never | void
}

View File

@@ -0,0 +1,10 @@
const getFullTimestamp = (n: number): number => {
const length = String(n).length
let str = ''
while (str.length + length < 13) {
str += '0'
}
return parseInt(`${n}${str}`)
}
export const toDate = (n: number) => new Date(getFullTimestamp(n))
export const toTimestamp = (d: Date) => d.getTime()

View File

@@ -0,0 +1,6 @@
export const getTomorrow = (): Date => {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
return tomorrow;
}

View File

@@ -0,0 +1,27 @@
import type { IEntity } from "../../types/entity.type";
import type { Scheme } from "../../types/scheme.type";
import { getTomorrow } from "../date/getters";
export const getDefaultValues = <T extends IEntity>(scheme: Scheme<T>) => {
const keys = Object.keys(scheme) as (keyof typeof scheme)[]
let obj: any = {}
for (let key of keys) {
const primitive = scheme[key]?.type?.primitive
if (primitive == 'string' || primitive == 'multiple') {
obj[key] = ''
} else if (primitive == 'date') {
obj[key] = getTomorrow()
} else if (primitive == 'boolean') {
obj[key] = false
} else if (primitive == 'number') {
obj[key] = 1
} else if (scheme[key].type?.many) {
obj[key] = []
} else if (scheme[key]?.type?.nested?.values) {
obj[key] = scheme[key].type.nested.values[0]
}
}
return obj as T
}

View File

@@ -0,0 +1,18 @@
export const structView = (item: any, path: any) => {
if (!item) return;
if (!path?.length) return item;
let result = item
let i = 0
let current
while (current != path[path.length - 1] && result) {
current = path[i]
result = result[current]
i++
}
return result
}
export const manyStructsView = (items: any, path: any) => {
if (!Array.isArray(items)) return structView(items, path);
return items.map(item => structView(item, path)).join(", ")
}

View File

@@ -0,0 +1 @@
export const ifNull = (data: any, elseData: any) => data? data : elseData