add chat
Main daploy / deploy (push) Successful in 45s

This commit is contained in:
2024-11-20 01:59:52 +02:00
parent d2eb6f02fd
commit 99ff4beba3
18 changed files with 626 additions and 68 deletions
+4
View File
@@ -8,6 +8,9 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
Button: typeof import('primevue/button')['default']
Card: typeof import('primevue/card')['default']
Chat: typeof import('./src/components/ChatComponent.vue')['default']
ChatComponent: typeof import('./src/components/ChatComponent.vue')['default']
Checkbox: typeof import('primevue/checkbox')['default']
ContactInfo: typeof import('./src/components/read/ReadContactInfo.vue')['default']
CreateActionsSection: typeof import('./src/components/create/CreateActionsSection.vue')['default']
@@ -35,6 +38,7 @@ declare module 'vue' {
MessageSender: typeof import('./src/components/read/ReadMessageSender.vue')['default']
PlacementInstructions: typeof import('./src/components/manage/ManagePlacementInstructions.vue')['default']
QRCodeReadySection: typeof import('./src/components/create/CreateReadySection.vue')['default']
QueryRender: typeof import('./src/components/QueryRender.vue')['default']
RadioButton: typeof import('primevue/radiobutton')['default']
ReadContactInfo: typeof import('./src/components/read/ReadContactInfo.vue')['default']
ReadMessageSender: typeof import('./src/components/read/ReadMessageSender.vue')['default']
+233 -1
View File
@@ -16,10 +16,12 @@
"md5": "^2.3.0",
"pinia": "^2.2.6",
"pouchdb-browser": "^9.0.0",
"pouchdb-find": "^9.0.0",
"primeicons": "^7.0.0",
"primevue": "^4.2.1",
"qr-code-styling": "^1.8.4",
"svgo": "^3.3.2",
"tailwind-scrollbar": "^3.1.0",
"tailwindcss-primeui": "^0.3.4",
"vue": "^3.5.12",
"vue-query": "^1.26.0",
@@ -30,6 +32,7 @@
"@primevue/auto-import-resolver": "^4.2.1",
"@tsconfig/node20": "^20.1.4",
"@types/jsdom": "^21.1.7",
"@types/md5": "^2.3.5",
"@types/node": "^20.17.6",
"@types/pouchdb-browser": "^6.1.5",
"@vitejs/plugin-vue": "^5.1.4",
@@ -1911,6 +1914,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/md5": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.5.tgz",
"integrity": "sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ms": {
"version": "0.7.34",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
@@ -4126,6 +4136,40 @@
"reusify": "^1.0.4"
}
},
"node_modules/fetch-cookie": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz",
"integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==",
"license": "Unlicense",
"dependencies": {
"set-cookie-parser": "^2.4.8",
"tough-cookie": "^4.0.0"
}
},
"node_modules/fetch-cookie/node_modules/tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"license": "BSD-3-Clause",
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/fetch-cookie/node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -5281,6 +5325,48 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz",
"integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-fetch/node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/node-fetch/node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/node-fetch/node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/node-releases": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
@@ -5978,6 +6064,27 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
"node_modules/pouchdb-abstract-mapreduce": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/pouchdb-abstract-mapreduce/-/pouchdb-abstract-mapreduce-9.0.0.tgz",
"integrity": "sha512-SnTtqwAEiAa3uxKbc1J7LfiBViwEkKe2xkK92zxyTXPqWBvMnh4UU3GXxx7GrXTM4L9llsQ3lSjpbH4CNqG1Mw==",
"license": "Apache-2.0",
"dependencies": {
"pouchdb-binary-utils": "9.0.0",
"pouchdb-collate": "9.0.0",
"pouchdb-errors": "9.0.0",
"pouchdb-fetch": "9.0.0",
"pouchdb-mapreduce-utils": "9.0.0",
"pouchdb-md5": "9.0.0",
"pouchdb-utils": "9.0.0"
}
},
"node_modules/pouchdb-binary-utils": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/pouchdb-binary-utils/-/pouchdb-binary-utils-9.0.0.tgz",
"integrity": "sha512-2OMtgDZi82vqs+zNDE0YiYjOaWkYCUcZJZKK3WkRr+XYRu+2B7umJrnygJFhUwoGedBbHSrlQBLhdNV3F1AX1A==",
"license": "Apache-2.0"
},
"node_modules/pouchdb-browser": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/pouchdb-browser/-/pouchdb-browser-9.0.0.tgz",
@@ -5989,6 +6096,83 @@
"vuvuzela": "1.0.3"
}
},
"node_modules/pouchdb-collate": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/pouchdb-collate/-/pouchdb-collate-9.0.0.tgz",
"integrity": "sha512-TrnEDNZEmIIl+W3xKUO8h+geqVLQ90oZe5ujPkl8myUzpREULWXWQBnV5EzPXVEKDBpJlb8T3I6oy/zdWGQpdA==",
"license": "Apache-2.0"
},
"node_modules/pouchdb-errors": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/pouchdb-errors/-/pouchdb-errors-9.0.0.tgz",
"integrity": "sha512-961PSMLhW0UqqdJ566g+CdLZ5pkBJRd6l4WWpCDdD0USvE4xYfYGzv43w7nZZBw1k3Xdy092yqPge7yX/tfnyw==",
"license": "Apache-2.0"
},
"node_modules/pouchdb-fetch": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/pouchdb-fetch/-/pouchdb-fetch-9.0.0.tgz",
"integrity": "sha512-TbE3cUcAJQrwb9kr44tDP0X+NAbcqgjsTvcL30L4xzBNJeCPTIRjukYX80s154SHJUXBxcWRiPsMmNqpXsjfCA==",
"license": "Apache-2.0",
"dependencies": {
"fetch-cookie": "2.2.0",
"node-fetch": "2.6.9"
}
},
"node_modules/pouchdb-find": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/pouchdb-find/-/pouchdb-find-9.0.0.tgz",
"integrity": "sha512-vvVhq4eEOmSkwSRwf2NBYtdhURB7ryJ7sUI4WDN00GuLUj2g8jAXBJuZIryVgdYt/5S5cfn70iRL6Eow+LFhpA==",
"license": "Apache-2.0",
"dependencies": {
"pouchdb-abstract-mapreduce": "9.0.0",
"pouchdb-collate": "9.0.0",
"pouchdb-errors": "9.0.0",
"pouchdb-fetch": "9.0.0",
"pouchdb-md5": "9.0.0",
"pouchdb-selector-core": "9.0.0",
"pouchdb-utils": "9.0.0"
}
},
"node_modules/pouchdb-mapreduce-utils": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/pouchdb-mapreduce-utils/-/pouchdb-mapreduce-utils-9.0.0.tgz",
"integrity": "sha512-Bjh8W6QXqp1j7MKmHhYYp5cYlcQsm5drD8Jd/F+ZlfNt18uiD2SQXWzGM5797+tiW/LszFGb8ttw0uHWjxufCQ==",
"license": "Apache-2.0",
"dependencies": {
"pouchdb-utils": "9.0.0"
}
},
"node_modules/pouchdb-md5": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/pouchdb-md5/-/pouchdb-md5-9.0.0.tgz",
"integrity": "sha512-58xUYBvW3/s+aH0j4uOhhN8yCk0LQ254cxBzI/gbKA9PrfwHpe4zrr0L/ia5ml3A30oH1f8aTnuVMwWDkFcuww==",
"license": "Apache-2.0",
"dependencies": {
"pouchdb-binary-utils": "9.0.0",
"spark-md5": "3.0.2"
}
},
"node_modules/pouchdb-selector-core": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/pouchdb-selector-core/-/pouchdb-selector-core-9.0.0.tgz",
"integrity": "sha512-ZYHYsdoedwm8j5tYofz+3+uUSK8i+7tRCBb01T0OuqDQb17+w5mzjHF8Ppi160xdPUPaWCo1Un+nLWGJzkmA3g==",
"license": "Apache-2.0",
"dependencies": {
"pouchdb-collate": "9.0.0",
"pouchdb-utils": "9.0.0"
}
},
"node_modules/pouchdb-utils": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/pouchdb-utils/-/pouchdb-utils-9.0.0.tgz",
"integrity": "sha512-xWZE5c+nAslgmLC8JBZbky8AYgdz7pKtv7KTSi6CD2tuQD0WyNKib0YnhZndeE84dksTeZlqlg56RQHsHoB2LQ==",
"license": "Apache-2.0",
"dependencies": {
"pouchdb-errors": "9.0.0",
"pouchdb-md5": "9.0.0",
"uuid": "8.3.2"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -6056,11 +6240,19 @@
"dev": true,
"license": "ISC"
},
"node_modules/psl": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.10.0.tgz",
"integrity": "sha512-KSKHEbjAnpUuAUserOq0FxGXCUrzC3WniuSJhvdbs102rL55266ZcHBqLWOsG30spQMlPdpy7icATiAQehg/iA==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -6093,6 +6285,12 @@
"integrity": "sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==",
"license": "MIT"
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"license": "MIT"
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -6199,6 +6397,12 @@
"integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==",
"license": "MIT"
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -6416,6 +6620,12 @@
"node": ">=10"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -6775,6 +6985,18 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/tailwind-scrollbar": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-3.1.0.tgz",
"integrity": "sha512-pmrtDIZeHyu2idTejfV59SbaJyvp1VRjYxAjZBH0jnyrPRo6HL1kD5Glz8VPagasqr6oAx6M05+Tuw429Z8jxg==",
"license": "MIT",
"engines": {
"node": ">=12.13.0"
},
"peerDependencies": {
"tailwindcss": "3.x"
}
},
"node_modules/tailwindcss": {
"version": "3.4.14",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz",
@@ -7184,6 +7406,16 @@
"punycode": "^2.1.0"
}
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"license": "MIT",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+3
View File
@@ -27,10 +27,12 @@
"md5": "^2.3.0",
"pinia": "^2.2.6",
"pouchdb-browser": "^9.0.0",
"pouchdb-find": "^9.0.0",
"primeicons": "^7.0.0",
"primevue": "^4.2.1",
"qr-code-styling": "^1.8.4",
"svgo": "^3.3.2",
"tailwind-scrollbar": "^3.1.0",
"tailwindcss-primeui": "^0.3.4",
"vue": "^3.5.12",
"vue-query": "^1.26.0",
@@ -41,6 +43,7 @@
"@primevue/auto-import-resolver": "^4.2.1",
"@tsconfig/node20": "^20.1.4",
"@types/jsdom": "^21.1.7",
"@types/md5": "^2.3.5",
"@types/node": "^20.17.6",
"@types/pouchdb-browser": "^6.1.5",
"@vitejs/plugin-vue": "^5.1.4",
+4
View File
@@ -0,0 +1,4 @@
import PouchDB from 'pouchdb-browser'
import PouchDBFindPlugin from 'pouchdb-find'
export default PouchDB.plugin(PouchDBFindPlugin)
+12 -2
View File
@@ -1,8 +1,18 @@
import PouchDB from 'pouchdb-browser'
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import PouchDB from '@/api/PouchDB'
import type { QRCodeDocument, MessageDocument } from '@/types/DBDocumentTypes'
export const qrCodeDbRemote = new PouchDB<QRCodeDocument>('https://hereconnect.condev.ru/api/qr', {
skip_setup: true,
})
export const qrCodeDbLocal = new PouchDB<QRCodeDocument>('qr')
export const messagesDbLocal = new PouchDB<MessageDocument>('messages')
export const messagesDbRemote = new PouchDB<MessageDocument>(
'https://hereconnect.condev.ru/api/messages',
{
skip_setup: true,
},
)
+193
View File
@@ -0,0 +1,193 @@
<template>
<div class="flex flex-col flex-grow">
<!-- Список сообщений -->
<div class="flex-grow overflow-y-auto px-4 py-2" ref="messagesContainer">
<div
v-for="message in messages"
:key="message._id"
class="mb-4 max-w-md"
:class="{ 'ml-auto': message.from === userRole, 'mr-auto': message.from !== userRole }"
>
<Card
class="p-card-sm"
:class="{
'bg-blue-50 border-blue-300 text-blue-500': message.from === userRole,
'bg-gray-50 border-gray-300 text-gray-700': message.from !== userRole,
}"
>
<template #content>
{{ message.body }}
</template>
</Card>
</div>
<slot name="endOfMessages" :count="messages.length"></slot>
</div>
<!-- Форма отправки сообщений -->
<form @submit.prevent="sendMessage" class="flex gap-2 items-center" ref="form">
<InputText
id="newMessage"
v-model="newMessage"
class="flex-1"
placeholder="Напишите сообщение"
/>
<Button type="submit" icon="pi pi-send" class="p-button-md" :disabled="!newMessage.trim()" />
</form>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, useTemplateRef, watch, nextTick } from 'vue'
import { messagesDbLocal as db, messagesDbRemote as remoteDb } from '@/api/dbs'
import { type MessageDocument } from '@/types/DBDocumentTypes'
const formRef = useTemplateRef('form')
const messagesContainerRef = useTemplateRef('messagesContainer')
const { qr_code_uri, chat, userRole } = defineProps<{
qr_code_uri: MessageDocument['qr_code_uri']
chat: MessageDocument['chat']
userRole: MessageDocument['from']
}>()
// Локальные состояния
const messages = ref<MessageDocument[]>([])
const newMessage = ref('')
let changesSubscription: PouchDB.Core.Changes<MessageDocument> | null = null
let syncHandler: PouchDB.Replication.Sync<MessageDocument> | null = null
// Селектор для фильтрации сообщений
const messageSelector = {
type: 'message',
qr_code_uri,
chat,
}
// Загрузка сообщений
async function loadMessages() {
try {
const result = await db.find({
selector: messageSelector,
})
messages.value = result.docs as MessageDocument[]
} catch (error) {
console.error('Ошибка загрузки сообщений:', error)
}
}
// Отправка нового сообщения
async function sendMessage() {
const created_at = new Date().toISOString()
const message: MessageDocument = {
_id: `message:${qr_code_uri}:${chat}:${created_at}`,
type: 'message',
qr_code_uri,
chat,
created_at,
from: userRole,
body: newMessage.value.trim(),
}
try {
await db.put(message)
newMessage.value = ''
} catch (error) {
console.error('Ошибка отправки сообщения:', error)
}
}
// Отслеживание изменений
function watchChanges() {
changesSubscription = db
.changes({
since: 'now',
live: true,
include_docs: true,
selector: messageSelector,
})
.on('change', (change: { doc?: MessageDocument }) => {
if (change.doc) {
const index = messages.value.findIndex((m) => m._id === change.doc!._id)
if (index === -1) {
// Если сообщения нет, добавляем его
messages.value.push(change.doc)
} else {
// Если сообщение существует, обновляем его
messages.value[index] = change.doc
}
// Сортируем массив сообщений
messages.value.sort((a, b) => (a.created_at > b.created_at ? 1 : -1))
}
})
.on('error', (err) => {
console.error('Ошибка отслеживания изменений:', err)
})
}
// Настройка синхронизации
function setupSync() {
syncHandler = db.sync(remoteDb, {
live: true,
retry: true,
selector: messageSelector,
})
syncHandler.on('error', (err) => {
console.error('Ошибка синхронизации:', err)
})
}
// Остановка синхронизации
function stopSync() {
if (syncHandler) {
syncHandler.cancel()
syncHandler = null
}
}
// Отписка от изменений
function stopWatchingChanges() {
if (changesSubscription) {
changesSubscription.cancel()
changesSubscription = null
}
}
// Логика жизненного цикла
onMounted(async () => {
watchChanges()
setupSync()
await loadMessages()
formRef.value?.querySelector('input')?.focus()
})
watch(
() => messages.value?.length,
async () => {
if (messagesContainerRef.value) {
await nextTick()
messagesContainerRef.value.scrollTop = messagesContainerRef.value.scrollHeight
}
},
)
onUnmounted(() => {
stopWatchingChanges()
stopSync()
})
</script>
<style scoped>
/* Чат занимает всё доступное пространство */
.flex-grow {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.overflow-y-auto {
overflow-y: auto;
height: 0;
flex-grow: 1;
}
</style>
+19
View File
@@ -0,0 +1,19 @@
<script setup lang="ts" generic="R, E">
import type { UseQueryReturnType } from '@tanstack/vue-query'
type Props = {
query: UseQueryReturnType<R, E>
}
const { query } = defineProps<Props>()
const { data, error, refetch, isSuccess, isError, isLoading } = query
</script>
<template>
<slot v-if="isSuccess" name="default" :data="data! as R" />
<slot v-else-if="isLoading" name="loading"><p class="text-gray-500">Загрузка данных...</p></slot>
<slot v-else-if="isError" name="error" :error="error as E" :reload="refetch">
Ошибка загрузки данных: {{ error && 'message' in error ? error.message : error }}
</slot>
</template>
+2 -3
View File
@@ -48,10 +48,9 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import md5 from 'md5'
import type { ContactInfo } from '@/types/DBDocumentTypes'
const { contactInfo } = defineProps<{
contactInfo: { name?: string; phone?: string; email?: string }
}>()
const { contactInfo } = defineProps<{ contactInfo: ContactInfo }>()
const success = ref(false)
const error = ref<string | null>(null)
+32 -12
View File
@@ -1,14 +1,14 @@
<template>
<section class="max-w-prose w-full">
<p class="text-gray-600 mb-4 text-center" v-if="presetMessages.length">
<p class="text-gray-600 mb-4 text-center" v-if="doc.predefinedMessages?.length">
Выберите сообщение из списка или напишите своё:
</p>
<!-- Предустановленные сообщения -->
<div v-if="presetMessages.length" class="mb-6">
<div v-if="doc.predefinedMessages?.length" class="mb-6">
<div class="flex flex-col gap-3">
<Button
v-for="(message, index) in presetMessages"
v-for="(message, index) in doc.predefinedMessages"
:key="index"
class="p-button-outlined w-full text-left"
:disabled="loading"
@@ -43,31 +43,51 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { messagesDbLocal as db } from '@/api/dbs'
import type { MessageDocument, QRCodeDocument } from '@/types/DBDocumentTypes'
import { ROUTE_NAMES } from '@/constants/routes'
const { qr_code_uri, predefinedMessages } = defineProps<{
qr_code_uri: string
predefinedMessages: string[]
const { doc } = defineProps<{
doc: QRCodeDocument
}>()
const router = useRouter()
// Локальные состояния
const presetMessages = ref(predefinedMessages || [])
const customMessage = ref('')
const success = ref(false)
const error = ref<string | null>(null)
const loading = ref(false)
// Генерация идентификатора чата
// Функция для отправки сообщения
async function sendMessage(message: string) {
async function sendMessage(content: string) {
try {
loading.value = true
console.log({
qr_code_uri,
message: message.trim(),
})
const chat = (new Date().getTime() - new Date(doc.created_at).getTime()).toString(36)
const message: MessageDocument = {
_id: `message:${doc.uri}:${chat}:${new Date().toISOString()}`,
type: 'message',
qr_code_uri: doc.uri,
chat,
created_at: new Date().toISOString(),
from: 'guest',
body: content.trim(),
}
// Сохраняем сообщение в локальную базу данных
await db.put(message)
success.value = true
error.value = null
customMessage.value = ''
// Перенаправляем на страницу чата
await router.push({ name: ROUTE_NAMES.PUBLIC_CHAT, params: { qr_code_uri: doc.uri, chat } })
} catch {
success.value = false
error.value = 'Ошибка отправки сообщения'
+18
View File
@@ -0,0 +1,18 @@
export const ROUTE_NAMES = {
HOME: 'home',
CREATE: 'create',
MANAGE_QR_CODE_READY: 'manage-qr-code-ready',
READ_QR_CODE: 'read-qr-code',
DONATE: 'donate',
PUBLIC_CHAT: 'public-chat',
// CREATE_ACTIONS: 'create-actions',
// MANAGE_SETTINGS: 'manage-settings',
// MANAGE_PROFILE: 'manage-profile',
// MANAGE_CHAT: 'manage-chat',
// PUBLIC_QR: 'public-qr',
// HELP: 'help',
// ABOUT: 'about',
// CONTACT: 'contact',
// PRIVACY_POLICY: 'privacy-policy',
// TERMS_OF_SERVICE: 'terms-of-service',
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { createApp } from 'vue'
import { VueQueryPlugin } from '@tanstack/vue-query'
import { createPinia } from 'pinia'
import PouchDB from 'pouchdb-browser'
import PouchDB from '@/api/PouchDB'
import PrimeVue from 'primevue/config'
import { definePreset } from '@primevue/themes'
+23 -23
View File
@@ -1,91 +1,91 @@
import { createRouter, createWebHistory } from 'vue-router'
import { ROUTE_NAMES } from '@/constants/routes'
import HomeView from '@/views/HomeView.vue'
const routes = [
{
path: '/',
name: 'home',
name: ROUTE_NAMES.HOME,
component: HomeView,
},
{
path: '/create',
name: ROUTE_NAMES.CREATE,
component: () => import('@/views/CreateView.vue'),
},
{
path: '/manage/:qr_code_uri/ready',
name: 'ManageQRCodeReady',
name: ROUTE_NAMES.MANAGE_QR_CODE_READY,
component: () => import('@/views/ManageQRCodeReadyView.vue'),
props: true,
},
{
path: '/read/:qr_code_uri',
name: 'ReadQRCode',
name: ROUTE_NAMES.READ_QR_CODE,
component: () => import('@/views/ReadQRCodeView.vue'),
props: true,
},
{
path: '/donate',
name: 'Donate',
name: ROUTE_NAMES.DONATE,
component: () => import('@/views/DonateView.vue'),
},
{
path: '/chat/:qr_code_uri/:chat',
name: ROUTE_NAMES.PUBLIC_CHAT,
component: () => import('@/views/PublicChatView.vue'),
props: true,
},
// Закомментированные маршруты
// {
// path: '/create/actions',
// name: 'create-actions',
// name: ROUTE_NAMES.CREATE_ACTIONS,
// component: () => import('@/views/CreateActionsView.vue'), // Второй шаг создания QR‑кода
// },
// {
// path: '/manage/:qr_code/settings',
// name: 'manage-settings',
// name: ROUTE_NAMES.MANAGE_SETTINGS,
// component: () => import('@/views/SettingsView.vue'), // Настройки QR‑кода для владельца
// },
// {
// path: '/manage/profile',
// name: 'manage-profile',
// name: ROUTE_NAMES.MANAGE_PROFILE,
// component: () => import('@/views/ProfileView.vue'), // Личный кабинет владельца
// },
// {
// path: '/manage/:qr_code/:chat',
// name: 'manage-chat',
// name: ROUTE_NAMES.MANAGE_CHAT,
// component: () => import('@/views/ChatView.vue'), // Чат для владельца
// },
// {
// path: '/chat/:qr_code/:chat',
// name: 'public-chat',
// component: () => import('@/views/PublicChatView.vue'), // Публичный чат для гостей
// },
// {
// path: '/:qr_code',
// name: 'public-qr',
// name: ROUTE_NAMES.PUBLIC_QR,
// component: () => import('@/views/PublicQRView.vue'), // Публичная страница QR‑кода
// },
// {
// path: '/help',
// name: 'help',
// name: ROUTE_NAMES.HELP,
// component: () => import('@/views/HelpView.vue'), // Страница помощи и FAQ
// },
// {
// path: '/about',
// name: 'about',
// name: ROUTE_NAMES.ABOUT,
// component: () => import('@/views/AboutView.vue'), // Страница информации о компании и проекте
// },
// {
// path: '/contact',
// name: 'contact',
// name: ROUTE_NAMES.CONTACT,
// component: () => import('@/views/ContactView.vue'), // Страница с контактной информацией
// },
// {
// path: '/privacy-policy',
// name: 'privacy-policy',
// name: ROUTE_NAMES.PRIVACY_POLICY,
// component: () => import('@/views/PrivacyPolicyView.vue'), // Политика конфиденциальности
// },
// {
// path: '/terms-of-service',
// name: 'terms-of-service',
// name: ROUTE_NAMES.TERMS_OF_SERVICE,
// component: () => import('@/views/TermsOfServiceView.vue'), // Условия использования
// },
]
+1 -1
View File
@@ -9,7 +9,7 @@ export interface MessageDocument extends BaseDocument {
type: 'message'
/** Идентификатор QR‑кода, к которому относится сообщение */
qr_code: string
qr_code_uri: string
/** Идентификатор чата для связывания сообщений */
chat: string
@@ -1,5 +1,11 @@
import type { BaseDocument } from './BaseDocument'
export type ContactInfo = {
name?: string
phone?: string
email?: string
}
/**
* Интерфейс для документа QR‑кода.
* Представляет QR‑код, созданный пользователем, и информацию о привязанном объекте.
@@ -30,4 +36,6 @@ export interface QRCodeDocument extends BaseDocument {
/** Предустановленное сообщение для гостей */
predefinedMessages?: string[]
contactInfo?: ContactInfo
}
+5 -1
View File
@@ -47,6 +47,7 @@ import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { createQrCodeDocument } from '@/api/qrCode'
import type { QRCodeDocument } from '@/types/DBDocumentTypes'
import { ROUTE_NAMES } from '@/constants/routes'
// Состояния шагов
const placement = ref<null | QRCodeDocument['placement']>(null)
@@ -70,7 +71,10 @@ async function createQrCode() {
})
error.value = null
await router.push({ name: 'ManageQRCodeReady', params: { qr_code_uri: qrCodeDocument.uri } })
await router.push({
name: ROUTE_NAMES.MANAGE_QR_CODE_READY,
params: { qr_code_uri: qrCodeDocument.uri },
})
} catch (err) {
error.value = (err as Error).message || 'Неизвестная ошибка'
}
+51
View File
@@ -0,0 +1,51 @@
<template>
<div class="flex flex-col min-h-screen">
<!-- Основной контент -->
<main class="flex-grow flex flex-col">
<section class="flex flex-col flex-grow max-w-prose w-full mx-auto p-4">
<ChatComponent userRole="guest" :qr_code_uri="qr_code_uri" :chat="chat">
<template #endOfMessages="{ count }">
<!-- Уведомление, если гость отправил только одно сообщение -->
<div
v-if="count === 1"
class="flex flex-col items-center justify-center text-center bg-gray-50 border border-gray-200 rounded-lg shadow-md p-6 mt-6"
>
<p class="text-gray-700 font-medium text-lg">Ваше сообщение отправлено.</p>
<p class="text-gray-600 mt-2">
Вы можете написать ещё одно сообщение или подождать ответа.
</p>
</div>
<p class="mt-10 text-sm text-gray-500 text-center">
Хотите создать собственный QRкод?
<a href="/create" class="underline">Создайте его здесь</a>.
</p>
<p class="mt-1 text-sm text-gray-500 text-center">
Сделайте НаСвязи удобнее. Мы будем рады вашей&nbsp;<RouterLink
to="/donate"
class="underline"
>поддержке</RouterLink
>.
</p>
</template>
</ChatComponent>
</section>
</main>
<!-- Футер -->
<!-- <footer class="bg-gray-100 text-center py-4 border-t text-sm text-gray-700">-->
<!-- <p><a href="/create" class="underline">Создайте свой QRкод</a>.</p>-->
<!-- <p class="mt-1">-->
<!-- Сделайте НаСвязи удобнее. Будем рады вашей&nbsp;<a href="/donate" class="underline"-->
<!-- >поддержке</a-->
<!-- >.-->
<!-- </p>-->
<!-- </footer>-->
</div>
</template>
<script setup lang="ts">
import type { MessageDocument } from '@/types/DBDocumentTypes'
import ChatComponent from '@/components/ChatComponent.vue'
defineProps<{ qr_code_uri: MessageDocument['qr_code_uri']; chat: MessageDocument['chat'] }>()
</script>
+16 -23
View File
@@ -2,14 +2,15 @@
<div class="flex flex-col min-h-screen">
<main class="flex-grow flex items-center justify-center py-6 px-4 md:px-6">
<section class="max-w-prose w-full flex flex-col gap-10">
<template v-for="action in actions">
<ReadContactInfo v-if="action === 'viewContactInfo'" :contactInfo="contactInfo" />
<ReadMessageSender
v-else-if="action === 'sendMessage'"
:qr_code_uri="qr_code_uri"
:predefinedMessages="predefinedMessages"
/>
</template>
<QueryRender :query #default="{ data: doc }">
<template v-for="action in doc.actions" :key="action">
<ReadContactInfo
v-if="action === 'viewContactInfo' && doc.contactInfo"
:contactInfo="doc.contactInfo"
/>
<ReadMessageSender v-else-if="action === 'sendMessage'" :doc />
</template>
</QueryRender>
</section>
</main>
@@ -28,24 +29,16 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useQuery } from '@tanstack/vue-query'
import { getQrCodeDocument } from '@/api/qrCode'
import type { QRCodeDocument } from '@/types/DBDocumentTypes'
import QueryRender from '@/components/QueryRender.vue'
const { qr_code_uri } = defineProps<{ qr_code_uri: string }>()
// Локальные состояния
const predefinedMessages = ref<string[]>([])
const contactInfo = ref<{ name?: string; phone?: string; email?: string }>({})
const actions = ref<string[]>([])
onMounted(async () => {
try {
const qrCodeDoc = await getQrCodeDocument(qr_code_uri)
predefinedMessages.value = qrCodeDoc!.predefinedMessage || []
contactInfo.value = qrCodeDoc!.contactInfo || { name: 'waka', phone: '+123' }
actions.value = qrCodeDoc!.actions || []
} catch (err) {
console.error(err)
}
// Хук для загрузки данных QR-кода
const query = useQuery({
queryKey: ['qrCodeData', qr_code_uri],
queryFn: (): Promise<QRCodeDocument> => getQrCodeDocument(qr_code_uri),
})
</script>
+1 -1
View File
@@ -4,5 +4,5 @@ export default {
theme: {
extend: {},
},
plugins: [require('tailwindcss-primeui')],
plugins: [require('tailwindcss-primeui'), require('tailwind-scrollbar')],
}