diff --git a/package.json b/package.json index 7ebb65b..527553d 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,15 @@ "@vueuse/integrations": "^10.1.2", "@vueuse/router": "^10.1.2", "core-js": "^3.8.3", + "date-fns": "^2.30.0", + "echarts": "^5.4.3", "grapheme-splitter": "^1.0.4", "linqts": "^1.15.0", "universal-cookie": "^4.0.4", "vite": "^4.3.9", "vite-svg-loader": "^4.0.0", "vue": "^3.2.13", + "vue-echarts": "^6.6.1", "vue-meta": "^3.0.0-alpha.10", "vue-request": "^2.0.3", "vue-router": "4", diff --git a/src/api/user.ts b/src/api/user.ts index 0db742f..0b2cda3 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -5,11 +5,11 @@ import { ref } from 'vue' import { useRouteParams } from '@vueuse/router' import { useRoute } from 'vue-router' -export const USERS = ref<{ [uId: number]: UserInfo }>({}) +export const USERS = ref<{ [id: string]: UserInfo }>({}) -export async function useUser(id: number | undefined = undefined) { +export async function useUser(id: string | undefined = undefined) { const route = useRoute() - id ??= Number(route.params.id) + id ??= route.params.id.toString() if (id) { if (!USERS.value[id]) { const result = await GetInfo(id) @@ -23,16 +23,18 @@ export async function useUser(id: number | undefined = undefined) { } } export async function useUserWithUId(id: number) { - if (!USERS.value[id]) { - const result = await GetInfo(id) + if (!USERS.value[id.toString()]) { + const result = await QueryGetAPI(`${USER_API_URL}info`, { + uId: id, + }) if (result.code == 200) { - USERS.value[id] = result.data + USERS.value[id.toString()] = result.data } } - return USERS.value[id] + return USERS.value[id.toString()] } -export async function GetInfo(id: number): Promise> { +export async function GetInfo(id: string): Promise> { return QueryGetAPI(`${USER_API_URL}info`, { id: id, }) diff --git a/src/components/RegisterAndLogin.vue b/src/components/RegisterAndLogin.vue index f4bcaa4..c4a5379 100644 --- a/src/components/RegisterAndLogin.vue +++ b/src/components/RegisterAndLogin.vue @@ -124,9 +124,7 @@ function onregisterButtonClick(e: MouseEvent) { }) }) } -function onLoginButtonClick(e: MouseEvent) { - e.preventDefault() - +function onLoginButtonClick() { formRef.value?.validate().then(async () => { isLoading.value = true await QueryPostAPI<{ @@ -178,7 +176,7 @@ function onLoginButtonClick(e: MouseEvent) { - + diff --git a/src/components/SongList.vue b/src/components/SongList.vue index 3304b33..4c505a7 100644 --- a/src/components/SongList.vue +++ b/src/components/SongList.vue @@ -146,9 +146,17 @@ const authorColumn = ref>({ }, filterOptions: authorsOptions.value, render(data) { - return h(NSpace, { size: 5 }, () => data.author.map((a) => h(NButton, { size: 'tiny', type: 'info', secondary: true, onClick: () => (authorColumn.value.filterOptionValue = a) }, () => a))) + return h(NSpace, { size: 5 }, () => data.author.map((a) => h(NButton, { size: 'tiny', type: 'info', secondary: true, onClick: () => onAuthorClick(a) }, () => a))) }, }) +const onAuthorClick = (author: string) => { + if(authorColumn.value.filterOptionValue == author){ + authorColumn.value.filterOptionValue = undefined + } + else { + authorColumn.value.filterOptionValue = author + } +} function createColumns(): DataTableColumns { authorColumn.value.filterOptions = authorsOptions.value @@ -405,7 +413,7 @@ onMounted(() => { - 共 {{ songsInternal.length }} 首 + 共 {{ songsComputed.length }} 首
@@ -434,7 +442,7 @@ onMounted(() => { - + diff --git a/src/data/constants.ts b/src/data/constants.ts index bb8ccc8..6380043 100644 --- a/src/data/constants.ts +++ b/src/data/constants.ts @@ -18,3 +18,4 @@ export const SONG_API_URL = `${BASE_API}song-list/` export const NOTIFACTION_API_URL = `${BASE_API}notifaction/` export const QUESTION_API_URL = `${BASE_API}qa/` export const LOTTERY_API_URL = `${BASE_API}lottery/` +export const HISTORY_API_URL = `${BASE_API}history/` diff --git a/src/router/index.ts b/src/router/index.ts index 35c9cd1..7536463 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -76,9 +76,14 @@ const routes: Array = [ component: () => import('@/views/manage/BiliVerifyView.vue'), }, { - path: 'setting', - name: 'manage-setting', - component: () => import('@/views/manage/SettingsManageView.vue'), + path: 'history', + name: 'manage-history', + component: () => import('@/views/manage/HistoryView.vue'), + }, + { + path: 'schedule', + name: 'manage-schedule', + component: () => import('@/views/manage/ScheduleManageView.vue'), }, ], }, diff --git a/src/views/ManageLayout.vue b/src/views/ManageLayout.vue index 4eaba45..40d5276 100644 --- a/src/views/ManageLayout.vue +++ b/src/views/ManageLayout.vue @@ -22,8 +22,8 @@ import { useMessage, } from 'naive-ui' import { h, onMounted, ref } from 'vue' -import { BrowsersOutline, Chatbox, Moon, MusicalNote, Sunny } from '@vicons/ionicons5' -import { Lottery24Filled } from '@vicons/fluent' +import { BrowsersOutline, Chatbox, Moon, MusicalNote, Sunny, AnalyticsSharp } from '@vicons/ionicons5' +import { CalendarClock24Filled, Lottery24Filled } from '@vicons/fluent' import { isLoadingAccount, useAccount } from '@/api/account' import RegisterAndLogin from '@/components/RegisterAndLogin.vue' import { RouterLink } from 'vue-router' @@ -48,6 +48,36 @@ function renderIcon(icon: unknown) { } const menuOptions = [ + { + label: () => + h( + RouterLink, + { + to: { + name: 'manage-history', + }, + disabled: !accountInfo.value?.isEmailVerified, + }, + { default: () => '历史' } + ), + key: 'manage-history', + icon: renderIcon(AnalyticsSharp), + }, + { + label: () => + h( + RouterLink, + { + to: { + name: 'manage-schedule', + }, + disabled: !accountInfo.value?.isEmailVerified, + }, + { default: () => '日程' } + ), + key: 'manage-schedule', + icon: renderIcon(CalendarClock24Filled), + }, { label: () => h( @@ -196,9 +226,7 @@ onMounted(() => { diff --git a/src/views/ViewerLayout.vue b/src/views/ViewerLayout.vue index 0f2b7d8..e156287 100644 --- a/src/views/ViewerLayout.vue +++ b/src/views/ViewerLayout.vue @@ -16,6 +16,7 @@ import { NModal, NEllipsis, MenuOption, +NSpin, } from 'naive-ui' import { computed, h, onMounted, ref } from 'vue' import { BookOutline as BookIcon, Chatbox, Home, Moon, MusicalNote, PersonOutline as PersonIcon, Sunny, WineOutline as WineIcon } from '@vicons/ionicons5' @@ -30,7 +31,7 @@ import { isDarkMode } from '@/Utils' const route = useRoute() const id = computed(() => { - return Number(route.params.id) + return route.params.id }) const themeType = useStorage('Settings.Theme', ThemeType.Auto); @@ -160,11 +161,14 @@ onMounted(async () => {
- + - + +
diff --git a/src/views/manage/HistoryView.vue b/src/views/manage/HistoryView.vue index cfe1f39..25f5144 100644 --- a/src/views/manage/HistoryView.vue +++ b/src/views/manage/HistoryView.vue @@ -1,2 +1,251 @@ \ No newline at end of file +import { useAccount } from '@/api/account' +import { QueryGetAPI } from '@/api/query' +import { HISTORY_API_URL } from '@/data/constants' +import { NCard, useMessage } from 'naive-ui' +import { onMounted, ref } from 'vue' +import { use } from 'echarts/core' +import { CanvasRenderer } from 'echarts/renderers' +import { LineChart } from 'echarts/charts' +import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, DataZoomComponent } from 'echarts/components' +import VChart, { THEME_KEY } from 'vue-echarts' +import { format } from 'date-fns' + +use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, DataZoomComponent, LineChart]) + +const accountInfo = useAccount() +const message = useMessage() + +const fansHistory = ref<{ time: number; count: number }[]>() +const guardHistory = ref<{ time: number; count: number }[]>() +const fansOption = ref() +const guardsOption = ref() + +async function getFansHistory() { + await QueryGetAPI< + { + time: number + count: number + }[] + >(HISTORY_API_URL + 'fans', { + id: accountInfo.value?.id, + }) + .then((data) => { + if (data.code == 200) { + fansHistory.value = data.data + } else { + message.error('加载失败: ' + data.message) + } + }) + .catch((err) => { + console.error(err) + message.error('加载失败') + }) +} +async function getGuardsHistory() { + await QueryGetAPI< + { + time: number + count: number + }[] + >(HISTORY_API_URL + 'guards', { + id: accountInfo.value?.id, + }) + .then((data) => { + if (data.code == 200) { + guardHistory.value = data.data + } else { + message.error('加载失败: ' + data.message) + } + }) + .catch((err) => { + console.error(err) + message.error('加载失败') + }) +} +function isSameDay(time1: number, time2: number) { + const time1Date = new Date(time1) + const time2Date = new Date(time2) + return time1Date.getFullYear() === time2Date.getFullYear() && time1Date.getMonth() === time2Date.getMonth() && time1Date.getDate() === time2Date.getDate() +} +function getOptions() { + let fansIncreacement = [] as { time: number; count: number; timeString: string }[] + let guards = [] as { time: number; count: number; timeString: string }[] + + let fansIncreacementLastHour = 0 + let lastHourFans = 0 + fansHistory.value?.forEach((f) => { + if (!isSameDay(f.time, fansIncreacementLastHour)) { + fansIncreacement.push({ + time: fansIncreacementLastHour, + count: fansIncreacementLastHour == 0 ? 0 : f.count - lastHourFans, + //将timeString转换为yyyy-MM-dd HH + timeString: format(f.time, 'yyyy-MM-dd'), + }) + fansIncreacementLastHour = f.time + lastHourFans = f.count + } + }) + let lastDayGuards = 0 + let lastDay = 0 + guardHistory.value?.forEach((g) => { + if (!isSameDay(g.time, lastDayGuards)) { + guards.push({ + time: lastDayGuards, + count: lastDay == 0 ? 0 : g.count - lastDayGuards, + //将timeString转换为yyyy-MM-dd HH + timeString: format(g.time, 'yyyy-MM-dd'), + }) + lastDay = g.time + lastDayGuards = g.count + } + }) + + fansOption.value = { + title: { + text: '粉丝数', + left: 'left', + }, + tooltip: { + trigger: 'axis', + }, + legend: {}, + yAxis: [ + { + type: 'value', + }, + { + type: 'value', + }, + ], + xAxis: [ + { + type: 'category', + axisTick: { + alignWithLabel: true, + }, + axisLine: { + onZero: false, + lineStyle: { + color: '#EE6666', + }, + }, + // prettier-ignore + data: fansIncreacement.map((f) => f.timeString), + }, + { + type: 'category', + axisTick: { + alignWithLabel: true, + }, + axisLine: { + onZero: false, + lineStyle: { + color: '#5470C6', + }, + }, + // prettier-ignore + data: fansHistory.value?.map(f => format(f.time, 'yyyy-MM-dd HH:mm')), + }, + ], + series: [ + { + name: '粉丝数', + type: 'line', + xAxisIndex: 1, + yAxisIndex: 1, + emphasis: { + focus: 'series', + }, + data: fansHistory.value?.map((f) => f.count), + }, + { + name: '增量 /日', + type: 'line', + smooth: true, + showSymbol: false, + emphasis: { + focus: 'series', + }, + data: fansIncreacement.map((f) => f.count), + }, + ], + dataZoom: [ + { + show: true, + realtime: true, + start: 0, + end: 100, + xAxisIndex: [0, 1], + }, + ], + } + guardsOption.value = { + title: { + text: '舰长数', + left: 'left', + }, + tooltip: { + trigger: 'axis', + }, + legend: {}, + yAxis: [ + { + type: 'value', + }, + { + type: 'value', + }, + ], + xAxis: [ + { + type: 'category', + axisTick: { + alignWithLabel: true, + }, + axisLine: { + onZero: false, + lineStyle: { + color: '#EE6666', + }, + }, + // prettier-ignore + data: guards.map((f) => f.timeString ), + }, + ], + series: [ + { + name: '舰长数', + type: 'line', + step: 'start', + emphasis: { + focus: 'series', + }, + data: guards.map((f) => f.count), + }, + ], + dataZoom: [ + { + show: true, + realtime: true, + start: 0, + end: 100, + xAxisIndex: [0, 1], + }, + ], + } +} + +onMounted(async () => { + await getFansHistory() + await getGuardsHistory() + getOptions() +}) + + + diff --git a/src/views/manage/LotteryView.vue b/src/views/manage/LotteryView.vue index f4cf11a..beb8c0f 100644 --- a/src/views/manage/LotteryView.vue +++ b/src/views/manage/LotteryView.vue @@ -1,9 +1,11 @@ + diff --git a/src/views/manage/QuestionBoxManageView.vue b/src/views/manage/QuestionBoxManageView.vue index 4d1c212..b621ffc 100644 --- a/src/views/manage/QuestionBoxManageView.vue +++ b/src/views/manage/QuestionBoxManageView.vue @@ -30,6 +30,7 @@ async function GetRecieveQAInfo() { if (data.data.length > 0) { recieveQuestions.value = new List(data.data) .OrderBy((d) => d.isReaded) + //.ThenByDescending(d => d.isFavorite) .ThenByDescending((d) => d.sendAt) .ToArray() } @@ -106,7 +107,7 @@ async function read(question: QAInfo, read: boolean) { async function favorite(question: QAInfo, fav: boolean) { await QueryGetAPI(QUESTION_API_URL + 'favorite', { id: question.id, - favorite: fav ? 'true' : 'false', + favorite: fav, }) .then((data) => { if (data.code == 200) { diff --git a/src/views/manage/ScheduleManageView.vue b/src/views/manage/ScheduleManageView.vue new file mode 100644 index 0000000..c3685af --- /dev/null +++ b/src/views/manage/ScheduleManageView.vue @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/src/views/view/QuestionBoxView.vue b/src/views/view/QuestionBoxView.vue index 0fe49ab..373fce9 100644 --- a/src/views/view/QuestionBoxView.vue +++ b/src/views/view/QuestionBoxView.vue @@ -1,24 +1,28 @@ diff --git a/src/views/view/ScheduleView.vue b/src/views/view/ScheduleView.vue new file mode 100644 index 0000000..e69de29 diff --git a/src/views/view/SongListView.vue b/src/views/view/SongListView.vue index fc1f0e8..25444a4 100644 --- a/src/views/view/SongListView.vue +++ b/src/views/view/SongListView.vue @@ -4,24 +4,23 @@ diff --git a/src/views/view/UserIndexView.vue b/src/views/view/UserIndexView.vue index 37904dc..36e7c94 100644 --- a/src/views/view/UserIndexView.vue +++ b/src/views/view/UserIndexView.vue @@ -3,20 +3,20 @@ diff --git a/src/views/view/indexTemplate/DefaultIndexTemplate.vue b/src/views/view/indexTemplate/DefaultIndexTemplate.vue index b0cd2f8..c576187 100644 --- a/src/views/view/indexTemplate/DefaultIndexTemplate.vue +++ b/src/views/view/indexTemplate/DefaultIndexTemplate.vue @@ -17,7 +17,7 @@ function navigate(url: string) {