init
This commit is contained in:
parent
d3b139327b
commit
ccda81b4be
|
@ -0,0 +1,2 @@
|
|||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
|
@ -0,0 +1,107 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
data/
|
||||
error
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,9 @@
|
|||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
DarkModeSwitcher: typeof import('./src/components/DarkModeSwitcher.vue')['default']
|
||||
Edit: typeof import('./src/components/icons/Edit.vue')['default']
|
||||
ElCarousel: typeof import('element-plus/es')['ElCarousel']
|
||||
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
Header: typeof import('./src/components/Header.vue')['default']
|
||||
Moon: typeof import('./src/components/icons/Moon.vue')['default']
|
||||
Play: typeof import('./src/components/icons/Play.vue')['default']
|
||||
Player: typeof import('./src/components/Player.vue')['default']
|
||||
RoomList: typeof import('./src/components/RoomList.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Sun: typeof import('./src/components/icons/Sun.vue')['default']
|
||||
Trash: typeof import('./src/components/icons/Trash.vue')['default']
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="cn">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SyncTV</title>
|
||||
<link
|
||||
href="https://font.sec.miui.com/font/css?family=MiSans:400,500,600,700:Chinese_Simplify,Latin,Chinese_Traditional&display=swap"
|
||||
rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"name": "synctv-web",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite --port 8085 --strictPort",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^10.4.1",
|
||||
"artplayer": "^5.0.9",
|
||||
"artplayer-plugin-danmuku": "^5.0.1",
|
||||
"axios": "^1.4.0",
|
||||
"element-plus": "^2.3.8",
|
||||
"gsap": "^3.12.2",
|
||||
"less": "^4.1.3",
|
||||
"less-loader": "^11.1.3",
|
||||
"mpegts.js": "^1.7.3",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.4",
|
||||
"terser": "^5.18.2",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.2",
|
||||
"@tsconfig/node18": "^2.0.1",
|
||||
"@types/node": "^18.16.19",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@vue/eslint-config-prettier": "^7.1.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-plugin-vue": "^9.15.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.26",
|
||||
"prettier": "^2.8.8",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "~5.0.4",
|
||||
"unplugin-auto-import": "^0.16.6",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^4.4.2",
|
||||
"vue-tsc": "^1.8.4"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
|
@ -0,0 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import Header from "./components/Header.vue";
|
||||
import { RouterView } from "vue-router";
|
||||
import { roomStore } from "./stores/room";
|
||||
const room = roomStore();
|
||||
|
||||
localStorage.login === "true" ? (room.login = true) : false;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Header />
|
||||
<el-container>
|
||||
<el-main>
|
||||
<RouterView />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
|
@ -0,0 +1,183 @@
|
|||
@private-color: rgb(23, 108, 255);
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: MiSans;
|
||||
transition: background 0.5s;
|
||||
@apply dark:bg-black dark:text-zinc-200;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
transition: all 0.6s;
|
||||
}
|
||||
|
||||
.l-input {
|
||||
margin: 10px;
|
||||
padding: 10px 8px;
|
||||
border: 2px solid #4b4b4b;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s;
|
||||
@apply dark:bg-zinc-900;
|
||||
&:hover {
|
||||
border: 2px solid @private-color;
|
||||
}
|
||||
&:focus {
|
||||
@apply outline-none;
|
||||
border: 2px solid @private-color;
|
||||
box-shadow: 0 0 10px #176cff70;
|
||||
}
|
||||
}
|
||||
|
||||
.l-input-violet {
|
||||
@apply p-2 rounded-lg border-2 border-violet-500 bg-violet-50 text-violet-800 dark:bg-violet-950 dark:text-violet-200;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
@apply border-violet-600;
|
||||
}
|
||||
&:focus {
|
||||
@apply border-violet-500 outline-none shadow-md;
|
||||
box-shadow: 0 0 10px #c4b5fd;
|
||||
}
|
||||
}
|
||||
|
||||
.l-input-slate {
|
||||
@apply p-2 rounded-lg border-2 border-slate-500 bg-slate-100 text-slate-800 dark:bg-gray-800 dark:text-slate-200;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
@apply border-slate-600;
|
||||
}
|
||||
&:focus {
|
||||
@apply border-slate-500 outline-none shadow-md;
|
||||
box-shadow: 0 0 10px #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
@apply transition-all duration-500 text-blue-500 hover:text-blue-300;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 6px 10px;
|
||||
transition: all 0.3s;
|
||||
@apply bg-blue-600 text-white cursor-pointer border-2 border-solid rounded-lg dark:border-blue-900;
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px #176cff70;
|
||||
border: 2px solid @private-color;
|
||||
padding: 6px 15px;
|
||||
color: @private-color;
|
||||
|
||||
background-color: rgba(23, 108, 255, 0.16);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(0.2);
|
||||
&:hover {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-dense {
|
||||
padding: 4px 8px;
|
||||
@apply text-sm;
|
||||
|
||||
&:hover {
|
||||
padding: 5px 9px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
@apply bg-green-600 dark:border-green-900;
|
||||
&:hover {
|
||||
@apply border-green-600 text-green-600 bg-green-100 shadow-green-500 shadow-lg dark:bg-green-950 dark:border-green-800;
|
||||
box-shadow: 0 0 10px #8cebaf;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
@apply bg-yellow-500 dark:border-yellow-800;
|
||||
&:hover {
|
||||
@apply border-yellow-500 text-yellow-600 bg-yellow-100 shadow-yellow-500 shadow-lg dark:bg-[#493c08] dark:backdrop-blur-sm;
|
||||
box-shadow: 0 0 10px #facc15;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-error {
|
||||
@apply bg-red-500 dark:border-red-800;
|
||||
&:hover {
|
||||
@apply border-red-500 text-red-600 bg-red-100 shadow-red-500 shadow-lg dark:bg-[#4b0000];
|
||||
box-shadow: 0 0 10px #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
// 通知样式
|
||||
#notifyBox {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 15px;
|
||||
width: 233px;
|
||||
.notifyyy {
|
||||
background-color: #176cff;
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片
|
||||
.card {
|
||||
border-radius: 1.3rem;
|
||||
transition: all 0.5s;
|
||||
@apply bg-gray-100 dark:bg-zinc-900;
|
||||
|
||||
.card-title {
|
||||
@apply pl-5 p-4 text-xl font-bold;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
@apply px-5;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
@apply p-5 flex justify-end;
|
||||
}
|
||||
}
|
||||
|
||||
// 播放器
|
||||
.artplayer-app {
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
|
||||
.art-video-player {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.art-danmuku-emitter,
|
||||
.art-layer-danmuku-emitter {
|
||||
display: none !important;
|
||||
}
|
||||
.art-video-player {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.el-main {
|
||||
padding: 0 !important;
|
||||
|
||||
.el-row {
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
|
||||
.el-col {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.art-control-fullscreenWeb {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
html,
|
||||
body,
|
||||
div,
|
||||
span,
|
||||
applet,
|
||||
object,
|
||||
iframe,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
blockquote,
|
||||
pre,
|
||||
a,
|
||||
abbr,
|
||||
acronym,
|
||||
address,
|
||||
big,
|
||||
cite,
|
||||
code,
|
||||
del,
|
||||
dfn,
|
||||
em,
|
||||
img,
|
||||
ins,
|
||||
kbd,
|
||||
q,
|
||||
s,
|
||||
samp,
|
||||
small,
|
||||
strike,
|
||||
strong,
|
||||
sub,
|
||||
sup,
|
||||
tt,
|
||||
var,
|
||||
b,
|
||||
u,
|
||||
i,
|
||||
center,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
ol,
|
||||
ul,
|
||||
li,
|
||||
fieldset,
|
||||
form,
|
||||
label,
|
||||
legend,
|
||||
table,
|
||||
caption,
|
||||
tbody,
|
||||
tfoot,
|
||||
thead,
|
||||
tr,
|
||||
th,
|
||||
td,
|
||||
article,
|
||||
aside,
|
||||
canvas,
|
||||
details,
|
||||
embed,
|
||||
figure,
|
||||
figcaption,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
output,
|
||||
ruby,
|
||||
section,
|
||||
summary,
|
||||
time,
|
||||
mark,
|
||||
audio,
|
||||
video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
blockquote,
|
||||
q {
|
||||
quotes: none;
|
||||
}
|
||||
|
||||
blockquote:before,
|
||||
blockquote:after,
|
||||
q:before,
|
||||
q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 106 KiB |
|
@ -0,0 +1,31 @@
|
|||
<script setup lang="ts">
|
||||
import SunIcon from "./icons/Sun.vue";
|
||||
import { roomStore } from "@/stores/room";
|
||||
const room = roomStore();
|
||||
|
||||
if (
|
||||
localStorage.darkMode === "true" ||
|
||||
(!("darkMode" in localStorage) &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
) {
|
||||
document.documentElement.classList.add("dark");
|
||||
room.isDarkMode = true;
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
room.isDarkMode = false;
|
||||
}
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
localStorage.darkMode = room.isDarkMode = !room.isDarkMode;
|
||||
room.isDarkMode
|
||||
? document.documentElement.classList.add("dark")
|
||||
: document.documentElement.classList.remove("dark");
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span @click="toggleDarkMode" class="cursor-pointer">
|
||||
<SunIcon v-if="!room.isDarkMode" width="18" height="18" color="#000" />
|
||||
<MoonIcon v-else width="18" height="18" color="#e4e4e7" />
|
||||
</span>
|
||||
</template>
|
|
@ -0,0 +1,164 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { RouterLink } from "vue-router";
|
||||
import { roomStore } from "@/stores/room";
|
||||
import DarkModeSwitcher from "@/components/DarkModeSwitcher.vue";
|
||||
const room = roomStore();
|
||||
const mobileMenu = ref(false);
|
||||
</script>
|
||||
<template>
|
||||
<header class="bg-gray-50 h-16 dark:bg-zinc-900 dark:text-zinc-100">
|
||||
<nav
|
||||
class="flex mx-auto max-w-7xl items-center justify-between lg:px-8 p-4 lg:p-5 px-6"
|
||||
>
|
||||
<div class="flex lg:flex-1">
|
||||
<span class="-m-1.5 p-1.5 font-bold"> SyncTV </span>
|
||||
</div>
|
||||
|
||||
<div class="flex lg:hidden">
|
||||
<span class="p-2 mr-2">
|
||||
<DarkModeSwitcher />
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-700 dark:text-zinc-100"
|
||||
@click="mobileMenu = true"
|
||||
>
|
||||
<span class="sr-only">打开菜单</span>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:flex lg:gap-x-12">
|
||||
<RouterLink to="/"> 首 页 </RouterLink>
|
||||
<RouterLink to="/joinRoom"> 加入房间 </RouterLink>
|
||||
<RouterLink to="/createRoom"> 创建房间 </RouterLink>
|
||||
<RouterLink to="/cinema" v-if="room.login">
|
||||
影 厅
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="hidden lg:flex lg:flex-1 lg:justify-end">
|
||||
<DarkModeSwitcher />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 移动端菜单 -->
|
||||
<transition name="slide-to-bottom">
|
||||
<div
|
||||
class="fixed inset-y-0 right-0 z-[100] w-full overflow-y-auto bg-white px-6 py-5 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10 dark:bg-zinc-800"
|
||||
v-show="mobileMenu"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="-m-1.5 p-1.5 font-bold"> SyncTV </span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="-m-2.5 rounded-md p-2.5 text-gray-600"
|
||||
@click="mobileMenu = false"
|
||||
>
|
||||
<span class="sr-only">关闭菜单</span>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-6 flow-root">
|
||||
<div class="-my-6 divide-y divide-gray-500/10">
|
||||
<div class="space-y-2 py-6 moblie-menu">
|
||||
<RouterLink to="/" @click="mobileMenu = false">
|
||||
首 页
|
||||
</RouterLink>
|
||||
<RouterLink to="/joinRoom" @click="mobileMenu = false">
|
||||
加入房间
|
||||
</RouterLink>
|
||||
<RouterLink to="/createRoom" @click="mobileMenu = false">
|
||||
创建房间
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/cinema"
|
||||
@click="mobileMenu = false"
|
||||
v-if="room.login"
|
||||
>
|
||||
影 厅
|
||||
</RouterLink>
|
||||
</div>
|
||||
<!-- <div class="py-6">
|
||||
<a href="javascript::">关于此项目</a>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
header {
|
||||
transition: background 0.5s;
|
||||
nav {
|
||||
a {
|
||||
@apply text-sm font-normal leading-6 text-zinc-600 dark:text-ellipsis dark:text-zinc-200;
|
||||
|
||||
&:hover {
|
||||
@apply text-blue-600 dark:text-sky-500;
|
||||
}
|
||||
|
||||
&.router-link-exact-active {
|
||||
@apply font-medium text-blue-700 dark:text-sky-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moblie-menu {
|
||||
a {
|
||||
@apply -mx-3 block rounded-lg px-3 py-2 text-base font-normal leading-7 text-gray-900 dark:text-zinc-200;
|
||||
|
||||
&:hover,
|
||||
&.router-link-exact-active {
|
||||
@apply bg-gray-100 font-semibold dark:text-zinc-200 dark:bg-zinc-700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端菜单动画
|
||||
.slide-to-bottom-enter-from,
|
||||
.slide-to-bottom-leave-to {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.slide-to-bottom-enter-to,
|
||||
.slide-to-bottom-leave-from {
|
||||
transform: translateY(0%);
|
||||
}
|
||||
|
||||
.slide-to-bottom-enter-active,
|
||||
.slide-to-bottom-leave-active {
|
||||
transition: all 0.6s ease;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,251 @@
|
|||
<script setup lang="ts">
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import { roomStore } from "@/stores/room";
|
||||
import Artplayer from "artplayer";
|
||||
import type { Option } from "artplayer/types/option";
|
||||
import { onMounted, onBeforeUnmount, ref, watch } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import artplayerPluginDanmuku from "artplayer-plugin-danmuku";
|
||||
import mpegts from "mpegts.js";
|
||||
const room = roomStore();
|
||||
|
||||
const artplayer = ref<HTMLDivElement>();
|
||||
|
||||
let art: Artplayer;
|
||||
|
||||
interface options {
|
||||
url: string;
|
||||
}
|
||||
|
||||
const Props = defineProps({
|
||||
options: {
|
||||
type: Object as PropType<options>,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const Emits = defineEmits(["get-instance", "set-player-status", "ws-send"]);
|
||||
// 监听当前正在播放影片变化
|
||||
// watch(
|
||||
// () => room.currentMovie,
|
||||
// () => {
|
||||
// const jsonData = room.currentMovie;
|
||||
|
||||
// setTimeout(() => {
|
||||
// if (jsonData.url === "") {
|
||||
// art.switchUrl("https://live.lazy.ink/hd.mp4");
|
||||
// localStorage.getItem("dev") === "114514" && console.log("视频为空!");
|
||||
// } else {
|
||||
// localStorage.getItem("dev") === "114514" &&
|
||||
// console.log("变了!", jsonData.url);
|
||||
|
||||
// art.option.type = "";
|
||||
|
||||
// if (jsonData.live) {
|
||||
// art.option.type = "flv";
|
||||
|
||||
// jsonData.url = `${window.location.origin}/api/live/${jsonData.url}.flv`;
|
||||
// }
|
||||
|
||||
// art.switchUrl(jsonData.url).catch((err) => {
|
||||
// ElMessage.error("视频加载失败!");
|
||||
// console.error("视频加载失败:", err);
|
||||
// });
|
||||
// }
|
||||
// }, 233);
|
||||
// }
|
||||
// );
|
||||
|
||||
// 监听弹幕变化
|
||||
watch(
|
||||
() => room.danmuku,
|
||||
() => {
|
||||
art.plugins.artplayerPluginDanmuku.emit(room.danmuku);
|
||||
}
|
||||
);
|
||||
|
||||
const playFlv = (player: HTMLMediaElement, url: string, art: any) => {
|
||||
if (mpegts.isSupported()) {
|
||||
const flv = mpegts.createPlayer(
|
||||
{ type: "flv", url },
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer " + localStorage.token
|
||||
}
|
||||
}
|
||||
);
|
||||
flv.attachMediaElement(player);
|
||||
flv.load();
|
||||
art.flv = flv;
|
||||
art.on("destroy", () => flv.destroy());
|
||||
} else {
|
||||
art.notice.show = "Unsupported playback format: flv";
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const option: Option = {
|
||||
container: artplayer.value!,
|
||||
volume: 0, // 音量
|
||||
autoplay: false, // 自动播放
|
||||
autoSize: false, // 隐藏黑边
|
||||
autoMini: false,
|
||||
theme: "#00a1d6",
|
||||
loop: false, // 循环播放
|
||||
flip: true, // 显示视频翻转功能
|
||||
playbackRate: true, // 显示视频播放速度功能
|
||||
aspectRatio: true, // 显示视频长宽比功能
|
||||
screenshot: false, // 显示视频截图功能
|
||||
setting: true,
|
||||
hotkey: true, // 快捷键
|
||||
pip: true, // 显示画中画按钮
|
||||
mutex: true, // 互斥,阻止多个播放器同时播放
|
||||
fullscreen: true, // 全屏按钮
|
||||
fullscreenWeb: true, // 网页全屏
|
||||
subtitleOffset: false, // 显示字幕偏移功能
|
||||
miniProgressBar: true, // 迷你进度条,播放器失去焦点后且正在播放时出现
|
||||
playsInline: true, // 在移动端是否使用 playsInline 模式
|
||||
lock: true, // 移动端显示锁定按钮
|
||||
fastForward: false, // 移动端添加长按视频快进功能
|
||||
autoPlayback: false, // 使用自动回放功能
|
||||
autoOrientation: true, // 移动端的网页全屏时,根据视频尺寸和视口尺寸,旋转播放器
|
||||
airplay: false, // 隔空播放
|
||||
// isLive: room.currentMovie.live,
|
||||
plugins: [
|
||||
artplayerPluginDanmuku({
|
||||
// 弹幕数组
|
||||
danmuku: [],
|
||||
speed: 4
|
||||
})
|
||||
],
|
||||
// type: 'm3u8',
|
||||
...Props.options,
|
||||
// url: "",
|
||||
// type: "flv",
|
||||
customType: {
|
||||
flv: playFlv
|
||||
}
|
||||
};
|
||||
|
||||
art = new Artplayer(option);
|
||||
|
||||
const isPlaying = ref(false);
|
||||
|
||||
const onReady = () => {
|
||||
watch(
|
||||
() => room.currentMovieStatus.playing,
|
||||
() => {
|
||||
// 非直播流才同步
|
||||
if (!room.currentMovie.live) {
|
||||
if (room.currentMovieStatus.playing === isPlaying.value) return;
|
||||
room.currentMovieStatus.playing ? art.play() : art.pause();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => room.currentMovieStatus.seek,
|
||||
() => {
|
||||
localStorage.getItem("dev") === "114514" &&
|
||||
console.log("seek变了:", room.currentMovieStatus.seek);
|
||||
|
||||
if (!room.currentMovie.live)
|
||||
room.currentMovieStatus.seek === 0 ? false : (art.seek = room.currentMovieStatus.seek);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => room.currentMovieStatus.rate,
|
||||
() => {
|
||||
localStorage.getItem("dev") === "114514" &&
|
||||
console.log("rate变了:", room.currentMovieStatus.rate);
|
||||
|
||||
if (!room.currentMovie.live) {
|
||||
room.currentMovieStatus.rate === art.playbackRate
|
||||
? void 0
|
||||
: (art.playbackRate = room.currentMovieStatus.rate);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
if (!room.currentMovie.live) {
|
||||
art.seek = room.currentMovieStatus.seek;
|
||||
room.currentMovieStatus.playing ? art.play() : art.pause();
|
||||
console.log("seek同步成功:", room.currentMovieStatus.seek);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
localStorage.getItem("dev") === "114514" && console.log("art.seek:", art.currentTime);
|
||||
localStorage.getItem("dev") === "114514" &&
|
||||
console.log("room.seek:", room.currentMovieStatus.seek);
|
||||
|
||||
Emits("ws-send", "PLAYER:视频已就绪");
|
||||
// art.off('ready', onReady);
|
||||
|
||||
// 视频播放
|
||||
const vPlayAndPause = useDebounceFn((type: number) => {
|
||||
if (!room.currentMovie.live)
|
||||
Emits(
|
||||
"set-player-status",
|
||||
JSON.stringify({
|
||||
Type: type,
|
||||
Seek: art.currentTime,
|
||||
Rate: art.playbackRate
|
||||
})
|
||||
);
|
||||
}, 1000);
|
||||
|
||||
art.on("play", () => {
|
||||
vPlayAndPause(3);
|
||||
isPlaying.value = true;
|
||||
localStorage.getItem("dev") === "114514" && console.log("视频播放,seek:", art.currentTime);
|
||||
});
|
||||
|
||||
// 视频暂停
|
||||
art.on("pause", () => {
|
||||
vPlayAndPause(4);
|
||||
isPlaying.value = false;
|
||||
localStorage.getItem("dev") === "114514" &&
|
||||
console.log("视频暂停中,,seek:", art.currentTime);
|
||||
});
|
||||
|
||||
// 空降
|
||||
const debouncedFn = useDebounceFn((currentTime: number) => {
|
||||
if (!room.currentMovie.live)
|
||||
Emits(
|
||||
"set-player-status",
|
||||
JSON.stringify({
|
||||
Type: 9,
|
||||
Seek: currentTime,
|
||||
Rate: art.playbackRate
|
||||
})
|
||||
);
|
||||
localStorage.getItem("dev") === "114514" && console.log("视频空降,:", art.currentTime);
|
||||
}, 1000);
|
||||
|
||||
art.on("seek", (currentTime) => {
|
||||
debouncedFn(currentTime);
|
||||
});
|
||||
|
||||
// 倍速
|
||||
art.on("video:ratechange", () => {
|
||||
console.log(art.playbackRate);
|
||||
|
||||
isPlaying.value ? vPlayAndPause(3) : vPlayAndPause(4);
|
||||
});
|
||||
};
|
||||
art.on("ready", onReady);
|
||||
Emits("get-instance", art);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// art.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="artplayer-app" ref="artplayer"></div>
|
||||
</template>
|
||||
|
||||
<style></style>
|
|
@ -0,0 +1,203 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import axios, { type AxiosResponse } from "axios";
|
||||
import { ElNotification } from "element-plus";
|
||||
import { roomStore } from "@/stores/room";
|
||||
import router from "@/router";
|
||||
import { roomListApi, joinRoomApi } from "@/services/apis/room";
|
||||
import type { RoomList } from "@/types/Room";
|
||||
import JoinRoom from "@/views/JoinRoom.vue";
|
||||
|
||||
const room = roomStore();
|
||||
const __roomList = ref<RoomList[]>([
|
||||
// { roomid: "1", peoplenum: 0, needpassword: false },
|
||||
// { roomid: "OPPPPPPPPPPPPPPPPPPPPPPPPPPP", peoplenum: 10, needpassword: false },
|
||||
// { roomid: "1asdsadasdz", peoplenum: 114514, needpassword: true },
|
||||
// { roomid: "1sdfgdgscv", peoplenum: 114114514514, needpassword: false },
|
||||
// { roomid: "ewadfds1", peoplenum: 114514, needpassword: true },
|
||||
// { roomid: "asdfsaddf1", peoplenum: 114114514114514514, needpassword: false },
|
||||
// { roomid: "1fadfsf", peoplenum: 114514, needpassword: true },
|
||||
// { roomid: "sdxccvrsd1", peoplenum: 114114514514, needpassword: false },
|
||||
// { roomid: "fsddfsd1", peoplenum: 114114514114514514, needpassword: false },
|
||||
// { roomid: "sfdsff1", peoplenum: 114514, needpassword: false },
|
||||
// { roomid: "cdc1", peoplenum: 114514, needpassword: false },
|
||||
// { roomid: "aDS1", peoplenum: 0, needpassword: true },
|
||||
// { roomid: "df", peoplenum: 0, needpassword: true },
|
||||
// { roomid: "sdaXdasd1", peoplenum: 0, needpassword: false },
|
||||
// { roomid: "3WAQsdwq1", peoplenum: 0, needpassword: false },
|
||||
// { roomid: "asdsdAD1", peoplenum: 0, needpassword: false },
|
||||
// { roomid: "ad1", peoplenum: 0, needpassword: false },
|
||||
// { roomid: "aDxasds1", peoplenum: 0, needpassword: false },
|
||||
// { roomid: "adsds1", peoplenum: 0, needpassword: false },
|
||||
// { roomid: "SADDASDS1", peoplenum: 0, needpassword: false }
|
||||
]);
|
||||
|
||||
const JoinRoomDialog = ref(false);
|
||||
|
||||
const formData = ref({
|
||||
// uname: "",
|
||||
// RoomID: "",
|
||||
// password: "",
|
||||
// hasPwd: false
|
||||
roomId: "",
|
||||
password: "",
|
||||
username: localStorage.getItem("uname") || "",
|
||||
userPassword: localStorage.getItem("uPasswd") || ""
|
||||
});
|
||||
|
||||
const openJoinRoomDialog = (item: RoomList) => {
|
||||
// if (!hasPwd && hasUname) return joinRoom(localStorage.uname, RoomID, "");
|
||||
|
||||
formData.value.roomId = item.roomId;
|
||||
JoinRoomDialog.value = true;
|
||||
};
|
||||
|
||||
const { state: roomList, execute: reqRoomList } = roomListApi();
|
||||
const totalItems = ref(0);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const sort = ref("peopleNum");
|
||||
const order = ref("desc");
|
||||
|
||||
const getRoomList = async (showMsg = false) => {
|
||||
__roomList.value = [];
|
||||
try {
|
||||
await reqRoomList({
|
||||
params: {
|
||||
page: currentPage.value,
|
||||
max: pageSize.value,
|
||||
sort: sort.value,
|
||||
order: order.value
|
||||
}
|
||||
});
|
||||
if (roomList.value && roomList.value.list) {
|
||||
totalItems.value = roomList.value.total;
|
||||
for (let i = 0; i < roomList.value.list.length; i++) {
|
||||
__roomList.value.push(roomList.value.list[i]);
|
||||
}
|
||||
}
|
||||
|
||||
showMsg &&
|
||||
ElNotification({
|
||||
title: `更新列表成功`,
|
||||
type: "success"
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error(err.message);
|
||||
ElNotification({
|
||||
title: "错误",
|
||||
message: err.response.data.error || err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getRoomList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card mx-auto lg:w-9/12 max-sm:rounded-none">
|
||||
<div class="card-title flex flex-wrap justify-between">
|
||||
<div>房间列表({{ __roomList.length }})</div>
|
||||
<div class="text-base">
|
||||
排序方式:<el-select
|
||||
v-model="sort"
|
||||
class="m-2"
|
||||
placeholder="排序方式"
|
||||
@change="getRoomList(false)"
|
||||
>
|
||||
<el-option label="房间ID" value="roomId" />
|
||||
<el-option label="房间人数" value="peopleNum" />
|
||||
<el-option label="创建人首字母" value="creator" />
|
||||
<el-option label="创建时间" value="createAt" />
|
||||
<el-option label="是否有密码" value="needPassword" />
|
||||
</el-select>
|
||||
<button
|
||||
class="btn btn-dense"
|
||||
@click="
|
||||
order === 'desc' ? (order = 'asc') : (order = 'desc');
|
||||
getRoomList();
|
||||
"
|
||||
>
|
||||
{{ order === "asc" ? "👆" : "👇" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body flex flex-wrap justify-center">
|
||||
<el-empty v-if="__roomList.length === 0" description="无房间,去创建一个吧~" />
|
||||
<div
|
||||
v-else
|
||||
v-for="item in __roomList"
|
||||
:key="item.roomId"
|
||||
class="flex flex-wrap m-2 rounded-lg bg-stone-50 hover:bg-white transition-all dark:bg-zinc-800 hover:dark:bg-neutral-800 max-w-[220px]"
|
||||
>
|
||||
<div class="overflow-hidden text-ellipsis m-auto p-2 w-full">
|
||||
<b class="block text-base font-semibold truncate"> {{ item["roomId"] }}</b>
|
||||
</div>
|
||||
<div class="text-sm p-2">
|
||||
<div class="inline mr-2">
|
||||
在线人数:<span :class="item.peopleNum > 0 ? 'text-green-500' : 'text-red-500'">{{
|
||||
item["peopleNum"]
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="inline">创建者:{{ item.creator }}</div>
|
||||
<hr />
|
||||
<div>创建时间:{{ new Date(item.createAt).toLocaleString() }}</div>
|
||||
</div>
|
||||
<div class="flex p-2 w-full justify-between items-center">
|
||||
<el-tag disabled :type="item.needPassword ? 'danger' : 'success'">
|
||||
{{ item.needPassword ? "有密码" : "无密码" }}
|
||||
</el-tag>
|
||||
<button class="btn btn-dense" @click="openJoinRoomDialog(item)">
|
||||
加入房间
|
||||
<PlayIcon class="inline-block" width="18px" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer justify-between flex-wrap overflow-hidden">
|
||||
<button class="btn btn-success max-sm:mb-4" @click="getRoomList(true)">更新列表</button>
|
||||
<el-pagination
|
||||
v-if="__roomList.length > 0"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:pager-count="4"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="totalItems"
|
||||
@size-change="getRoomList(false)"
|
||||
@current-change="getRoomList(false)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="JoinRoomDialog"
|
||||
:title="'加入房间 ' + formData.roomId"
|
||||
class="rounded-lg dark:bg-zinc-800 w-[443px] max-sm:w-[90%]"
|
||||
>
|
||||
<!-- <el-form
|
||||
label-position="top"
|
||||
ref="form"
|
||||
@submit.prevent="joinRoom(formData.uname, formData.RoomID, formData.password)"
|
||||
>
|
||||
<el-form-item label="设置一个用户名:" v-if="!hasUname">
|
||||
<input type="text" class="l-input m-0 p-0 pl-2 w-full" v-model="formData.uname" />
|
||||
</el-form-item>
|
||||
<el-form-item label="房间密码:" v-if="formData.hasPwd">
|
||||
<input type="text" class="l-input m-0 p-0 pl-2 w-full" v-model="formData.password" />
|
||||
</el-form-item>
|
||||
</el-form> -->
|
||||
<JoinRoom :item="formData" />
|
||||
<!-- <template #footer>
|
||||
<button
|
||||
class="btn btn-success"
|
||||
@click="joinRoom(formData.uname, formData.RoomID, formData.password)"
|
||||
>
|
||||
加入
|
||||
</button>
|
||||
</template> -->
|
||||
</el-dialog>
|
||||
</template>
|
|
@ -0,0 +1,30 @@
|
|||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
width?: string;
|
||||
height?: string;
|
||||
color?: string;
|
||||
}>(),
|
||||
{
|
||||
width: "15",
|
||||
height: "15",
|
||||
color: "#ffff",
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<svg
|
||||
:width="width"
|
||||
:height="height"
|
||||
:fill="color"
|
||||
viewBox="0 0 15 15"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.1464 1.14645C12.3417 0.951184 12.6583 0.951184 12.8535 1.14645L14.8535 3.14645C15.0488 3.34171 15.0488 3.65829 14.8535 3.85355L10.9109 7.79618C10.8349 7.87218 10.7471 7.93543 10.651 7.9835L6.72359 9.94721C6.53109 10.0435 6.29861 10.0057 6.14643 9.85355C5.99425 9.70137 5.95652 9.46889 6.05277 9.27639L8.01648 5.34897C8.06455 5.25283 8.1278 5.16507 8.2038 5.08907L12.1464 1.14645ZM12.5 2.20711L8.91091 5.79618L7.87266 7.87267L8.12731 8.12732L10.2038 7.08907L13.7929 3.5L12.5 2.20711ZM9.99998 2L8.99998 3H4.9C4.47171 3 4.18056 3.00039 3.95552 3.01877C3.73631 3.03668 3.62421 3.06915 3.54601 3.10899C3.35785 3.20487 3.20487 3.35785 3.10899 3.54601C3.06915 3.62421 3.03669 3.73631 3.01878 3.95552C3.00039 4.18056 3 4.47171 3 4.9V11.1C3 11.5283 3.00039 11.8194 3.01878 12.0445C3.03669 12.2637 3.06915 12.3758 3.10899 12.454C3.20487 12.6422 3.35785 12.7951 3.54601 12.891C3.62421 12.9309 3.73631 12.9633 3.95552 12.9812C4.18056 12.9996 4.47171 13 4.9 13H11.1C11.5283 13 11.8194 12.9996 12.0445 12.9812C12.2637 12.9633 12.3758 12.9309 12.454 12.891C12.6422 12.7951 12.7951 12.6422 12.891 12.454C12.9309 12.3758 12.9633 12.2637 12.9812 12.0445C12.9996 11.8194 13 11.5283 13 11.1V6.99998L14 5.99998V11.1V11.1207C14 11.5231 14 11.8553 13.9779 12.1259C13.9549 12.407 13.9057 12.6653 13.782 12.908C13.5903 13.2843 13.2843 13.5903 12.908 13.782C12.6653 13.9057 12.407 13.9549 12.1259 13.9779C11.8553 14 11.5231 14 11.1207 14H11.1H4.9H4.87934C4.47686 14 4.14468 14 3.87409 13.9779C3.59304 13.9549 3.33469 13.9057 3.09202 13.782C2.7157 13.5903 2.40973 13.2843 2.21799 12.908C2.09434 12.6653 2.04506 12.407 2.0221 12.1259C1.99999 11.8553 1.99999 11.5231 2 11.1207V11.1206V11.1V4.9V4.87935V4.87932V4.87931C1.99999 4.47685 1.99999 4.14468 2.0221 3.87409C2.04506 3.59304 2.09434 3.33469 2.21799 3.09202C2.40973 2.71569 2.7157 2.40973 3.09202 2.21799C3.33469 2.09434 3.59304 2.04506 3.87409 2.0221C4.14468 1.99999 4.47685 1.99999 4.87932 2H4.87935H4.9H9.99998Z"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,30 @@
|
|||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
width?: string;
|
||||
height?: string;
|
||||
color?: string;
|
||||
}>(),
|
||||
{
|
||||
width: "15",
|
||||
height: "15",
|
||||
color: "#ffff",
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<svg
|
||||
:width="width"
|
||||
:height="height"
|
||||
:fill="color"
|
||||
viewBox="0 0 15 15"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.89998 0.499976C2.89998 0.279062 2.72089 0.0999756 2.49998 0.0999756C2.27906 0.0999756 2.09998 0.279062 2.09998 0.499976V1.09998H1.49998C1.27906 1.09998 1.09998 1.27906 1.09998 1.49998C1.09998 1.72089 1.27906 1.89998 1.49998 1.89998H2.09998V2.49998C2.09998 2.72089 2.27906 2.89998 2.49998 2.89998C2.72089 2.89998 2.89998 2.72089 2.89998 2.49998V1.89998H3.49998C3.72089 1.89998 3.89998 1.72089 3.89998 1.49998C3.89998 1.27906 3.72089 1.09998 3.49998 1.09998H2.89998V0.499976ZM5.89998 3.49998C5.89998 3.27906 5.72089 3.09998 5.49998 3.09998C5.27906 3.09998 5.09998 3.27906 5.09998 3.49998V4.09998H4.49998C4.27906 4.09998 4.09998 4.27906 4.09998 4.49998C4.09998 4.72089 4.27906 4.89998 4.49998 4.89998H5.09998V5.49998C5.09998 5.72089 5.27906 5.89998 5.49998 5.89998C5.72089 5.89998 5.89998 5.72089 5.89998 5.49998V4.89998H6.49998C6.72089 4.89998 6.89998 4.72089 6.89998 4.49998C6.89998 4.27906 6.72089 4.09998 6.49998 4.09998H5.89998V3.49998ZM1.89998 6.49998C1.89998 6.27906 1.72089 6.09998 1.49998 6.09998C1.27906 6.09998 1.09998 6.27906 1.09998 6.49998V7.09998H0.499976C0.279062 7.09998 0.0999756 7.27906 0.0999756 7.49998C0.0999756 7.72089 0.279062 7.89998 0.499976 7.89998H1.09998V8.49998C1.09998 8.72089 1.27906 8.89997 1.49998 8.89997C1.72089 8.89997 1.89998 8.72089 1.89998 8.49998V7.89998H2.49998C2.72089 7.89998 2.89998 7.72089 2.89998 7.49998C2.89998 7.27906 2.72089 7.09998 2.49998 7.09998H1.89998V6.49998ZM8.54406 0.98184L8.24618 0.941586C8.03275 0.917676 7.90692 1.1655 8.02936 1.34194C8.17013 1.54479 8.29981 1.75592 8.41754 1.97445C8.91878 2.90485 9.20322 3.96932 9.20322 5.10022C9.20322 8.37201 6.82247 11.0878 3.69887 11.6097C3.45736 11.65 3.20988 11.6772 2.96008 11.6906C2.74563 11.702 2.62729 11.9535 2.77721 12.1072C2.84551 12.1773 2.91535 12.2458 2.98667 12.3128L3.05883 12.3795L3.31883 12.6045L3.50684 12.7532L3.62796 12.8433L3.81491 12.9742L3.99079 13.089C4.11175 13.1651 4.23536 13.2375 4.36157 13.3059L4.62496 13.4412L4.88553 13.5607L5.18837 13.6828L5.43169 13.7686C5.56564 13.8128 5.70149 13.8529 5.83857 13.8885C5.94262 13.9155 6.04767 13.9401 6.15405 13.9622C6.27993 13.9883 6.40713 14.0109 6.53544 14.0298L6.85241 14.0685L7.11934 14.0892C7.24637 14.0965 7.37436 14.1002 7.50322 14.1002C11.1483 14.1002 14.1032 11.1453 14.1032 7.50023C14.1032 7.25044 14.0893 7.00389 14.0623 6.76131L14.0255 6.48407C13.991 6.26083 13.9453 6.04129 13.8891 5.82642C13.8213 5.56709 13.7382 5.31398 13.6409 5.06881L13.5279 4.80132L13.4507 4.63542L13.3766 4.48666C13.2178 4.17773 13.0353 3.88295 12.8312 3.60423L12.6782 3.40352L12.4793 3.16432L12.3157 2.98361L12.1961 2.85951L12.0355 2.70246L11.8134 2.50184L11.4925 2.24191L11.2483 2.06498L10.9562 1.87446L10.6346 1.68894L10.3073 1.52378L10.1938 1.47176L9.95488 1.3706L9.67791 1.2669L9.42566 1.1846L9.10075 1.09489L8.83599 1.03486L8.54406 0.98184ZM10.4032 5.30023C10.4032 4.27588 10.2002 3.29829 9.83244 2.40604C11.7623 3.28995 13.1032 5.23862 13.1032 7.50023C13.1032 10.593 10.596 13.1002 7.50322 13.1002C6.63646 13.1002 5.81597 12.9036 5.08355 12.5522C6.5419 12.0941 7.81081 11.2082 8.74322 10.0416C8.87963 10.2284 9.10028 10.3497 9.34928 10.3497C9.76349 10.3497 10.0993 10.0139 10.0993 9.59971C10.0993 9.24256 9.84965 8.94373 9.51535 8.86816C9.57741 8.75165 9.63653 8.63334 9.6926 8.51332C9.88358 8.63163 10.1088 8.69993 10.35 8.69993C11.0403 8.69993 11.6 8.14028 11.6 7.44993C11.6 6.75976 11.0406 6.20024 10.3505 6.19993C10.3853 5.90487 10.4032 5.60464 10.4032 5.30023Z"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,30 @@
|
|||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
width?: string;
|
||||
height?: string;
|
||||
color?: string;
|
||||
}>(),
|
||||
{
|
||||
width: "15",
|
||||
height: "15",
|
||||
color: "#ffff",
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<svg
|
||||
:width="width"
|
||||
:height="height"
|
||||
:fill="color"
|
||||
viewBox="0 0 15 15"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.24182 2.32181C3.3919 2.23132 3.5784 2.22601 3.73338 2.30781L12.7334 7.05781C12.8974 7.14436 13 7.31457 13 7.5C13 7.68543 12.8974 7.85564 12.7334 7.94219L3.73338 12.6922C3.5784 12.774 3.3919 12.7687 3.24182 12.6782C3.09175 12.5877 3 12.4252 3 12.25V2.75C3 2.57476 3.09175 2.4123 3.24182 2.32181ZM4 3.57925V11.4207L11.4288 7.5L4 3.57925Z"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
width?: string;
|
||||
height?: string;
|
||||
color?: string;
|
||||
}>(),
|
||||
{
|
||||
width: "15",
|
||||
height: "15",
|
||||
color: "#ffff",
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<svg
|
||||
:width="width"
|
||||
:height="height"
|
||||
:fill="color"
|
||||
viewBox="0 0 15 15"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.5 0C7.77614 0 8 0.223858 8 0.5V2.5C8 2.77614 7.77614 3 7.5 3C7.22386 3 7 2.77614 7 2.5V0.5C7 0.223858 7.22386 0 7.5 0ZM2.1967 2.1967C2.39196 2.00144 2.70854 2.00144 2.90381 2.1967L4.31802 3.61091C4.51328 3.80617 4.51328 4.12276 4.31802 4.31802C4.12276 4.51328 3.80617 4.51328 3.61091 4.31802L2.1967 2.90381C2.00144 2.70854 2.00144 2.39196 2.1967 2.1967ZM0.5 7C0.223858 7 0 7.22386 0 7.5C0 7.77614 0.223858 8 0.5 8H2.5C2.77614 8 3 7.77614 3 7.5C3 7.22386 2.77614 7 2.5 7H0.5ZM2.1967 12.8033C2.00144 12.608 2.00144 12.2915 2.1967 12.0962L3.61091 10.682C3.80617 10.4867 4.12276 10.4867 4.31802 10.682C4.51328 10.8772 4.51328 11.1938 4.31802 11.3891L2.90381 12.8033C2.70854 12.9986 2.39196 12.9986 2.1967 12.8033ZM12.5 7C12.2239 7 12 7.22386 12 7.5C12 7.77614 12.2239 8 12.5 8H14.5C14.7761 8 15 7.77614 15 7.5C15 7.22386 14.7761 7 14.5 7H12.5ZM10.682 4.31802C10.4867 4.12276 10.4867 3.80617 10.682 3.61091L12.0962 2.1967C12.2915 2.00144 12.608 2.00144 12.8033 2.1967C12.9986 2.39196 12.9986 2.70854 12.8033 2.90381L11.3891 4.31802C11.1938 4.51328 10.8772 4.51328 10.682 4.31802ZM8 12.5C8 12.2239 7.77614 12 7.5 12C7.22386 12 7 12.2239 7 12.5V14.5C7 14.7761 7.22386 15 7.5 15C7.77614 15 8 14.7761 8 14.5V12.5ZM10.682 10.682C10.8772 10.4867 11.1938 10.4867 11.3891 10.682L12.8033 12.0962C12.9986 12.2915 12.9986 12.608 12.8033 12.8033C12.608 12.9986 12.2915 12.9986 12.0962 12.8033L10.682 11.3891C10.4867 11.1938 10.4867 10.8772 10.682 10.682ZM5.5 7.5C5.5 6.39543 6.39543 5.5 7.5 5.5C8.60457 5.5 9.5 6.39543 9.5 7.5C9.5 8.60457 8.60457 9.5 7.5 9.5C6.39543 9.5 5.5 8.60457 5.5 7.5ZM7.5 4.5C5.84315 4.5 4.5 5.84315 4.5 7.5C4.5 9.15685 5.84315 10.5 7.5 10.5C9.15685 10.5 10.5 9.15685 10.5 7.5C10.5 5.84315 9.15685 4.5 7.5 4.5Z"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,30 @@
|
|||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
width?: string;
|
||||
height?: string;
|
||||
color?: string;
|
||||
}>(),
|
||||
{
|
||||
width: "15",
|
||||
height: "15",
|
||||
color: "#ffff",
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<svg
|
||||
:width="width"
|
||||
:height="height"
|
||||
:fill="color"
|
||||
viewBox="0 0 15 15"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5.5 1C5.22386 1 5 1.22386 5 1.5C5 1.77614 5.22386 2 5.5 2H9.5C9.77614 2 10 1.77614 10 1.5C10 1.22386 9.77614 1 9.5 1H5.5ZM3 3.5C3 3.22386 3.22386 3 3.5 3H5H10H11.5C11.7761 3 12 3.22386 12 3.5C12 3.77614 11.7761 4 11.5 4H11V12C11 12.5523 10.5523 13 10 13H5C4.44772 13 4 12.5523 4 12V4L3.5 4C3.22386 4 3 3.77614 3 3.5ZM5 4H10V12H5V4Z"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
|
@ -0,0 +1,28 @@
|
|||
import './assets/reset.css'
|
||||
import './assets/global.less'
|
||||
import 'element-plus/dist/index.css'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
import SunIcon from './components/icons/Sun.vue'
|
||||
import MoonIcon from './components/icons/Moon.vue'
|
||||
import PlayIcon from './components/icons/Play.vue'
|
||||
import EditIcon from './components/icons/Edit.vue'
|
||||
import TrashIcon from './components/icons/Trash.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
app
|
||||
.component('SunIcon', SunIcon)
|
||||
.component('MoonIcon', MoonIcon)
|
||||
.component('PlayIcon', PlayIcon)
|
||||
.component('EditIcon', EditIcon)
|
||||
.component('TrashIcon', TrashIcon)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
|
@ -0,0 +1,51 @@
|
|||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import { start, close } from "@/utils/nprogress";
|
||||
|
||||
const Base_Title = "SyncTV";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.VITE_BASEURL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: () => import("../views/HomeView.vue"),
|
||||
meta: { title: "首页" }
|
||||
},
|
||||
{
|
||||
path: "/createRoom",
|
||||
name: "createRoom",
|
||||
component: () => import("../views/CreateRoom.vue"),
|
||||
meta: { title: "创建房间" }
|
||||
},
|
||||
{
|
||||
path: "/joinRoom",
|
||||
name: "joinRoom",
|
||||
component: () => import("../views/JoinRoom.vue"),
|
||||
meta: { title: "加入房间" }
|
||||
},
|
||||
{
|
||||
path: "/cinema",
|
||||
name: "cinema",
|
||||
component: () => import("../views/Cinema.vue"),
|
||||
meta: { title: "影厅" }
|
||||
}
|
||||
],
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
return {
|
||||
top: 0
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
router.beforeEach((to: any, from: any, next) => {
|
||||
start();
|
||||
window.document.title = Base_Title + " - " + to.meta.title;
|
||||
next();
|
||||
});
|
||||
|
||||
router.afterEach(() => {
|
||||
close();
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,140 @@
|
|||
import { useDefineApi } from "@/stores/useDefineApi";
|
||||
import type { BaseMovieInfo, MovieInfo, EditMovieInfo, MovieStatus } from "@/types/Movie";
|
||||
|
||||
// 获取影片列表
|
||||
export const movieListApi = useDefineApi<
|
||||
{
|
||||
headers: { Authorization: string };
|
||||
},
|
||||
{
|
||||
current: {
|
||||
movie: MovieInfo;
|
||||
status: MovieStatus;
|
||||
};
|
||||
movies: MovieInfo[] | [];
|
||||
total: number;
|
||||
}
|
||||
>({
|
||||
url: "/api/movie/list",
|
||||
method: "GET"
|
||||
});
|
||||
|
||||
// 添加影片
|
||||
export const pushMovieApi = useDefineApi<
|
||||
// request
|
||||
{
|
||||
params: {
|
||||
pos: string;
|
||||
};
|
||||
data: BaseMovieInfo;
|
||||
headers: { Authorization: string };
|
||||
},
|
||||
// response
|
||||
{
|
||||
id: number;
|
||||
}
|
||||
>({
|
||||
url: "/api/movie/push",
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
// 编辑影片信息
|
||||
export const editMovieInfoApi = useDefineApi<
|
||||
{
|
||||
data: EditMovieInfo;
|
||||
headers: { Authorization: string };
|
||||
},
|
||||
{}
|
||||
>({
|
||||
url: "/api/movie/edit",
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
// 删除影片
|
||||
export const delMovieApi = useDefineApi<
|
||||
{
|
||||
data: {
|
||||
ids: Array<number>;
|
||||
};
|
||||
headers: { Authorization: string };
|
||||
},
|
||||
{}
|
||||
>({
|
||||
url: "/api/movie/delete",
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
// 交换影片位置
|
||||
export const swapMovieApi = useDefineApi<
|
||||
{
|
||||
data: {
|
||||
id1: number;
|
||||
id2: number;
|
||||
};
|
||||
headers: { Authorization: string };
|
||||
},
|
||||
{}
|
||||
>({
|
||||
url: "/api/movie/swap",
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
// 当前影片状态
|
||||
export const movieStatusApi = useDefineApi<
|
||||
{
|
||||
headers: { Authorization: string };
|
||||
},
|
||||
{
|
||||
current: {
|
||||
movie: MovieInfo;
|
||||
status: MovieStatus;
|
||||
};
|
||||
}
|
||||
>({
|
||||
url: "/api/movie/current",
|
||||
method: "GET"
|
||||
});
|
||||
|
||||
// 更改正在播放的影片
|
||||
export const changeCurrentMovieApi = useDefineApi<
|
||||
{
|
||||
headers: { Authorization: string };
|
||||
data: {
|
||||
id: number;
|
||||
};
|
||||
},
|
||||
{}
|
||||
>({
|
||||
url: "/api/movie/current",
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
// 清空影片列表
|
||||
export const clearMovieListApi = useDefineApi<
|
||||
{
|
||||
headers: { Authorization: string };
|
||||
},
|
||||
{}
|
||||
>({
|
||||
url: "/api/movie/clear",
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
// 获取直播信息
|
||||
export const liveInfoApi = useDefineApi<
|
||||
{
|
||||
data: {
|
||||
id: number;
|
||||
};
|
||||
headers: { Authorization: string };
|
||||
},
|
||||
{
|
||||
host: string;
|
||||
port: number;
|
||||
app: string;
|
||||
token: string;
|
||||
}
|
||||
>({
|
||||
url: "/api/movie/live/publishKey",
|
||||
method: "POST"
|
||||
});
|
|
@ -0,0 +1,98 @@
|
|||
import { useDefineApi } from "@/stores/useDefineApi";
|
||||
import type { RoomList } from "@/types/Room";
|
||||
import type { MovieInfo } from "@/types/Movie";
|
||||
|
||||
// 房间列表
|
||||
export const roomListApi = useDefineApi<
|
||||
// request
|
||||
{
|
||||
params: {
|
||||
page: number;
|
||||
max: number;
|
||||
sort: string;
|
||||
order: string;
|
||||
};
|
||||
},
|
||||
// response 服务器返回的 data: {}里面的内容
|
||||
{
|
||||
list: RoomList[] | null;
|
||||
total: number;
|
||||
}
|
||||
>({
|
||||
url: "/api/room/list",
|
||||
method: "GET"
|
||||
});
|
||||
|
||||
// 创建房间
|
||||
export const createRoomApi = useDefineApi<
|
||||
// request
|
||||
{
|
||||
data: {
|
||||
roomId: string;
|
||||
password: string;
|
||||
username: string;
|
||||
userPassword: string;
|
||||
hidden: boolean;
|
||||
};
|
||||
},
|
||||
// response 服务器返回的 data: {}里面的内容
|
||||
{
|
||||
token: string;
|
||||
}
|
||||
>({
|
||||
url: "/api/room/create",
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
// 加入房间
|
||||
export const joinRoomApi = useDefineApi<
|
||||
// request
|
||||
{
|
||||
data: {
|
||||
roomId: string;
|
||||
password: string;
|
||||
username: string;
|
||||
userPassword: string;
|
||||
};
|
||||
params: {
|
||||
autoNew: boolean;
|
||||
};
|
||||
},
|
||||
// response
|
||||
{
|
||||
token: string;
|
||||
}
|
||||
>({
|
||||
url: "/api/room/login",
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
// 删除房间
|
||||
export const delRoomApi = useDefineApi<
|
||||
{
|
||||
data: {
|
||||
roomId: string;
|
||||
};
|
||||
headers: { Authorization: string };
|
||||
},
|
||||
{}
|
||||
>({
|
||||
url: "/api/room/delete",
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
// 更新房间密码
|
||||
export const updateRoomPasswordApi = useDefineApi<
|
||||
{
|
||||
data: {
|
||||
password: string;
|
||||
};
|
||||
headers: { Authorization: string };
|
||||
},
|
||||
{
|
||||
token: string;
|
||||
}
|
||||
>({
|
||||
url: "/api/room/pwd",
|
||||
method: "POST"
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
import { ref, computed } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import type { MovieInfo, MovieStatus } from "@/types/Movie";
|
||||
export const roomStore = defineStore("roomStore", () => {
|
||||
const login = ref(false);
|
||||
|
||||
const isDarkMode = ref(false);
|
||||
|
||||
// 影片列表
|
||||
const movieList = ref({});
|
||||
|
||||
// 设置播放当前影片
|
||||
const currentMovie = ref<MovieInfo>({
|
||||
name: "",
|
||||
live: false,
|
||||
proxy: false,
|
||||
url: "",
|
||||
rtmpSource: false,
|
||||
type: "",
|
||||
headers: null,
|
||||
createAt: Date.now(),
|
||||
creator: "SYSTEM",
|
||||
id: 1,
|
||||
lastEditAt: Date.now()
|
||||
});
|
||||
|
||||
// 当前影片播放状态
|
||||
const currentMovieStatus = ref<MovieStatus>({
|
||||
playing: false,
|
||||
rate: 1,
|
||||
seek: 0
|
||||
});
|
||||
|
||||
// 是否主动上报
|
||||
const play = ref(true);
|
||||
|
||||
const count = ref(3);
|
||||
|
||||
// 在线人数
|
||||
const peopleNum = ref(1);
|
||||
|
||||
const doubleCount = computed(() => count.value * 2);
|
||||
function increment() {
|
||||
count.value++;
|
||||
}
|
||||
|
||||
const danmuku = ref({});
|
||||
|
||||
return {
|
||||
count,
|
||||
isDarkMode,
|
||||
login,
|
||||
movieList,
|
||||
currentMovie,
|
||||
currentMovieStatus,
|
||||
play,
|
||||
doubleCount,
|
||||
increment,
|
||||
danmuku,
|
||||
peopleNum
|
||||
};
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import { type AxiosRequestConfig } from "axios";
|
||||
import { request } from "@/utils/requests";
|
||||
|
||||
export const useDefineApi = <P, T>(config2: AxiosRequestConfig) => {
|
||||
return () => {
|
||||
const { isLoading, state, isReady, execute } = request<T>(config2);
|
||||
return {
|
||||
isLoading,
|
||||
state,
|
||||
isReady,
|
||||
execute: async (config?: P & AxiosRequestConfig) => {
|
||||
await execute(0, config ? config : {});
|
||||
return state;
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
export interface MovieInfo extends BaseMovieInfo {
|
||||
createAt: number;
|
||||
creator: string;
|
||||
id: number;
|
||||
lastEditAt: number;
|
||||
pullKey?: string;
|
||||
}
|
||||
|
||||
export interface BaseMovieInfo {
|
||||
url: string;
|
||||
name: string;
|
||||
live: boolean;
|
||||
proxy: boolean;
|
||||
rtmpSource: boolean;
|
||||
type: string;
|
||||
headers: null;
|
||||
}
|
||||
|
||||
export interface EditMovieInfo {
|
||||
id: number;
|
||||
url: string;
|
||||
name: string;
|
||||
type: string;
|
||||
headers: null;
|
||||
}
|
||||
|
||||
export interface MovieStatus {
|
||||
playing: boolean;
|
||||
rate: number;
|
||||
seek: number;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import type { MovieInfo, MovieStatus } from "./Movie";
|
||||
|
||||
export interface RoomList {
|
||||
roomId: string;
|
||||
peopleNum: number;
|
||||
needPassword: boolean;
|
||||
creator: string;
|
||||
createAt: number;
|
||||
}
|
||||
|
||||
export interface RoomInfo {}
|
||||
|
||||
export interface WsMessage {
|
||||
type: number;
|
||||
|
||||
sender: string;
|
||||
message: string;
|
||||
|
||||
rate: number;
|
||||
seek: number;
|
||||
|
||||
movies: MovieInfo[];
|
||||
|
||||
current: {
|
||||
movie: MovieInfo;
|
||||
status: MovieStatus;
|
||||
};
|
||||
|
||||
peopleNum: number;
|
||||
}
|
||||
|
||||
export enum WsMessageType {
|
||||
MESSAGE = 2,
|
||||
PLAY = 3,
|
||||
PAUSE = 4,
|
||||
SEEK = 9,
|
||||
CURRENT_MOVIE = 10,
|
||||
PLAY_LIST_UPDATE = 11,
|
||||
PEOPLE_NUM = 12
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* @author Lazy
|
||||
* @description 弹窗组件
|
||||
* 能用,但只能用一点点。纯摆设
|
||||
*/
|
||||
|
||||
class Notify {
|
||||
private title!: string;
|
||||
private content!: string;
|
||||
private status!: string;
|
||||
|
||||
constructor() {
|
||||
let notifyBox = document.createElement("div");
|
||||
notifyBox.id = "notifyBox";
|
||||
document.body.appendChild(notifyBox);
|
||||
}
|
||||
|
||||
init(title: string, content: string, status: string) {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.status = status;
|
||||
this.show();
|
||||
}
|
||||
|
||||
async show() {
|
||||
let div = document.createElement("div");
|
||||
let nid = Date.now();
|
||||
div.className = "notifyyy";
|
||||
div.setAttribute("nid", String(nid));
|
||||
div.innerHTML = `
|
||||
<h2>${this.title}</h2>
|
||||
<p>${this.content}</p>
|
||||
`;
|
||||
document.querySelector("#notifyBox")?.appendChild(div);
|
||||
setTimeout(() => {
|
||||
document.querySelector(`div[nid="${nid}"]`)?.remove();
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
export default Notify;
|
|
@ -0,0 +1,18 @@
|
|||
import NProgress from 'nprogress'
|
||||
import 'nprogress/nprogress.css'
|
||||
|
||||
NProgress.configure({
|
||||
easing: 'ease',
|
||||
speed: 500,
|
||||
showSpinner: false,
|
||||
trickleSpeed: 200,
|
||||
minimum: 0.2
|
||||
})
|
||||
|
||||
export const start = () => {
|
||||
NProgress.start()
|
||||
}
|
||||
|
||||
export const close = () => {
|
||||
NProgress.done()
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import axios, { type AxiosRequestConfig } from "axios";
|
||||
import { useAsyncState } from "@vueuse/core";
|
||||
|
||||
const _req = async <T>(config: AxiosRequestConfig): Promise<T | undefined> => {
|
||||
const result = await axios(config);
|
||||
let realData = result.data;
|
||||
if (realData.data) realData = realData.data;
|
||||
return realData;
|
||||
};
|
||||
|
||||
export const request = <T>(config: AxiosRequestConfig) => {
|
||||
return useAsyncState<T | undefined, AxiosRequestConfig[]>(
|
||||
async (config2) => {
|
||||
config = Object.assign({}, config, config2);
|
||||
const result = await _req<T>(config);
|
||||
return result;
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
immediate: false,
|
||||
shallow: false as any,
|
||||
throwError: true,
|
||||
resetOnExecute: false
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,985 @@
|
|||
<script setup lang="ts">
|
||||
import axios, { type AxiosResponse } from "axios";
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { useWebSocket, useWindowSize } from "@vueuse/core";
|
||||
import Player from "@/components/Player.vue";
|
||||
import ArtPlayer from "artplayer";
|
||||
import { roomStore } from "@/stores/room";
|
||||
import { ElNotification, ElMessage } from "element-plus";
|
||||
import router from "@/router";
|
||||
import { updateRoomPasswordApi, delRoomApi } from "@/services/apis/room";
|
||||
import {
|
||||
movieListApi,
|
||||
pushMovieApi,
|
||||
editMovieInfoApi,
|
||||
delMovieApi,
|
||||
swapMovieApi,
|
||||
movieStatusApi,
|
||||
changeCurrentMovieApi,
|
||||
clearMovieListApi,
|
||||
liveInfoApi
|
||||
} from "@/services/apis/movie";
|
||||
import type { BaseMovieInfo, MovieInfo, EditMovieInfo, MovieStatus } from "@/types/Movie";
|
||||
import type { WsMessage } from "@/types/Room";
|
||||
import { WsMessageType } from "@/types/Room";
|
||||
|
||||
const { width: WindowWidth } = useWindowSize();
|
||||
const room = roomStore();
|
||||
|
||||
// 检查是否登录
|
||||
(() => !room.login && router.push("/"))();
|
||||
|
||||
// 获取房间信息
|
||||
const roomID = localStorage.roomId;
|
||||
let password = localStorage.password;
|
||||
|
||||
// 启动websocket连接
|
||||
const { status, data, send, close } = useWebSocket(`ws://${window.location.host}/api/room/ws`, {
|
||||
protocols: [localStorage.token],
|
||||
autoReconnect: {
|
||||
retries: 3,
|
||||
delay: 1000,
|
||||
onFailed() {
|
||||
ElMessage.error("Websocket 自动重连失败!");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 更新房间密码
|
||||
const { state: newToken, execute: reqUpdateRoomPasswordApi } = updateRoomPasswordApi();
|
||||
const changePassword = async () => {
|
||||
try {
|
||||
await reqUpdateRoomPasswordApi({
|
||||
data: {
|
||||
password: password
|
||||
},
|
||||
headers: { Authorization: localStorage.token }
|
||||
});
|
||||
|
||||
ElNotification({
|
||||
title: "更新成功",
|
||||
type: "success"
|
||||
});
|
||||
if (newToken.value) {
|
||||
localStorage.setItem("token", newToken.value.token);
|
||||
localStorage.setItem("password", password);
|
||||
setInterval(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
ElNotification({
|
||||
title: "更新失败",
|
||||
message: err.response.data.error || err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 显示房间密码
|
||||
let isShowPassword = ref(false);
|
||||
|
||||
// 删除房间
|
||||
const { execute: reqDelRoomApi } = delRoomApi();
|
||||
const deleteRoom = async () => {
|
||||
try {
|
||||
await reqDelRoomApi({
|
||||
data: {
|
||||
roomId: localStorage.roomId
|
||||
},
|
||||
headers: { Authorization: localStorage.token }
|
||||
});
|
||||
|
||||
ElNotification({
|
||||
title: "删除成功",
|
||||
type: "success"
|
||||
});
|
||||
setInterval(() => {
|
||||
localStorage.removeItem("RoomID");
|
||||
localStorage.removeItem("password");
|
||||
localStorage.removeItem("login");
|
||||
localStorage.removeItem("token");
|
||||
window.location.href = window.location.origin;
|
||||
}, 500);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
ElNotification({
|
||||
title: "删除失败",
|
||||
message: err.response.data.error || err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 获取影片列表
|
||||
let movieList = ref<MovieInfo[]>([]);
|
||||
const { state: _movieList, isLoading: movieListLoading, execute: reqMovieListApi } = movieListApi();
|
||||
/**
|
||||
* @argument updateStatus 是否更新当前正在播放的影片(包括状态)
|
||||
*/
|
||||
const getMovieList = async (updateStatus: boolean) => {
|
||||
try {
|
||||
await reqMovieListApi({
|
||||
headers: { Authorization: localStorage.token }
|
||||
});
|
||||
|
||||
if (_movieList.value) {
|
||||
localStorage.getItem("dev") === "114514" && console.log(_movieList.value);
|
||||
room.movieList = movieList.value = _movieList.value.movies;
|
||||
if (updateStatus) {
|
||||
room.currentMovie = _movieList.value.current.movie;
|
||||
room.currentMovieStatus = _movieList.value.current.status;
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
localStorage.getItem("dev") === "114514" && console.log(err);
|
||||
if (err.response.status === 401) {
|
||||
ElNotification({
|
||||
title: "身份验证失败,请重新进入房间",
|
||||
message: err.message,
|
||||
type: "error"
|
||||
});
|
||||
setInterval(() => {
|
||||
localStorage.removeItem("roomId");
|
||||
localStorage.removeItem("password");
|
||||
localStorage.removeItem("login");
|
||||
localStorage.removeItem("token");
|
||||
window.location.href = window.location.origin;
|
||||
}, 500);
|
||||
}
|
||||
ElNotification({
|
||||
title: "获取影片列表失败",
|
||||
message: err.response.data.error || err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 清空影片列表
|
||||
const { execute: reqClearMovieListApi } = clearMovieListApi();
|
||||
const clearMovieList = async (id: number) => {
|
||||
try {
|
||||
await reqClearMovieListApi({
|
||||
headers: { Authorization: localStorage.token }
|
||||
});
|
||||
ElNotification({
|
||||
title: "已清空",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
ElNotification({
|
||||
title: "清除成功",
|
||||
message: err.response.data.error || err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 新影片信息
|
||||
let newMovieInfo = ref<BaseMovieInfo>({
|
||||
name: "",
|
||||
live: false,
|
||||
proxy: false,
|
||||
url: "",
|
||||
rtmpSource: false,
|
||||
type: "",
|
||||
headers: null
|
||||
});
|
||||
|
||||
// 直播相关
|
||||
const liveInfoDialog = ref(false);
|
||||
const liveInfoForm = ref({
|
||||
host: "",
|
||||
port: 0,
|
||||
app: "",
|
||||
token: ""
|
||||
});
|
||||
const { state: liveInfo, execute: reqLiveInfoApi } = liveInfoApi();
|
||||
const getLiveInfo = async (id: number) => {
|
||||
try {
|
||||
await reqLiveInfoApi({
|
||||
data: {
|
||||
id: id
|
||||
},
|
||||
headers: { Authorization: localStorage.token }
|
||||
});
|
||||
|
||||
liveInfoDialog.value = true;
|
||||
if (liveInfo.value) liveInfoForm.value = liveInfo.value;
|
||||
console.log(liveInfo.value);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
ElNotification({
|
||||
title: "获取失败",
|
||||
message: err.response.data.error || err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => newMovieInfo.value.live,
|
||||
() => {
|
||||
!newMovieInfo.value.live ? (newMovieInfo.value.rtmpSource = false) : void 0;
|
||||
}
|
||||
);
|
||||
|
||||
// 把视频链接添加到列表
|
||||
const { execute: reqPushMovieApi } = pushMovieApi();
|
||||
const pushMovie = async (dir: string) => {
|
||||
if (newMovieInfo.value.live) {
|
||||
if (newMovieInfo.value.name === "")
|
||||
return ElNotification({
|
||||
title: "添加失败",
|
||||
message: "请填写表单完整",
|
||||
type: "error"
|
||||
});
|
||||
} else {
|
||||
if (newMovieInfo.value.url === "" || newMovieInfo.value.name === "")
|
||||
return ElNotification({
|
||||
title: "添加失败",
|
||||
message: "请填写表单完整",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await reqPushMovieApi({
|
||||
params: {
|
||||
pos: dir
|
||||
},
|
||||
data: newMovieInfo.value,
|
||||
headers: { Authorization: localStorage.token }
|
||||
});
|
||||
|
||||
ElNotification({
|
||||
title: "添加成功",
|
||||
type: "success"
|
||||
});
|
||||
newMovieInfo.value.name = newMovieInfo.value.url = "";
|
||||
getMovieList(false);
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
ElNotification({
|
||||
title: "添加失败",
|
||||
message: err.response.data.error || err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前影片状态
|
||||
const { state: movieStatus, execute: reqMovieStatusApi } = movieStatusApi();
|
||||
const getCurrentMovieStatus = async () => {
|
||||
try {
|
||||
await reqMovieStatusApi({
|
||||
headers: { Authorization: localStorage.token }
|
||||
});
|
||||
if (movieStatus.value) {
|
||||
setCurrentMovieStatus(movieStatus.value.current.status);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(err.message);
|
||||
ElNotification({
|
||||
title: "获取失败",
|
||||
type: "error",
|
||||
message: err.response.data.error || err.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 设置影片状态
|
||||
const setCurrentMovieStatus = (movieStatus: MovieStatus) => {
|
||||
room.currentMovieStatus.playing === movieStatus.playing
|
||||
? void 0
|
||||
: (room.currentMovieStatus.playing = movieStatus.playing);
|
||||
room.currentMovieStatus.rate === movieStatus.rate
|
||||
? void 0
|
||||
: (room.currentMovieStatus.rate = movieStatus.rate);
|
||||
if (
|
||||
room.currentMovieStatus.seek - movieStatus.seek > 1 ||
|
||||
room.currentMovieStatus.seek - movieStatus.seek > -2
|
||||
)
|
||||
room.currentMovieStatus.seek = movieStatus.seek;
|
||||
};
|
||||
|
||||
// 当前影片信息
|
||||
let cMovieInfo = ref<EditMovieInfo>({
|
||||
id: 0,
|
||||
url: "",
|
||||
name: "",
|
||||
type: "",
|
||||
headers: null
|
||||
});
|
||||
|
||||
// 打开编辑对话框
|
||||
const editDialog = ref(false);
|
||||
const openEditDialog = (item: MovieInfo) => {
|
||||
cMovieInfo.value.id = item.id;
|
||||
cMovieInfo.value.url = item.url;
|
||||
cMovieInfo.value.name = item.name;
|
||||
cMovieInfo.value.type = item.type;
|
||||
cMovieInfo.value.headers = item.headers;
|
||||
editDialog.value = true;
|
||||
};
|
||||
|
||||
// 编辑影片信息
|
||||
const { isLoading: editMovieInfoLoading, execute: reqEditMovieInfoApi } = editMovieInfoApi();
|
||||
const editMovieInfo = async () => {
|
||||
try {
|
||||
await reqEditMovieInfoApi({
|
||||
data: cMovieInfo.value,
|
||||
headers: { Authorization: localStorage.token }
|
||||
});
|
||||
ElNotification({
|
||||
title: "更新成功",
|
||||
type: "success"
|
||||
});
|
||||
editDialog.value = false;
|
||||
} catch (err: any) {
|
||||
console.error(err.message);
|
||||
ElNotification({
|
||||
title: "更新失败",
|
||||
type: "error",
|
||||
message: err.response.data.error || err.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 删除影片
|
||||
const { execute: reqDelMovieApi } = delMovieApi();
|
||||
const deleteMovie = async (ids: Array<number>) => {
|
||||
try {
|
||||
await reqDelMovieApi({
|
||||
data: {
|
||||
ids: ids
|
||||
},
|
||||
headers: { Authorization: localStorage.token }
|
||||
});
|
||||
for (const id of ids) {
|
||||
movieList.value.splice(
|
||||
movieList.value.findIndex((movie: MovieInfo) => movie["id"] === id),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
ElNotification({
|
||||
title: "删除成功",
|
||||
type: "success"
|
||||
});
|
||||
selectMovies.value = [];
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
ElNotification({
|
||||
title: "删除失败",
|
||||
message: err.response.data.error || err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 交换两个影片的位置
|
||||
const selectMovies = ref<number[]>([]);
|
||||
const { execute: reqSwapMovieApi } = swapMovieApi();
|
||||
const swapMovie = async () => {
|
||||
try {
|
||||
await reqSwapMovieApi({
|
||||
data: {
|
||||
id1: selectMovies.value[0],
|
||||
id2: selectMovies.value[1]
|
||||
},
|
||||
headers: { Authorization: localStorage.token }
|
||||
});
|
||||
|
||||
ElNotification({
|
||||
title: "交换成功",
|
||||
type: "success"
|
||||
});
|
||||
selectMovies.value = [];
|
||||
getMovieList(false);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
ElNotification({
|
||||
title: "交换失败",
|
||||
message: err.response.data.error || err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 设置当前正在播放的影片
|
||||
const playerLoaded = ref(false);
|
||||
const playerOptions = ref({
|
||||
url: "",
|
||||
isLive: false
|
||||
});
|
||||
const { execute: reqChangeCurrentMovieApi } = changeCurrentMovieApi();
|
||||
const changeCurrentMovie = async (id: number) => {
|
||||
// playerLoaded.value = false;
|
||||
try {
|
||||
await reqChangeCurrentMovieApi({
|
||||
data: {
|
||||
id: id
|
||||
},
|
||||
headers: { Authorization: localStorage.token }
|
||||
});
|
||||
|
||||
ElNotification({
|
||||
title: "设置成功",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
// playerLoaded.value = true;
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
ElNotification({
|
||||
title: "设置失败",
|
||||
message: err.response.data.error || err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let noPlayArea: HTMLElement | null;
|
||||
let playArea: HTMLElement | null;
|
||||
|
||||
// 消息列表
|
||||
let chatArea: HTMLElement | null;
|
||||
let msgList = ref<string[]>([]);
|
||||
|
||||
// 更新消息列表
|
||||
const updateMsgList = (msg: string) => {
|
||||
msgList.value.push(msg);
|
||||
};
|
||||
|
||||
// 监听ws信息变化
|
||||
watch(
|
||||
() => data.value,
|
||||
() => {
|
||||
if (data.value === "")
|
||||
return localStorage.getItem("dev") === "114514" && console.log("返回了空", data.value);
|
||||
|
||||
const jsonData: WsMessage = JSON.parse(data.value);
|
||||
localStorage.getItem("dev") === "114514" && console.log(`-----Ws Message Start-----`);
|
||||
localStorage.getItem("dev") === "114514" && console.log(jsonData);
|
||||
localStorage.getItem("dev") === "114514" && console.log(`-----Ws Message End-----`);
|
||||
switch (jsonData.type) {
|
||||
// 聊天消息
|
||||
case WsMessageType.MESSAGE: {
|
||||
msgList.value.push(`${jsonData.sender}:${jsonData.message}`);
|
||||
// jsonData.message.split(":")[0] !== "PLAYER" &&
|
||||
room.danmuku = {
|
||||
text: jsonData.message, // 弹幕文本
|
||||
//time: Date.now(), // 发送时间,单位秒
|
||||
color: "#fff", // 弹幕局部颜色
|
||||
border: false // 是否显示描边
|
||||
//mode: 0, // 弹幕模式: 0表示滚动, 1静止
|
||||
};
|
||||
|
||||
// 自动滚动到最底部
|
||||
if (chatArea) chatArea.scrollTop = chatArea.scrollHeight;
|
||||
|
||||
if (msgList.value.length > 40)
|
||||
return (msgList.value = [
|
||||
"<p><b>SYSTEM:</b>已达到最大聊天记录长度,系统已自动清空...</p>"
|
||||
]);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// 播放
|
||||
case WsMessageType.PLAY: {
|
||||
setCurrentMovieStatus({
|
||||
playing: true,
|
||||
seek: jsonData.seek,
|
||||
rate: jsonData.rate
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// 暂停
|
||||
case WsMessageType.PAUSE: {
|
||||
setCurrentMovieStatus({
|
||||
playing: false,
|
||||
seek: jsonData.seek,
|
||||
rate: jsonData.rate
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// 视频进度发生变化
|
||||
case WsMessageType.SEEK: {
|
||||
// room.currentMovie = jsonData.
|
||||
if (
|
||||
room.currentMovieStatus.seek - jsonData.seek > 1 ||
|
||||
room.currentMovieStatus.seek - jsonData.seek < -2
|
||||
)
|
||||
room.currentMovieStatus.seek = jsonData.seek;
|
||||
break;
|
||||
}
|
||||
|
||||
// 设置正在播放的影片
|
||||
case WsMessageType.CURRENT_MOVIE: {
|
||||
room.currentMovie = jsonData.current.movie;
|
||||
break;
|
||||
}
|
||||
|
||||
// 播放列表更新
|
||||
case WsMessageType.PLAY_LIST_UPDATE: {
|
||||
getMovieList(false);
|
||||
// jsonData.movies
|
||||
// ? (movieList.value = room.movieList = jsonData.movies)
|
||||
// : movieList.value.splice(0, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
// ん?
|
||||
case WsMessageType.PEOPLE_NUM: {
|
||||
room.peopleNum < jsonData.peopleNum
|
||||
? msgList.value.push(
|
||||
`<p><b>SYSTEM:</b>欢迎新成员加入,当前共有 ${jsonData.peopleNum} 人在观看</p>`
|
||||
)
|
||||
: room.peopleNum > jsonData.peopleNum
|
||||
? msgList.value.push(
|
||||
`<p><b>SYSTEM:</b>有人离开了房间,当前还剩 ${jsonData.peopleNum} 人在观看</p>`
|
||||
)
|
||||
: "";
|
||||
room.peopleNum = jsonData.peopleNum;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 发送消息(临时
|
||||
let sendText_ = ref("");
|
||||
const sendText = () => {
|
||||
if (sendText_.value === "")
|
||||
return ElMessage({
|
||||
message: "发送的消息不能为空",
|
||||
type: "warning"
|
||||
});
|
||||
const msg = JSON.stringify({
|
||||
Type: 2,
|
||||
Message: sendText_.value,
|
||||
Time: Date.now()
|
||||
});
|
||||
send(msg);
|
||||
sendText_.value = "";
|
||||
chatArea!.scrollTop = chatArea!.scrollHeight;
|
||||
localStorage.getItem("dev") === "114514" && console.log("sended:" + msg);
|
||||
};
|
||||
|
||||
let player: ArtPlayer;
|
||||
|
||||
function getInstance(art: ArtPlayer) {
|
||||
player = art;
|
||||
}
|
||||
|
||||
// 设置聊天框高度
|
||||
const resetChatAreaHeight = () => {
|
||||
chatArea = document.querySelector(".chatArea");
|
||||
noPlayArea = document.querySelector(".noPlayArea");
|
||||
playArea = document.querySelector(".playArea");
|
||||
const h = playArea ? playArea : noPlayArea;
|
||||
chatArea && h && (chatArea.style.height = h.scrollHeight - 63 + "px");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
resetChatAreaHeight();
|
||||
}, 233);
|
||||
|
||||
watch(WindowWidth, () => {
|
||||
resetChatAreaHeight();
|
||||
});
|
||||
|
||||
getMovieList(true);
|
||||
|
||||
// 监听当前正在播放影片变化
|
||||
watch(
|
||||
() => room.currentMovie,
|
||||
() => {
|
||||
const jsonData = room.currentMovie;
|
||||
playerLoaded.value = false;
|
||||
if (jsonData.pullKey !== "") {
|
||||
jsonData.url = `${window.location.origin}/api/movie/live/${jsonData.pullKey}.flv`;
|
||||
playerOptions.value = {
|
||||
url: jsonData.url,
|
||||
isLive: jsonData.live
|
||||
};
|
||||
setInterval(() => (playerLoaded.value = true), 20);
|
||||
} else if (jsonData.url === "") {
|
||||
// player.switchUrl("https://live.lazy.ink/hd.mp4");
|
||||
playerLoaded.value = false;
|
||||
} else {
|
||||
localStorage.getItem("dev") === "114514" && console.log("变了!", jsonData.url);
|
||||
playerOptions.value = {
|
||||
url: jsonData.url,
|
||||
isLive: jsonData.live
|
||||
};
|
||||
|
||||
setInterval(() => (playerLoaded.value = true), 20);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (player) player.destroy();
|
||||
close();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :md="18" class="mb-6 max-sm:my-2">
|
||||
<div class="card max-sm:rounded-none">
|
||||
<div
|
||||
class="card-title flex flex-wrap justify-between max-sm:text-sm"
|
||||
v-if="room.currentMovie.url !== ''"
|
||||
>
|
||||
{{ room.currentMovie.name }}
|
||||
<small>👁🗨 {{ room.peopleNum }} </small>
|
||||
</div>
|
||||
<div class="card-title flex flex-wrap justify-between max-sm:text-sm" v-else>
|
||||
当前没有影片播放,快去添加几部吧~<small class="font-normal"
|
||||
>👁🗨 {{ room.peopleNum }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="card-body playArea max-sm:p-0" v-if="playerLoaded">
|
||||
<div class="art-player">
|
||||
<!--
|
||||
https://www.llxz.cc/style/images/zhuye.mp4
|
||||
-->
|
||||
<Player
|
||||
@set-player-status="send"
|
||||
@ws-send="updateMsgList"
|
||||
@get-instance="getInstance"
|
||||
:options="playerOptions"
|
||||
></Player>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body noPlayArea max-sm:pb-3 max-sm:px-3" v-else>
|
||||
<!-- <img class="mx-auto" src="../assets/something-lost.png" /> -->
|
||||
<el-carousel height="37vmax" indicator-position="none" arrow="never" interval="5000">
|
||||
<el-carousel-item v-for="item in 4" :key="item">
|
||||
<img class="mx-auto" :src="'https://api.imlazy.ink/img?t=' + item" />
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
<div class="card-footer p-4 max-sm:hidden"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :md="6" class="mb-6 max-sm:mb-2">
|
||||
<div class="card h-full max-sm:rounded-none">
|
||||
<div class="card-title">在线聊天</div>
|
||||
<div class="card-body mb-2">
|
||||
<div class="chatArea">
|
||||
<div class="message" v-for="item in msgList" :key="item">
|
||||
<div v-html="item"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer" style="justify-content: center; padding: 0.5rem">
|
||||
<input
|
||||
type="text"
|
||||
@keyup.enter="sendText()"
|
||||
v-model="sendText_"
|
||||
placeholder="按 Enter 键即可发送..."
|
||||
class="l-input w-full bg-transparent"
|
||||
/>
|
||||
<button class="btn w-24 m-2.5 ml-0" @click="sendText()">发送</button>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<!-- 房间信息 -->
|
||||
<el-col :lg="6" :md="8" :sm="9" :xs="24" class="mb-6 max-sm:mb-2">
|
||||
<div class="card max-sm:rounded-none">
|
||||
<div class="card-title">房间信息</div>
|
||||
|
||||
<div class="card-body">
|
||||
<table class="table-auto i-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width="100">连接状态</td>
|
||||
<td>{{ status }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>房间ID</td>
|
||||
<td>{{ roomID }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>房间密码</td>
|
||||
<td>
|
||||
<input
|
||||
:type="isShowPassword ? 'text' : 'password'"
|
||||
v-model="password"
|
||||
class="w-full m-0 pl-1 inline-block bg-neutral-200 border border-neutral-200 rounded-md focus:outline-none hover:bg-neutral-100 transition-all text-sm dark:bg-neutral-700 dark:border-neutral-800"
|
||||
/>
|
||||
<button
|
||||
class="inline-block absolute -translate-x-5 opacity-50 pr-0.5"
|
||||
@click="isShowPassword = !isShowPassword"
|
||||
>
|
||||
{{ isShowPassword ? "●" : "◯" }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>在线人数</td>
|
||||
<td>{{ room.peopleNum }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card-footer flex-wrap justify-between">
|
||||
<el-popconfirm
|
||||
width="220"
|
||||
confirm-button-text="是"
|
||||
cancel-button-text="否"
|
||||
title="你确定要删除这个房间吗?!"
|
||||
@confirm="deleteRoom"
|
||||
>
|
||||
<template #reference>
|
||||
<button class="btn btn-error">删除房间</button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
|
||||
<el-popconfirm
|
||||
width="220"
|
||||
confirm-button-text="是"
|
||||
cancel-button-text="否"
|
||||
title="更新后,所有人将会被踢下线!"
|
||||
@confirm="changePassword"
|
||||
>
|
||||
<template #reference>
|
||||
<button class="btn btn-success">更新房间密码</button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<!-- 影片列表 -->
|
||||
<el-col :lg="12" :md="16" :sm="15" :xs="24" class="mb-6 max-sm:mb-2">
|
||||
<div class="card max-sm:rounded-none">
|
||||
<div class="card-title">影片列表({{ movieList.length }})</div>
|
||||
|
||||
<div class="card-body">
|
||||
<el-skeleton v-if="movieListLoading" :rows="1" animated />
|
||||
<div
|
||||
v-else
|
||||
v-for="item in movieList"
|
||||
:key="item.name"
|
||||
class="flex justify-around mb-2 rounded-lg bg-zinc-50 hover:bg-white transition-all dark:bg-zinc-800 hover:dark:bg-neutral-800"
|
||||
>
|
||||
<div class="m-auto pl-2">
|
||||
<input v-model="selectMovies" type="checkbox" :value="item['id']" />
|
||||
</div>
|
||||
<div class="overflow-hidden text-ellipsis m-auto p-2 w-7/12">
|
||||
<b class="block text-base font-semibold" :title="`ID: ${item.id}`">
|
||||
<el-tag class="mr-1" size="small" v-if="item.live"> 直播流 </el-tag>
|
||||
{{ item["name"] }}
|
||||
<button
|
||||
v-if="item.live"
|
||||
class="ml-1 font-normal text-sm border bg-rose-50 dark:bg-transparent border-rose-500 rounded-lg px-2 text-rose-500 hover:brightness-75 transition-all"
|
||||
@click="getLiveInfo(item['id'])"
|
||||
>
|
||||
查看推流信息
|
||||
</button>
|
||||
</b>
|
||||
<small class="truncate">{{ item["url"] || item["pullKey"] }}</small>
|
||||
</div>
|
||||
|
||||
<div class="m-auto p-2">
|
||||
<button class="btn btn-dense m-0 mr-1" @click="changeCurrentMovie(item['id'])">
|
||||
播放
|
||||
<PlayIcon class="inline-block" width="18px" />
|
||||
</button>
|
||||
<button class="btn btn-dense btn-warning m-0 mr-1" @click="openEditDialog(item)">
|
||||
编辑
|
||||
<EditIcon class="inline-block" width="16px" height="16px" />
|
||||
</button>
|
||||
<el-popconfirm
|
||||
width="220"
|
||||
confirm-button-text="是"
|
||||
cancel-button-text="否"
|
||||
title="你确定要删除这条影片吗?"
|
||||
@confirm="deleteMovie([item['id']])"
|
||||
>
|
||||
<template #reference>
|
||||
<button class="btn btn-dense btn-error m-0 mr-1">
|
||||
删除
|
||||
<TrashIcon class="inline-block" width="16px" height="16px" />
|
||||
</button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer justify-between">
|
||||
<div>
|
||||
<button class="btn mr-2" v-if="selectMovies.length === 2" @click="swapMovie">
|
||||
交换位置
|
||||
</button>
|
||||
|
||||
<el-popconfirm
|
||||
v-if="selectMovies.length >= 2"
|
||||
width="220"
|
||||
confirm-button-text="是"
|
||||
cancel-button-text="否"
|
||||
title="你确定要删除这些影片吗?"
|
||||
@confirm="deleteMovie(selectMovies)"
|
||||
>
|
||||
<template #reference>
|
||||
<button class="btn btn-error">批量删除</button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
|
||||
<div></div>
|
||||
<div>
|
||||
<el-popconfirm
|
||||
width="220"
|
||||
confirm-button-text="是"
|
||||
cancel-button-text="否"
|
||||
title="你确定要清空影片列表吗?!"
|
||||
@confirm="clearMovieList"
|
||||
>
|
||||
<template #reference>
|
||||
<button class="btn btn-error mr-2">清空列表</button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
<button class="btn btn-success" @click="getMovieList(true)">更新列表</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<!-- 添加影片 -->
|
||||
<el-col :lg="6" :md="14" :xs="24" class="mb-6 max-sm:mb-2">
|
||||
<div class="card max-sm:rounded-none">
|
||||
<div class="card-title">添加影片</div>
|
||||
<div class="card-body flex justify-around flex-wrap">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="影片Url"
|
||||
class="l-input-violet mb-1.5 w-full"
|
||||
v-if="!(newMovieInfo.live && newMovieInfo.rtmpSource)"
|
||||
v-model="newMovieInfo.url"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="影片名称"
|
||||
class="l-input-slate mt-1.5 w-full"
|
||||
v-model="newMovieInfo.name"
|
||||
/>
|
||||
|
||||
<div class="mt-4 mb-0 flex flex-wrap justify-around w-full">
|
||||
<div>
|
||||
<input type="checkbox" v-model="newMovieInfo.live" />
|
||||
<label> 这是一条直播流</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="newMovieInfo.rtmpSource"
|
||||
@click="newMovieInfo.live ? true : (newMovieInfo.live = true)"
|
||||
/>
|
||||
<label> 我想创建直播</label>
|
||||
</div>
|
||||
|
||||
<!-- <div>
|
||||
<input type="checkbox" v-model="newMovieInfo.proxy" />
|
||||
<label> isProxy</label>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer flex-wrap pt-3" style="justify-content: space-around">
|
||||
<button class="btn" @click="pushMovie('front')">添加到列表最<b>前</b>面</button>
|
||||
<button class="btn" @click="pushMovie('back')">添加到列表最<b>后</b>面</button>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 编辑影片对话框 -->
|
||||
<el-dialog
|
||||
v-model="editDialog"
|
||||
title="编辑影片"
|
||||
width="443px"
|
||||
class="rounded-lg dark:bg-zinc-800"
|
||||
>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="Url:">
|
||||
<input type="text" class="l-input m-0 p-0 pl-2 w-full" v-model="cMovieInfo.url" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Name:">
|
||||
<input type="text" class="l-input m-0 p-0 pl-2 w-full" v-model="cMovieInfo.name" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<button class="btn mr-2" @click="editDialog = false">取消</button>
|
||||
<button class="btn btn-success contrast-50" disabled v-if="editMovieInfoLoading">
|
||||
请求中...
|
||||
</button>
|
||||
<button class="btn btn-success" @click="editMovieInfo()" v-else>确定修改</button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 直播推流信息 -->
|
||||
<el-dialog
|
||||
v-model="liveInfoDialog"
|
||||
title="直播推流信息"
|
||||
width="443px"
|
||||
class="rounded-lg dark:bg-zinc-800"
|
||||
>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="推流地址:">
|
||||
<input
|
||||
type="text"
|
||||
class="l-input m-0 p-0 pl-2 w-full"
|
||||
:value="`rtmp://${liveInfoForm.host}/${liveInfoForm.app}/`"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="推流密钥:">
|
||||
<input type="text" class="l-input m-0 p-0 pl-2 w-full" :value="liveInfoForm.token" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<button class="btn btn-success" @click="liveInfoDialog = false">我已知晓</button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.art-player {
|
||||
// margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.chatArea {
|
||||
overflow-y: scroll;
|
||||
// height: 33vmax;
|
||||
}
|
||||
|
||||
.i-table {
|
||||
td {
|
||||
padding: 2px 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.a::after {
|
||||
content: "";
|
||||
width: 100%;
|
||||
background-image: url("https://api.imlazy.ink/img/?");
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,126 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { ElNotification } from "element-plus";
|
||||
import { roomStore } from "@/stores/room";
|
||||
import router from "@/router/index";
|
||||
import { createRoomApi } from "@/services/apis/room";
|
||||
const room = roomStore();
|
||||
|
||||
const { state: createRoomToken, execute: reqCreateRoomApi } = createRoomApi();
|
||||
|
||||
const formData = ref({
|
||||
roomId: "",
|
||||
password: "",
|
||||
username: localStorage.getItem("uname") || "",
|
||||
userPassword: "",
|
||||
hidden: false
|
||||
});
|
||||
const savePwd = ref(false);
|
||||
|
||||
const operateRoom = async () => {
|
||||
if (formData.value?.username === "" || formData.value?.roomId === "") {
|
||||
ElNotification({
|
||||
title: "错误",
|
||||
message: "请填写表单完整",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await reqCreateRoomApi({
|
||||
data: formData.value
|
||||
});
|
||||
if (!createRoomToken.value)
|
||||
return ElNotification({
|
||||
title: "错误",
|
||||
message: "服务器并未返回token",
|
||||
type: "error"
|
||||
});
|
||||
localStorage.setItem("token", createRoomToken.value?.token);
|
||||
ElNotification({
|
||||
title: "创建成功",
|
||||
type: "success"
|
||||
});
|
||||
room.login = true;
|
||||
|
||||
localStorage.setItem("uname", formData.value.username);
|
||||
savePwd.value && localStorage.setItem("uPasswd", formData.value.userPassword);
|
||||
localStorage.setItem("roomId", formData.value.roomId);
|
||||
savePwd.value && localStorage.setItem("password", formData.value.password);
|
||||
localStorage.setItem("login", "true");
|
||||
|
||||
router.replace("/cinema");
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
ElNotification({
|
||||
title: "错误",
|
||||
message: err.response.data.error || err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="room">
|
||||
<form @submit.prevent="" class="sm:w-96 w-full">
|
||||
<input
|
||||
class="l-input"
|
||||
type="text"
|
||||
v-model="formData.username"
|
||||
placeholder="用户名"
|
||||
required
|
||||
/>
|
||||
<br />
|
||||
<input
|
||||
class="l-input"
|
||||
type="text"
|
||||
v-model="formData.userPassword"
|
||||
placeholder="密码"
|
||||
required
|
||||
/>
|
||||
<br />
|
||||
<input class="l-input" type="text" v-model="formData.roomId" placeholder="房间名" required />
|
||||
<br />
|
||||
<input class="l-input" type="password" v-model="formData.password" placeholder="房间密码" />
|
||||
<br />
|
||||
<div>
|
||||
<input class="w-auto" type="checkbox" v-model="formData.hidden" />
|
||||
<label class="mr-6" title="不显示在房间列表"> 是否隐藏此房间</label>
|
||||
<input class="w-auto" type="checkbox" v-model="savePwd" />
|
||||
<label title="明文保存到本机哦~"> 记住密码</label>
|
||||
</div>
|
||||
<button class="btn m-[10px]" @click="operateRoom()">创建房间</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.room {
|
||||
text-align: center;
|
||||
margin-top: 5vmax;
|
||||
|
||||
form {
|
||||
// width: 443px;
|
||||
margin: auto;
|
||||
|
||||
input {
|
||||
width: 70%;
|
||||
|
||||
&:hover {
|
||||
padding: 10px 15px;
|
||||
width: 74%;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 15px;
|
||||
width: 70%;
|
||||
|
||||
&:hover {
|
||||
padding: 12px 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
import { roomStore } from "@/stores/room";
|
||||
import RoomList from "@/components/RoomList.vue";
|
||||
const room = roomStore();
|
||||
const devMode = localStorage.getItem("dev") === "114514" ? true : false;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<h1 class="text-3xl">首页</h1>
|
||||
<br />
|
||||
<RoomList />
|
||||
<button class="btn" @click="room.increment" v-if="devMode">
|
||||
{{ room.count }}
|
||||
</button>
|
||||
<br />
|
||||
<p class="text-zinc-400">*开发中页面,可能是最终品质</p>
|
||||
<p>© Copyright 2023 Lazy all right reserved</p>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,172 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { ElNotification } from "element-plus";
|
||||
import { roomStore } from "@/stores/room";
|
||||
import router from "@/router/index";
|
||||
import { useRoute } from "vue-router";
|
||||
import { joinRoomApi } from "@/services/apis/room";
|
||||
const route = useRoute();
|
||||
const room = roomStore();
|
||||
|
||||
// 是否为弹窗加载
|
||||
const isModal = computed(() => {
|
||||
return route.name === "joinRoom" ? false : true;
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
item?: {
|
||||
roomId: string;
|
||||
password: string;
|
||||
username: string;
|
||||
userPassword: string;
|
||||
};
|
||||
}>();
|
||||
|
||||
const formData = ref({
|
||||
roomId: localStorage.getItem("roomId") || "",
|
||||
password: localStorage.getItem("password") || "",
|
||||
username: localStorage.getItem("uname") || "",
|
||||
userPassword: localStorage.getItem("uPasswd") || ""
|
||||
});
|
||||
|
||||
if (props.item) formData.value = props.item;
|
||||
|
||||
const savePwd = ref(localStorage.getItem("uPasswd") ? true : false);
|
||||
|
||||
const { state: joinRoomToken, execute: reqJoinRoomApi } = joinRoomApi();
|
||||
|
||||
const JoinRoom = async () => {
|
||||
if (formData.value?.username === "" || formData.value?.roomId === "") {
|
||||
ElNotification({
|
||||
title: "错误",
|
||||
message: "请填写表单完整",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await reqJoinRoomApi({
|
||||
data: formData.value,
|
||||
params: {
|
||||
autoNew: true
|
||||
}
|
||||
});
|
||||
if (!joinRoomToken.value)
|
||||
return ElNotification({
|
||||
title: "错误",
|
||||
message: "服务器并未返回token",
|
||||
type: "error"
|
||||
});
|
||||
localStorage.setItem("token", joinRoomToken.value?.token);
|
||||
ElNotification({
|
||||
title: "加入成功",
|
||||
type: "success"
|
||||
});
|
||||
room.login = true;
|
||||
|
||||
localStorage.setItem("uname", formData.value.username);
|
||||
savePwd.value && localStorage.setItem("uPasswd", formData.value.userPassword);
|
||||
localStorage.setItem("roomId", formData.value.roomId);
|
||||
savePwd.value && localStorage.setItem("password", formData.value.password);
|
||||
localStorage.setItem("login", "true");
|
||||
|
||||
router.replace("/cinema");
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
ElNotification({
|
||||
title: "错误",
|
||||
message: err.response.data.error || err.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="isModal ? 'room-dialog' : 'room'">
|
||||
<form @submit.prevent="" :class="!isModal && 'sm:w-96 ' + 'w-full'">
|
||||
<input
|
||||
class="l-input"
|
||||
type="text"
|
||||
v-model="formData.username"
|
||||
placeholder="用户名"
|
||||
required
|
||||
/>
|
||||
<br />
|
||||
<input
|
||||
class="l-input"
|
||||
type="password"
|
||||
v-model="formData.userPassword"
|
||||
placeholder="密码"
|
||||
required
|
||||
/>
|
||||
<br />
|
||||
<input class="l-input" type="text" v-model="formData.roomId" placeholder="房间名" required />
|
||||
<br />
|
||||
<input class="l-input" type="password" v-model="formData.password" placeholder="房间密码" />
|
||||
<br />
|
||||
<div>
|
||||
<input class="w-auto" type="checkbox" v-model="savePwd" />
|
||||
<label title="明文保存到本机哦~"> 记住密码</label>
|
||||
</div>
|
||||
<button class="btn m-[10px]" @click="JoinRoom()">
|
||||
{{ "加入" }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.room {
|
||||
text-align: center;
|
||||
margin-top: 5vmax;
|
||||
|
||||
form {
|
||||
margin: auto;
|
||||
|
||||
input {
|
||||
width: 70%;
|
||||
|
||||
&:hover {
|
||||
padding: 10px 15px;
|
||||
width: 74%;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 15px;
|
||||
width: 70%;
|
||||
|
||||
&:hover {
|
||||
padding: 12px 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.room-dialog {
|
||||
text-align: center;
|
||||
|
||||
form {
|
||||
margin: auto;
|
||||
|
||||
input {
|
||||
width: 80%;
|
||||
|
||||
&:hover {
|
||||
padding: 10px 15px;
|
||||
width: 84%;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 15px;
|
||||
width: 80%;
|
||||
|
||||
&:hover {
|
||||
padding: 12px 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,14 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
important: true,
|
||||
darkMode: 'class',
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue","src/**/*.ts","src/**/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "ES2016",
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"extends": "@tsconfig/node18/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
const env = loadEnv('', process.cwd())
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
server: {
|
||||
host: true,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://127.0.0.1:8088",
|
||||
ws: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
vue()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
build: {
|
||||
cssCodeSplit: true,
|
||||
minify: 'terser'
|
||||
},
|
||||
base: env.VITE_BASEURL
|
||||
})
|
Loading…
Reference in New Issue