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

@@ -2,17 +2,4 @@
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support For `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

View File

@@ -20,7 +20,7 @@ export function Count(): Promise<number> & { cancel(): void } {
export function Create(item: $models.Author): Promise<$models.Author> & { cancel(): void } {
let $resultPromise = $Call.ByID(3684602449, item) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
let $typingPromise = $resultPromise.then(($result) => {
return $$createType0($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
@@ -34,7 +34,7 @@ export function Delete(id: number): Promise<void> & { cancel(): void } {
export function GetAll(): Promise<($models.Author | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3248293926) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
let $typingPromise = $resultPromise.then(($result) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
@@ -43,7 +43,7 @@ export function GetAll(): Promise<($models.Author | null)[]> & { cancel(): void
export function GetById(id: number): Promise<$models.Author | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(1703016211, id) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
let $typingPromise = $resultPromise.then(($result) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
@@ -52,7 +52,7 @@ export function GetById(id: number): Promise<$models.Author | null> & { cancel()
export function Update(item: $models.Author): Promise<$models.Author> & { cancel(): void } {
let $resultPromise = $Call.ByID(2240704960, item) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
let $typingPromise = $resultPromise.then(($result) => {
return $$createType0($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);

View File

@@ -20,7 +20,7 @@ export function Count(): Promise<number> & { cancel(): void } {
export function Create(item: $models.Post): Promise<$models.Post> & { cancel(): void } {
let $resultPromise = $Call.ByID(1443399856, item) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
let $typingPromise = $resultPromise.then(($result) => {
return $$createType0($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
@@ -39,7 +39,7 @@ export function ExportToExcel(): Promise<void> & { cancel(): void } {
export function GetAll(): Promise<($models.Post | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(65691059) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
let $typingPromise = $resultPromise.then(($result) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
@@ -48,7 +48,7 @@ export function GetAll(): Promise<($models.Post | null)[]> & { cancel(): void }
export function GetById(id: number): Promise<$models.Post | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(4074736792, id) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
let $typingPromise = $resultPromise.then(($result) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
@@ -57,7 +57,7 @@ export function GetById(id: number): Promise<$models.Post | null> & { cancel():
export function Update(item: $models.Post): Promise<$models.Post> & { cancel(): void } {
let $resultPromise = $Call.ByID(137798821, item) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
let $typingPromise = $resultPromise.then(($result) => {
return $$createType0($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);

BIN
frontend/bun.lockb Executable file

Binary file not shown.

Binary file not shown.

3537
frontend/dist/assets/index-Z-UIusQp.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.logo[data-v-7fd835af]{height:6em;padding:1.5em;will-change:filter}.logo[data-v-7fd835af]:hover{filter:drop-shadow(0 0 2em #e80000aa)}.logo.vue[data-v-7fd835af]:hover{filter:drop-shadow(0 0 2em #42b883aa)}

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -1,13 +1,12 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/wails.png" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/style.css" />
<title>Wails + Vue + TS</title>
<script type="module" crossorigin src="/assets/index-dNSs0EXc.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-tI2uSkFL.css">
<title>Vite + Vue + TS</title>
<script type="module" crossorigin src="/assets/index-Z-UIusQp.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-hwTd8BmI.css">
</head>
<body>
<div id="app"></div>

View File

@@ -1,146 +0,0 @@
:root {
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: rgba(27, 38, 54, 1);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
* {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
src: local(""),
url("./Inter-Medium.ttf") format("truetype");
}
h3 {
font-size: 3em;
line-height: 1.1;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
button {
width: 60px;
height: 30px;
line-height: 30px;
border-radius: 3px;
border: none;
margin: 0 0 0 20px;
padding: 0 8px;
cursor: pointer;
}
.result {
height: 20px;
line-height: 20px;
}
body {
margin: 0;
display: flex;
place-items: center;
place-content: center;
min-width: 320px;
min-height: 100vh;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
max-width: 1280px;
margin: 0 auto;
text-align: center;
}
.result {
height: 20px;
line-height: 20px;
margin: 1.5rem auto;
text-align: center;
}
.footer {
margin-top: 1rem;
align-content: center;
text-align: center;
color: rgba(255, 255, 255, 0.67);
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
.input-box .btn:hover {
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
color: #333333;
}
.input-box .input {
border: none;
border-radius: 3px;
outline: none;
height: 30px;
line-height: 30px;
padding: 0 10px;
color: black;
background-color: rgba(240, 240, 240, 1);
-webkit-font-smoothing: antialiased;
}
.input-box .input:hover {
border: none;
background-color: rgba(255, 255, 255, 1);
}
.input-box .input:focus {
border: none;
background-color: rgba(255, 255, 255, 1);
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -1,11 +1,10 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/wails.png" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/style.css" />
<title>Wails + Vue + TS</title>
<title>Vite + Vue + TS</title>
</head>
<body>
<div id="app"></div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,29 @@
{
"name": "frontend",
"name": "nto-ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.2.45"
"@primevue/themes": "^4.2.5",
"pinia": "^2.3.0",
"primeicons": "^7.0.0",
"primevue": "^4.2.5",
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.0.0",
"typescript": "^4.9.3",
"vite": "^5.0.0",
"@wailsio/runtime": "latest",
"vue-tsc": "^1.0.11"
"@vitejs/plugin-vue": "^5.2.1",
"@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.20",
"install": "^0.13.0",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"vite": "^6.0.5",
"vue-tsc": "^2.2.0"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

View File

@@ -1,146 +0,0 @@
:root {
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: rgba(27, 38, 54, 1);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
* {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
src: local(""),
url("./Inter-Medium.ttf") format("truetype");
}
h3 {
font-size: 3em;
line-height: 1.1;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
button {
width: 60px;
height: 30px;
line-height: 30px;
border-radius: 3px;
border: none;
margin: 0 0 0 20px;
padding: 0 8px;
cursor: pointer;
}
.result {
height: 20px;
line-height: 20px;
}
body {
margin: 0;
display: flex;
place-items: center;
place-content: center;
min-width: 320px;
min-height: 100vh;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
max-width: 1280px;
margin: 0 auto;
text-align: center;
}
.result {
height: 20px;
line-height: 20px;
margin: 1.5rem auto;
text-align: center;
}
.footer {
margin-top: 1rem;
align-content: center;
text-align: center;
color: rgba(255, 255, 255, 0.67);
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
.input-box .btn:hover {
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
color: #333333;
}
.input-box .input {
border: none;
border-radius: 3px;
outline: none;
height: 30px;
line-height: 30px;
padding: 0 10px;
color: black;
background-color: rgba(240, 240, 240, 1);
-webkit-font-smoothing: antialiased;
}
.input-box .input:hover {
border: none;
background-color: rgba(255, 255, 255, 1);
}
.input-box .input:focus {
border: none;
background-color: rgba(255, 255, 255, 1);
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

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

View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -0,0 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

View File

@@ -1,20 +1,7 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"noUnusedParameters": false,
"noImplicitAny": false,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -1,9 +1,24 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,7 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
})