mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 10:26:56 +08:00
nothing
This commit is contained in:
4
default.d.ts
vendored
Normal file
4
default.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module 'vue3-aplayer' {
|
||||
const content: any
|
||||
export = content
|
||||
}
|
||||
6
env.d.ts
vendored
Normal file
6
env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
interface ImportMeta {
|
||||
env: {
|
||||
VITE_DEBUG_API?: string
|
||||
}
|
||||
}
|
||||
21
index.html
Normal file
21
index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>vtsuru.live</title>
|
||||
<meta name="description" content="主包工具站" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>
|
||||
We're sorry but Vtsuru,live doesn't work properly without JavaScript
|
||||
enabled. Please enable it to continue.
|
||||
</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
15228
package-lock.json
generated
Normal file
15228
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@@ -3,34 +3,35 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "vite lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^10.1.0",
|
||||
"@types/node": "^20.2.5",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"@vueuse/integrations": "^10.1.2",
|
||||
"@vueuse/router": "^10.1.2",
|
||||
"core-js": "^3.8.3",
|
||||
"universal-cookie": "^4.0.4",
|
||||
"vite": "^4.3.9",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.0.3",
|
||||
"vue-meta": "^3.0.0-alpha.10",
|
||||
"vue-router": "4",
|
||||
"vue-turnstile": "^1.0.0",
|
||||
"vue3-aplayer": "^1.7.3",
|
||||
"vuex": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.21.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"@typescript-eslint/parser": "^5.4.0",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-plugin-pwa": "~5.0.0",
|
||||
"@vue/cli-plugin-router": "~5.0.0",
|
||||
"@vue/cli-plugin-typescript": "~5.0.0",
|
||||
"@vue/cli-plugin-vuex": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"@vicons/ionicons5": "^0.12.0",
|
||||
"@vue/eslint-config-airbnb": "^6.0.0",
|
||||
"@vue/eslint-config-typescript": "^9.1.0",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.11.0",
|
||||
@@ -39,7 +40,6 @@
|
||||
"naive-ui": "^2.34.3",
|
||||
"prettier": "^2.8.8",
|
||||
"stylus": "^0.55.0",
|
||||
"stylus-loader": "^6.1.0",
|
||||
"typescript": "~4.5.5"
|
||||
},
|
||||
"gitHooks": {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-Hans">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong
|
||||
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
|
||||
properly without JavaScript enabled. Please enable it to continue.
|
||||
</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
116
src/App.vue
116
src/App.vue
@@ -1,13 +1,113 @@
|
||||
<template>
|
||||
<router-view/>
|
||||
<NConfigProvider :theme-overrides="themeOverrides" style="height:100vh;">
|
||||
<ViewerLayout v-if="layout == 'viewer'" />
|
||||
<ManageLayout v-else-if="layout == 'manage'" />
|
||||
<template v-else>
|
||||
<RouterView />
|
||||
</template>
|
||||
</NConfigProvider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ViewerLayout from '@/views/ViewerLayout.vue'
|
||||
import ManageLayout from '@/views/ManageLayout.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { NConfigProvider } from 'naive-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const route = useRoute()
|
||||
const layout = computed(() => {
|
||||
if (route.path.startsWith('/user')) {
|
||||
return 'viewer'
|
||||
} else if (route.path.startsWith('/manage')) {
|
||||
return 'manage'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const themeOverrides = {
|
||||
common: {
|
||||
primaryColor: '#9ddddc',
|
||||
},
|
||||
// ...
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
#app
|
||||
font-family Avenir, Helvetica, Arial, sans-serif
|
||||
-webkit-font-smoothing antialiased
|
||||
-moz-osx-font-smoothing grayscale
|
||||
text-align center
|
||||
color #2c3e50
|
||||
margin-top 60px
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
/* 离开和进入过程中的样式 */
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
/* 添加过渡动画 */
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
/* 进入之后和离开之前的样式 */
|
||||
.v-enter-to,
|
||||
.v-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
.bounce-enter-active {
|
||||
animation: bounce 0.3s;
|
||||
}
|
||||
.bounce-leave-active {
|
||||
animation: bounce 0.3s reverse;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
60% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.scale-enter-active,
|
||||
.scale-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.scale-enter-from,
|
||||
.scale-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: all 0.5s ease-out;
|
||||
}
|
||||
.slide-enter-to {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
.slide-enter-from {
|
||||
position: absolute;
|
||||
right: -100%;
|
||||
}
|
||||
.slide-leave-to {
|
||||
position: absolute;
|
||||
left: -100%;
|
||||
}
|
||||
.slide-leave-from {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,20 +1,43 @@
|
||||
import QueryAPI from '@/api/query'
|
||||
import { BASE_API } from '@/data/constants'
|
||||
import { APIRoot } from './api-models'
|
||||
import { QueryPostAPI } from '@/api/query'
|
||||
import { UserInfo } from '@/api/api-models'
|
||||
import { ref } from 'vue'
|
||||
import { useCookies } from '@vueuse/integrations/useCookies'
|
||||
|
||||
const ACCOUNT_URL = `${BASE_API}account/`
|
||||
export const ACCOUNT = ref<UserInfo>()
|
||||
|
||||
export async function Register(name: string, email: string, password: string): Promise<APIRoot<string>> {
|
||||
return QueryAPI<string>(`${ACCOUNT_URL}register`, {
|
||||
const cookies = useCookies()
|
||||
|
||||
export async function GetSelfAccount() {
|
||||
const cookie = cookies.get('VTSURU_SESSION')
|
||||
if (cookie) {
|
||||
const result = await Self()
|
||||
if (result.code == 200) {
|
||||
ACCOUNT.value = result.data
|
||||
}
|
||||
}
|
||||
}
|
||||
export function useAccount() {
|
||||
return ACCOUNT
|
||||
}
|
||||
|
||||
export async function Register(name: string, email: string, password: string, token: string): Promise<APIRoot<string>> {
|
||||
return QueryPostAPI<string>(`${ACCOUNT_URL}register`, {
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
token,
|
||||
})
|
||||
}
|
||||
|
||||
export async function Login(nameOrEmail: string, password: string): Promise<APIRoot<string>> {
|
||||
return QueryAPI<string>(`${ACCOUNT_URL}login`, {
|
||||
return QueryPostAPI<string>(`${ACCOUNT_URL}login`, {
|
||||
nameOrEmail,
|
||||
password,
|
||||
})
|
||||
}
|
||||
export async function Self(): Promise<APIRoot<UserInfo>> {
|
||||
return QueryPostAPI<UserInfo>(`${ACCOUNT_URL}self`)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
export interface APIRoot<T> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
export interface PaginationResponse<T> {
|
||||
total: number
|
||||
index: number
|
||||
size: number
|
||||
hasMore: boolean
|
||||
datas: T
|
||||
}
|
||||
export interface UserInfo {
|
||||
name: string
|
||||
uId: number
|
||||
createAt: number
|
||||
}
|
||||
export interface AccountInfo extends UserInfo {
|
||||
isRoomValid: boolean
|
||||
enableFunctions: string[]
|
||||
}
|
||||
export interface SongsInfo {
|
||||
id: string
|
||||
name: string
|
||||
author: string
|
||||
url: string
|
||||
cover: string
|
||||
from: string
|
||||
language: string
|
||||
desc: string
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
/* eslint-disable indent */
|
||||
import { APIRoot } from './api-models';
|
||||
import { APIRoot, PaginationResponse } from './api-models'
|
||||
|
||||
export default async function QueryAPI<T>(url: string, body?: unknown): Promise<APIRoot<T>> {
|
||||
const data = await fetch(url,{
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
}); // 不处理异常, 在页面处理
|
||||
return await data.json() as APIRoot<T>;
|
||||
export async function QueryPostAPI<T>(url: string, body?: unknown): Promise<APIRoot<T>> {
|
||||
const data = await fetch(url, {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}) // 不处理异常, 在页面处理
|
||||
return (await data.json()) as APIRoot<T>
|
||||
}
|
||||
export async function QueryGetAPI<T>(urlString: string, params?: any): Promise<APIRoot<T>> {
|
||||
const url = new URL(urlString)
|
||||
url.search = new URLSearchParams(params).toString()
|
||||
const data = await fetch(url.toString()) // 不处理异常, 在页面处理
|
||||
return (await data.json()) as APIRoot<T>
|
||||
}
|
||||
export async function QueryPostPaginationAPI<T>(url: string, body?: unknown): Promise<APIRoot<PaginationResponse<T>>> {
|
||||
return await QueryPostAPI<PaginationResponse<T>>(url, body)
|
||||
}
|
||||
export async function QueryGetPaginationAPI<T>(urlString: string, params?: any): Promise<APIRoot<PaginationResponse<T>>> {
|
||||
return await QueryGetAPI<PaginationResponse<T>>(urlString, params)
|
||||
}
|
||||
|
||||
23
src/api/user.ts
Normal file
23
src/api/user.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { QueryGetAPI } from '@/api/query'
|
||||
import { BASE_API } from '@/data/constants'
|
||||
import { APIRoot, UserInfo } from './api-models'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const ACCOUNT_URL = `${BASE_API}user/`
|
||||
export const USERS = ref<{ [uId: number]: UserInfo }>({})
|
||||
|
||||
export async function useUser(uId: number) {
|
||||
if (!USERS.value[uId]) {
|
||||
const result = await GetInfo(uId)
|
||||
if (result.code == 200) {
|
||||
USERS.value[uId] = result.data
|
||||
}
|
||||
}
|
||||
return USERS.value[uId]
|
||||
}
|
||||
|
||||
export async function GetInfo(uId: number): Promise<APIRoot<UserInfo>> {
|
||||
return QueryGetAPI<UserInfo>(`${ACCOUNT_URL}info`, {
|
||||
uId: uId,
|
||||
})
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<div class="hello">
|
||||
<h1>{{ msg }}</h1>
|
||||
<p>
|
||||
For a guide and recipes on how to configure / customize this project,<br />
|
||||
check out the
|
||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
||||
</p>
|
||||
<h3>Installed CLI Plugins</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a>
|
||||
</li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa" target="_blank" rel="noopener">pwa</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
|
||||
</ul>
|
||||
<h3>Essential Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
||||
</ul>
|
||||
<h3>Ecosystem</h3>
|
||||
<ul>
|
||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HelloWorld',
|
||||
props: {
|
||||
msg: String,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="stylus">
|
||||
h3
|
||||
margin 40px 0 0
|
||||
|
||||
ul
|
||||
list-style-type none
|
||||
padding 0
|
||||
|
||||
li
|
||||
display inline-block
|
||||
margin 0 10px
|
||||
|
||||
a
|
||||
color #42b983
|
||||
</style>
|
||||
109
src/components/RegisterAndLogin.vue
Normal file
109
src/components/RegisterAndLogin.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { FormInst, FormItemInst, FormItemRule, FormRules, NButton, NCard, NForm, NFormItem, NInput, NSpace } from 'naive-ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface RegisterModel {
|
||||
username: string
|
||||
email: string
|
||||
password: string
|
||||
reenteredPassword: string
|
||||
}
|
||||
interface LoginModel {
|
||||
account: string
|
||||
password: string
|
||||
}
|
||||
|
||||
const isRegister = ref(false)
|
||||
|
||||
const registerModel = ref<RegisterModel>({} as RegisterModel)
|
||||
const loginModel = ref<LoginModel>({} as LoginModel)
|
||||
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
const rPasswordFormItemRef = ref<FormItemInst | null>(null)
|
||||
const rules: FormRules = {
|
||||
account: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入用户名或邮箱',
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
},
|
||||
],
|
||||
reenteredPassword: [
|
||||
{
|
||||
required: true,
|
||||
message: '请再次输入密码',
|
||||
trigger: ['input', 'blur'],
|
||||
},
|
||||
{
|
||||
validator: validatePasswordStartWith,
|
||||
message: '两次密码输入不一致',
|
||||
trigger: 'input',
|
||||
},
|
||||
{
|
||||
validator: validatePasswordSame,
|
||||
message: '两次密码输入不一致',
|
||||
trigger: ['blur', 'password-input'],
|
||||
},
|
||||
],
|
||||
}
|
||||
function validatePasswordStartWith(rule: FormItemRule, value: string): boolean {
|
||||
return !!registerModel.value.password && registerModel.value.password.startsWith(value) && registerModel.value.password.length >= value.length
|
||||
}
|
||||
function validatePasswordSame(rule: FormItemRule, value: string): boolean {
|
||||
return value === registerModel.value.password
|
||||
}
|
||||
function onPasswordInput() {
|
||||
if (registerModel.value.reenteredPassword) {
|
||||
rPasswordFormItemRef.value?.validate({ trigger: 'password-input' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard embedded>
|
||||
<template #header>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<span v-if="isRegister"> 注册 </span>
|
||||
<span v-else> 登陆 </span>
|
||||
</Transition>
|
||||
</template>
|
||||
<Transition name="scale" mode="out-in">
|
||||
<div v-if="isRegister">
|
||||
<NForm ref="formRef" :rules="rules" :model="registerModel">
|
||||
<NFormItem path="username" label="用户名">
|
||||
<NInput v-model:value="registerModel.username" />
|
||||
</NFormItem>
|
||||
<NFormItem path="email" label="邮箱">
|
||||
<NInput v-model:value="registerModel.email" />
|
||||
</NFormItem>
|
||||
<NFormItem path="password" label="密码">
|
||||
<NInput v-model:value="registerModel.password" type="password" @input="onPasswordInput" @keydown.enter.prevent />
|
||||
</NFormItem>
|
||||
<NFormItem ref="rPasswordFormItemRef" first path="reenteredPassword" label="重复密码">
|
||||
<NInput v-model:value="registerModel.reenteredPassword" :disabled="!registerModel.password" type="password" @keydown.enter.prevent />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<NButton @click="isRegister = false"> 或者现在去登陆 </NButton>
|
||||
</div>
|
||||
<div v-else>
|
||||
<NForm ref="formRef" :rules="rules" :model="registerModel">
|
||||
<NFormItem path="account" label="用户名或邮箱">
|
||||
<NInput v-model:value="loginModel.account" />
|
||||
</NFormItem>
|
||||
<NFormItem path="password" label="密码">
|
||||
<NInput v-model:value="loginModel.password" type="password" @input="onPasswordInput" @keydown.enter.prevent />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<NSpace vertical justify="center" align="center">
|
||||
<NButton type="primary" size="large"> 登陆 </NButton>
|
||||
<NButton @click="isRegister = true" size="small" text> 或者现在去注册 </NButton>
|
||||
</NSpace>
|
||||
</div>
|
||||
</Transition>
|
||||
</NCard>
|
||||
</template>
|
||||
132
src/components/SongList.vue
Normal file
132
src/components/SongList.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
import { SongsInfo } from '@/api/api-models'
|
||||
import { DataTableColumns, NAvatar, NButton, NDataTable, NInput, NList, NListItem, NSpace } from 'naive-ui'
|
||||
import { onMounted, h, ref } from 'vue'
|
||||
import APlayer from 'vue3-aplayer'
|
||||
|
||||
const props = defineProps<{
|
||||
songs: SongsInfo[]
|
||||
canEdit?: boolean
|
||||
}>()
|
||||
const songsInternal = ref<{ [id: string]: SongsInfo }>({})
|
||||
const columns = ref<DataTableColumns<SongsInfo>>()
|
||||
const aplayerMusic = ref<{
|
||||
title: string
|
||||
artist: string
|
||||
src: string
|
||||
pic: string
|
||||
}>()
|
||||
|
||||
const createColumns = (): DataTableColumns<SongsInfo> => [
|
||||
{
|
||||
title: '',
|
||||
key: 'cover',
|
||||
resizable: false,
|
||||
width: 50,
|
||||
render(data) {
|
||||
return h(NAvatar, {
|
||||
src: data.cover,
|
||||
imgProps: {
|
||||
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
key: 'name',
|
||||
resizable: true,
|
||||
minWidth: 100,
|
||||
render(data) {
|
||||
return props.canEdit
|
||||
? h(NInput, {
|
||||
value: data.name,
|
||||
onUpdateValue(v) {
|
||||
songsInternal.value[data.id].name = v
|
||||
},
|
||||
})
|
||||
: h('span', data.name)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '作者',
|
||||
key: 'author',
|
||||
resizable: true,
|
||||
render(data) {
|
||||
return props.canEdit
|
||||
? h(NInput, {
|
||||
value: data.author,
|
||||
onUpdateValue(v) {
|
||||
songsInternal.value[data.id].author = v
|
||||
},
|
||||
})
|
||||
: h('span', data.author)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
key: 'description',
|
||||
resizable: true,
|
||||
minWidth: 75,
|
||||
render(data) {
|
||||
return props.canEdit
|
||||
? h(NInput, {
|
||||
value: data.desc,
|
||||
onUpdateValue(v) {
|
||||
songsInternal.value[data.id].desc = v
|
||||
},
|
||||
})
|
||||
: h('span', data.desc)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'manage',
|
||||
minWidth: 75,
|
||||
render(data) {
|
||||
return h(NSpace, [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
onClick: () => console.log(1),
|
||||
},
|
||||
{
|
||||
default: () => '保存',
|
||||
}
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
type: 'primary',
|
||||
onClick: () => {
|
||||
aplayerMusic.value = {
|
||||
title: data.name,
|
||||
artist: data.author,
|
||||
src: data.url,
|
||||
pic: data.cover,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => '播放',
|
||||
}
|
||||
),
|
||||
])
|
||||
},
|
||||
},
|
||||
]
|
||||
onMounted(() => {
|
||||
props.songs.forEach((song) => {
|
||||
songsInternal.value[song.id] = song
|
||||
})
|
||||
columns.value = createColumns()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
歌单 {{ songs.length }}
|
||||
<Transition>
|
||||
<APlayer v-if="aplayerMusic" :music="aplayerMusic" />
|
||||
</Transition>
|
||||
<NDataTable :columns="columns" :data="songs"> </NDataTable>
|
||||
</template>
|
||||
@@ -1,3 +1,6 @@
|
||||
const debugAPI = `${process.env.VITE_debugAPI}/api/`
|
||||
const releseAPI = `${document.location.protocol}//api.vtsuru.live/api/`;
|
||||
export const BASE_API = process.env.NODE_ENV === 'development' ? debugAPI : releseAPI;
|
||||
const debugAPI = import.meta.env.VITE_DEBUG_API
|
||||
const releseAPI = `${document.location.protocol}//api.vtsuru.live/`
|
||||
export const BASE_API = process.env.NODE_ENV === 'development' ? debugAPI : releseAPI
|
||||
export const FETCH_API = 'https://fetch.vtsuru.live/'
|
||||
|
||||
export const USER_URL = `${BASE_API}user/`
|
||||
|
||||
13
src/main.ts
13
src/main.ts
@@ -1,6 +1,9 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import { GetSelfAccount } from './api/account'
|
||||
|
||||
createApp(App).use(store).use(router).mount('#app');
|
||||
createApp(App).use(store).use(router).mount('#app')
|
||||
|
||||
await GetSelfAccount()
|
||||
|
||||
@@ -1,25 +1,59 @@
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
||||
import IndexView from '../views/IndexView.vue';
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
||||
import IndexView from '../views/IndexView.vue'
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
name: 'index',
|
||||
component: IndexView,
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue'),
|
||||
path: '/user/:id',
|
||||
name: 'user',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'user-index',
|
||||
component: () => import('@/views/view/IndexView.vue'),
|
||||
meta: {
|
||||
title: '主页',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'songlist',
|
||||
name: 'user-songList',
|
||||
component: () => import('@/views/view/SongListView.vue'),
|
||||
meta: {
|
||||
title: '歌单',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
//管理页面
|
||||
{
|
||||
path: '/manage',
|
||||
name: 'manage',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'manage-index',
|
||||
component: () => import('@/views/manage/DashboardView.vue'),
|
||||
meta: {
|
||||
title: '管理',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'songlist',
|
||||
name: 'songList',
|
||||
component: () => import('@/views/view/SongListView.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes,
|
||||
});
|
||||
})
|
||||
|
||||
export default router;
|
||||
export default router
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page for Vtsuru.live</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,18 +0,0 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<img alt="Vue logo" src="../assets/logo.png" />
|
||||
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import HelloWorld from '@/components/HelloWorld.vue' // @ is an alias to /src
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HomeView',
|
||||
components: {
|
||||
HelloWorld,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,13 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import RegisterAndLogin from '@/components/RegisterAndLogin.vue';
|
||||
import { NGradientText, NSpace, NText } from 'naive-ui'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="display: flex;justify-content: center;">
|
||||
<div>
|
||||
<NText strong tag="h1">
|
||||
vtsuru
|
||||
</NText>
|
||||
<div class="index-background">
|
||||
<NSpace justify="center">
|
||||
<NGradientText
|
||||
:size="50"
|
||||
:gradient="{
|
||||
deg: 180,
|
||||
from: '#e5e5e5',
|
||||
to: '#c2ebeb',
|
||||
}"
|
||||
style="font-family: Microsoft YaHei,Times New Roman, Times, serif;"
|
||||
>
|
||||
VTSURU.LIVE
|
||||
</NGradientText>
|
||||
</NSpace>
|
||||
<div style="width:500px;">
|
||||
<RegisterAndLogin />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { NButton, NText } from 'naive-ui'
|
||||
</script>
|
||||
<style lang="stylus">
|
||||
body
|
||||
margin:0
|
||||
.index-background
|
||||
height: 100vh;
|
||||
background: #8360c3; /* fallback for old browsers */
|
||||
background: -webkit-linear-gradient(to right, #2ebf91, #8360c3); /* Chrome 10-25, Safari 5.1-6 */
|
||||
background: linear-gradient(to right, #2ebf91, #8360c3); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
|
||||
</style>
|
||||
|
||||
55
src/views/ManageLayout.vue
Normal file
55
src/views/ManageLayout.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { NAvatar, NButton, NCard, NIcon, NLayout, NLayoutFooter, NLayoutHeader, NLayoutSider, NMenu, NSpace, NText } from 'naive-ui'
|
||||
import { h } from 'vue'
|
||||
import { BookOutline } from '@vicons/ionicons5'
|
||||
import { useAccount } from '@/api/account'
|
||||
import RegisterAndLogin from '@/components/RegisterAndLogin.vue'
|
||||
|
||||
const accountInfo = useAccount()
|
||||
|
||||
function renderIcon(icon: unknown) {
|
||||
return () => h(NIcon, null, { default: () => h(icon as any) })
|
||||
}
|
||||
|
||||
const menuOptions = [
|
||||
{
|
||||
label: '歌单',
|
||||
key: 'hear-the-wind-sing',
|
||||
icon: renderIcon(BookOutline),
|
||||
},
|
||||
{
|
||||
label: '舞,舞,舞',
|
||||
key: 'dance-dance-dance',
|
||||
icon: renderIcon(BookOutline),
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NLayout v-if="accountInfo">
|
||||
<NLayoutHeader bordered style="height: 50px"> Header Header Header </NLayoutHeader>
|
||||
<NLayout has-sider style="height: calc(100vh - 50px)">
|
||||
<NLayoutSider bordered show-trigger collapse-mode="width" :collapsed-width="64" :width="240" :native-scrollbar="false" style="max-height: 320px">
|
||||
<NMenu :collapsed-width="64" :collapsed-icon-size="22" :options="menuOptions" />
|
||||
</NLayoutSider>
|
||||
<NLayout style="height: 100%">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<KeepAlive>
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
</RouterView>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
<template v-else>
|
||||
<div style="display: flex;justify-content: center;align-items: center;flex-direction: column;padding: 50px;height: 100%;box-sizing: border-box;">
|
||||
<NText>
|
||||
请登录或注册后使用
|
||||
</NText>
|
||||
<NButton tag="a" href="/">
|
||||
回到主页
|
||||
</NButton>
|
||||
<RegisterAndLogin style="max-width: 500px;min-width: 350px;"/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
101
src/views/ViewerLayout.vue
Normal file
101
src/views/ViewerLayout.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<!-- eslint-disable vue/component-name-in-template-casing -->
|
||||
<script setup lang="ts">
|
||||
import { NAvatar, NCard, NIcon, NLayout, NLayoutFooter, NLayoutHeader, NLayoutSider, NMenu, NSpace, NText, NButton, NEmpty, NResult } from 'naive-ui'
|
||||
import { computed, h, onMounted, ref } from 'vue'
|
||||
import { BookOutline as BookIcon, PersonOutline as PersonIcon, WineOutline as WineIcon } from '@vicons/ionicons5'
|
||||
import { GetInfo, useUser } from '@/api/user'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { UserInfo } from '@/api/api-models'
|
||||
import { FETCH_API, USER_URL } from '@/data/constants'
|
||||
import { useAccount } from '@/api/account'
|
||||
|
||||
const route = useRoute()
|
||||
const uId = computed(() => {
|
||||
return Number(route.params.id)
|
||||
})
|
||||
|
||||
const userInfo = ref<UserInfo>()
|
||||
const biliUserInfo = ref()
|
||||
const accountInfo = useAccount()
|
||||
|
||||
function renderIcon(icon: unknown) {
|
||||
return () => h(NIcon, null, { default: () => h(icon as any) })
|
||||
}
|
||||
const menuOptions = [
|
||||
{
|
||||
label: '歌单',
|
||||
key: 'hear-the-wind-sing',
|
||||
icon: renderIcon(BookIcon),
|
||||
},
|
||||
{
|
||||
label: '舞,舞,舞',
|
||||
key: 'dance-dance-dance',
|
||||
icon: renderIcon(BookIcon),
|
||||
},
|
||||
]
|
||||
async function RequestBiliUserData() {
|
||||
await fetch(FETCH_API + `https://account.bilibili.com/api/member/getCardByMid?mid=${uId.value}`)
|
||||
.then(async (respone) => {
|
||||
let data = await respone.json()
|
||||
if (data.code == 0) {
|
||||
biliUserInfo.value = data.card
|
||||
} else {
|
||||
throw new Error('Bili User API Error: ' + data.message)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await RequestBiliUserData()
|
||||
userInfo.value = await useUser(uId.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NResult v-if="!uId" status="error" title="输入的uId无效" description="再检查检查" />
|
||||
<NResult v-else-if="!userInfo" status="error" title="未找到指定 uId 的用户" description="或者是没有进行认证" />
|
||||
<NLayout v-else style="height: 100vh">
|
||||
<NLayoutHeader style="height: 50px; display: flex; justify-content: left; align-items: center; padding-left: 15px">
|
||||
<NSpace>
|
||||
<NText> VTSURU </NText>
|
||||
<span>
|
||||
{{ $route.meta.title }}
|
||||
</span>
|
||||
</NSpace>
|
||||
<NButton style="right: 0px; position: relative" type="primary"> 控制台 </NButton>
|
||||
</NLayoutHeader>
|
||||
<NLayout has-sider style="height: calc(100vh - 50px)">
|
||||
<NLayoutSider show-trigger collapse-mode="width" :collapsed-width="64" :width="180" :native-scrollbar="false">
|
||||
<Transition>
|
||||
<div v-if="biliUserInfo">
|
||||
<NSpace vertical justify="center" align="center">
|
||||
<NAvatar :src="biliUserInfo.face" :img-props="{ referrerpolicy: 'no-referrer' }" />
|
||||
</NSpace>
|
||||
</div>
|
||||
</Transition>
|
||||
<NMenu :collapsed-width="64" :collapsed-icon-size="22" :options="menuOptions" />
|
||||
</NLayoutSider>
|
||||
<NLayout style="height: 100%">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<KeepAlive>
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
</RouterView>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
</template>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.viewer-page-content{
|
||||
height: 100%;
|
||||
border-radius: 25px;
|
||||
box-shadow: inset 5px 5px 6px #8b8b8b17, inset -5px -5px 6px #8b8b8b17;
|
||||
padding: 15px;
|
||||
margin-right: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
16
src/views/manage/DashboardView.vue
Normal file
16
src/views/manage/DashboardView.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { useAccount } from '@/api/account';
|
||||
import { NThing } from 'naive-ui';
|
||||
|
||||
const accountInfo = useAccount()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<NThing>
|
||||
<template #header>
|
||||
|
||||
</template>
|
||||
</NThing>
|
||||
</template>
|
||||
13
src/views/view/IndexView.vue
Normal file
13
src/views/view/IndexView.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div style="display: flex;justify-content: center;">
|
||||
<div>
|
||||
<NText strong tag="h1">
|
||||
vtsuru
|
||||
</NText>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { NButton, NText } from 'naive-ui'
|
||||
</script>
|
||||
7
src/views/view/QuestionBoxView.vue
Normal file
7
src/views/view/QuestionBoxView.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
1
|
||||
</template>
|
||||
59
src/views/view/SongListView.vue
Normal file
59
src/views/view/SongListView.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { SongsInfo } from '@/api/api-models'
|
||||
import { QueryGetPaginationAPI } from '@/api/query'
|
||||
import SongList from '@/components/SongList.vue'
|
||||
import { USER_URL } from '@/data/constants'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouteParams } from '@vueuse/router'
|
||||
|
||||
const songs = ref<SongsInfo[]>()
|
||||
const uId = useRouteParams('id', '-1', { transform: Number })
|
||||
|
||||
async function RequestData() {
|
||||
songs.value = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'test',
|
||||
author: '雪路',
|
||||
url: 'https://music.163.com/song/media/outer/url?id=1995844771.mp3',
|
||||
cover: 'https://ukamnads.icu/file/components.png',
|
||||
from: '网易云',
|
||||
language: '中文',
|
||||
desc: 'xuelu',
|
||||
tags: ['hao'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'test2',
|
||||
author: '雪路2',
|
||||
url: 'https://music.163.com/song/media/outer/url?id=1995844771.mp3',
|
||||
cover: 'https://ukamnads.icu/file/components.png',
|
||||
from: '网易云2',
|
||||
language: '中文2',
|
||||
desc: 'xuelu',
|
||||
tags: ['hao'],
|
||||
},
|
||||
]
|
||||
await QueryGetPaginationAPI<SongsInfo[]>(`${USER_URL}info`, {
|
||||
uId: uId.value,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.code == 200) {
|
||||
songs.value = result.data.datas
|
||||
} else {
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await RequestData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
歌单
|
||||
<SongList :songs="songs ?? []" />
|
||||
</template>
|
||||
@@ -12,9 +12,7 @@
|
||||
"useDefineForClassFields": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": [
|
||||
"webpack-env",
|
||||
],
|
||||
"types": ["node"],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
@@ -33,7 +31,7 @@
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
, "env.d.ts", "default.d.ts" ],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
|
||||
16
vite.config.ts
Normal file
16
vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// vite.config.ts
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
define: {
|
||||
'process.env': {},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user