This commit is contained in:
Megghy
2023-06-05 15:31:37 +08:00
parent 4dedacf449
commit 981d873225
30 changed files with 16609 additions and 5653 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
VITE_DEBUG_API=https://127.0.0.1/

4
default.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module 'vue3-aplayer' {
const content: any
export = content
}

6
env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
interface ImportMeta {
env: {
VITE_DEBUG_API?: string
}
}

21
index.html Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -3,34 +3,35 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "vite",
"build": "vue-cli-service build", "build": "vite build",
"lint": "vue-cli-service lint" "lint": "vite lint"
}, },
"dependencies": { "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", "core-js": "^3.8.3",
"universal-cookie": "^4.0.4",
"vite": "^4.3.9",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-router": "^4.0.3", "vue-meta": "^3.0.0-alpha.10",
"vue-router": "4",
"vue-turnstile": "^1.0.0", "vue-turnstile": "^1.0.0",
"vue3-aplayer": "^1.7.3",
"vuex": "^4.0.0" "vuex": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.21.3", "@babel/eslint-parser": "^7.21.3",
"@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0", "@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-babel": "~5.0.0", "@vicons/ionicons5": "^0.12.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",
"@vue/eslint-config-airbnb": "^6.0.0", "@vue/eslint-config-airbnb": "^6.0.0",
"@vue/eslint-config-typescript": "^9.1.0", "@vue/eslint-config-typescript": "^9.1.0",
"eslint": "^8.39.0", "eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-import": "^2.25.3", "eslint-plugin-import": "^2.25.3",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.11.0", "eslint-plugin-vue": "^9.11.0",
@@ -39,7 +40,6 @@
"naive-ui": "^2.34.3", "naive-ui": "^2.34.3",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"stylus": "^0.55.0", "stylus": "^0.55.0",
"stylus-loader": "^6.1.0",
"typescript": "~4.5.5" "typescript": "~4.5.5"
}, },
"gitHooks": { "gitHooks": {

View File

@@ -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>

View File

@@ -1,13 +1,113 @@
<template> <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> </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"> <style lang="stylus">
#app .v-enter-from,
font-family Avenir, Helvetica, Arial, sans-serif .v-leave-to {
-webkit-font-smoothing antialiased opacity: 0;
-moz-osx-font-smoothing grayscale }
text-align center /* 离开和进入过程中的样式 */
color #2c3e50 .v-enter-active,
margin-top 60px .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> </style>

View File

@@ -1,20 +1,43 @@
import QueryAPI from '@/api/query'
import { BASE_API } from '@/data/constants' import { BASE_API } from '@/data/constants'
import { APIRoot } from './api-models' 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/` const ACCOUNT_URL = `${BASE_API}account/`
export const ACCOUNT = ref<UserInfo>()
export async function Register(name: string, email: string, password: string): Promise<APIRoot<string>> { const cookies = useCookies()
return QueryAPI<string>(`${ACCOUNT_URL}register`, {
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, name,
email, email,
password, password,
token,
}) })
} }
export async function Login(nameOrEmail: string, password: string): Promise<APIRoot<string>> { export async function Login(nameOrEmail: string, password: string): Promise<APIRoot<string>> {
return QueryAPI<string>(`${ACCOUNT_URL}login`, { return QueryPostAPI<string>(`${ACCOUNT_URL}login`, {
nameOrEmail, nameOrEmail,
password, password,
}) })
} }
export async function Self(): Promise<APIRoot<UserInfo>> {
return QueryPostAPI<UserInfo>(`${ACCOUNT_URL}self`)
}

View File

@@ -1,5 +1,32 @@
export interface APIRoot<T> { export interface APIRoot<T> {
code: number; code: number
message: string; message: string
data: T; 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[]
} }

View File

@@ -1,13 +1,25 @@
/* eslint-disable indent */ /* 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>> { export async function QueryPostAPI<T>(url: string, body?: unknown): Promise<APIRoot<T>> {
const data = await fetch(url,{ const data = await fetch(url, {
method: 'post', method: 'post',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}, },
body: JSON.stringify(body) body: JSON.stringify(body),
}); // 不处理异常, 在页面处理 }) // 不处理异常, 在页面处理
return await data.json() as APIRoot<T>; 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
View 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,
})
}

View File

@@ -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>

View 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
View 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>

View File

@@ -1,3 +1,6 @@
const debugAPI = `${process.env.VITE_debugAPI}/api/` const debugAPI = import.meta.env.VITE_DEBUG_API
const releseAPI = `${document.location.protocol}//api.vtsuru.live/api/`; const releseAPI = `${document.location.protocol}//api.vtsuru.live/`
export const BASE_API = process.env.NODE_ENV === 'development' ? debugAPI : releseAPI; 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/`

View File

@@ -1,6 +1,9 @@
import { createApp } from 'vue'; import { createApp } from 'vue'
import App from './App.vue'; import App from './App.vue'
import router from './router'; import router from './router'
import store from './store'; 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()

View File

@@ -1,25 +1,59 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import IndexView from '../views/IndexView.vue'; import IndexView from '../views/IndexView.vue'
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
path: '/', path: '/',
name: 'home', name: 'index',
component: IndexView, component: IndexView,
}, },
{ {
path: '/about', path: '/user/:id',
name: 'about', name: 'user',
// route level code-splitting children: [
// this generates a separate chunk (about.[hash].js) for this route {
// which is lazy-loaded when the route is visited. path: '',
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue'), 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({ const router = createRouter({
history: createWebHistory(process.env.BASE_URL), history: createWebHistory(process.env.BASE_URL),
routes, routes,
}); })
export default router; export default router

View File

@@ -1,5 +0,0 @@
<template>
<div class="about">
<h1>This is an about page for Vtsuru.live</h1>
</div>
</template>

View File

@@ -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>

View File

@@ -1,13 +1,35 @@
<script setup lang="ts">
import RegisterAndLogin from '@/components/RegisterAndLogin.vue';
import { NGradientText, NSpace, NText } from 'naive-ui'
</script>
<template> <template>
<div style="display: flex;justify-content: center;"> <div class="index-background">
<div> <NSpace justify="center">
<NText strong tag="h1"> <NGradientText
vtsuru :size="50"
</NText> :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>
</div> </div>
</template> </template>
<script lang="ts" setup> <style lang="stylus">
import { NButton, NText } from 'naive-ui' body
</script> 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>

View 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
View 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>

View 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>

View 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>

View File

@@ -0,0 +1,7 @@
<script setup lang="ts">
</script>
<template>
1
</template>

View 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>

View File

@@ -12,9 +12,7 @@
"useDefineForClassFields": true, "useDefineForClassFields": true,
"sourceMap": true, "sourceMap": true,
"baseUrl": ".", "baseUrl": ".",
"types": [ "types": ["node"],
"webpack-env",
],
"paths": { "paths": {
"@/*": [ "@/*": [
"src/*" "src/*"
@@ -33,7 +31,7 @@
"src/**/*.vue", "src/**/*.vue",
"tests/**/*.ts", "tests/**/*.ts",
"tests/**/*.tsx" "tests/**/*.tsx"
], , "env.d.ts", "default.d.ts" ],
"exclude": [ "exclude": [
"node_modules" "node_modules"
] ]

16
vite.config.ts Normal file
View 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': {},
},
})

5999
yarn.lock

File diff suppressed because it is too large Load Diff