From 624da23a1ca1d8f51b750c89e30e52e7e8d8b28e Mon Sep 17 00:00:00 2001 From: leonincs Date: Sun, 26 Apr 2026 15:47:17 +0800 Subject: [PATCH] Add frontend messaging experience --- frontend/src/api/message.ts | 10 + frontend/src/api/types.ts | 19 +- frontend/src/components/AppShell.vue | 249 +++++++----- frontend/src/router/index.ts | 19 +- frontend/src/style.css | 242 ++++++------ frontend/src/views/AccountView.vue | 100 ++--- frontend/src/views/MessageView.vue | 503 +++++++++++++++++++++++++ frontend/src/views/UserProfileView.vue | 133 ++++--- 8 files changed, 944 insertions(+), 331 deletions(-) create mode 100644 frontend/src/api/message.ts create mode 100644 frontend/src/views/MessageView.vue diff --git a/frontend/src/api/message.ts b/frontend/src/api/message.ts new file mode 100644 index 0000000..5052da8 --- /dev/null +++ b/frontend/src/api/message.ts @@ -0,0 +1,10 @@ +import { postJson } from './client' +import type { DirectMessage, ListMessagesResponse } from './types' + +export function sendMessage(toId: number, content: string) { + return postJson('/message/send', { to_id: toId, content }, { authRequired: true }) +} + +export function listMessages(peerId: number) { + return postJson('/message/list', { peer_id: peerId }, { authRequired: true }) +} diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 0311488..37d30a5 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -1,6 +1,19 @@ -export type MessageResponse = { message: string } - -export type TokenResponse = { token: string; refresh_token?: string; account_id?: number; username?: string } +export type MessageResponse = { message: string } + +export type DirectMessage = { + id: number + from_id: number + to_id: number + content: string + is_read: boolean + created_at: string +} + +export type ListMessagesResponse = { + messages: DirectMessage[] +} + +export type TokenResponse = { token: string; refresh_token?: string; account_id?: number; username?: string } export type Account = { id: number diff --git a/frontend/src/components/AppShell.vue b/frontend/src/components/AppShell.vue index 1e0042f..97756da 100644 --- a/frontend/src/components/AppShell.vue +++ b/frontend/src/components/AppShell.vue @@ -58,11 +58,12 @@ async function goSettings() { + 热榜 + 发布 + 账号 + 私信 + 设置 +
@@ -87,10 +88,18 @@ async function goSettings() {
-
- + 发布视频 -
- +
+ + 发布视频 +
+ + +
+ .dy-top-right { + display: none; + } + .dy-mobile-nav { + height: 48px; + display: grid; + grid-auto-flow: column; + grid-auto-columns: 1fr; + border-bottom: 1px solid var(--border); + background: rgba(12, 16, 23, 0.74); + backdrop-filter: blur(18px); + } + .dy-mobile-link { + display: grid; + place-items: center; + color: var(--muted); + font-size: 13px; + text-decoration: none; + border-bottom: 2px solid transparent; + } + .dy-mobile-link.router-link-active { + color: var(--text); + border-bottom-color: var(--primary); + } + .dy-search { + max-width: none; + } +} + diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 20905d5..2cf2347 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -6,10 +6,11 @@ import VideoView from '../views/VideoView.vue' import VideoDetailView from '../views/VideoDetailView.vue' import AccountView from '../views/AccountView.vue' import ChangePasswordView from '../views/ChangePasswordView.vue' -import RegisterView from '../views/RegisterView.vue' -import SettingsView from '../views/SettingsView.vue' -import UserProfileView from '../views/UserProfileView.vue' -import { useAuthStore } from '../stores/auth' +import RegisterView from '../views/RegisterView.vue' +import SettingsView from '../views/SettingsView.vue' +import UserProfileView from '../views/UserProfileView.vue' +import MessageView from '../views/MessageView.vue' +import { useAuthStore } from '../stores/auth' const router = createRouter({ history: createWebHistory(), @@ -22,10 +23,12 @@ const router = createRouter({ { path: '/account', name: 'account', component: AccountView }, { path: '/account/register', name: 'account-register', component: RegisterView }, { path: '/account/change-password', name: 'account-change-password', component: ChangePasswordView }, - { path: '/settings', name: 'settings', component: SettingsView, meta: { requiresAuth: true } }, - { path: '/u/:id', name: 'user-profile', component: UserProfileView, props: true }, - ], -}) + { path: '/settings', name: 'settings', component: SettingsView, meta: { requiresAuth: true } }, + { path: '/u/:id', name: 'user-profile', component: UserProfileView, props: true }, + { path: '/messages', name: 'message-list', component: MessageView, meta: { requiresAuth: true } }, + { path: '/messages/:peerId', name: 'messages', component: MessageView, meta: { requiresAuth: true } }, + ], +}) router.beforeEach((to, _from, next) => { const auth = useAuthStore() diff --git a/frontend/src/style.css b/frontend/src/style.css index 3fa9f7a..f7f9a55 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1,17 +1,19 @@ -:root { - color-scheme: dark; - --bg: #0b0b0f; - --panel: rgba(255, 255, 255, 0.06); - --panel-2: rgba(255, 255, 255, 0.1); - --text: rgba(255, 255, 255, 0.92); - --muted: rgba(255, 255, 255, 0.64); - --border: rgba(255, 255, 255, 0.12); - --primary: #fe2c55; - --primary-2: #25f4ee; - --danger: #fe2c55; - --ok: #22c55e; - font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, 'Apple Color Emoji', - 'Segoe UI Emoji'; +:root { + color-scheme: dark; + --bg: #0e1117; + --bg-soft: #141922; + --panel: rgba(22, 27, 37, 0.82); + --panel-2: rgba(35, 42, 55, 0.78); + --text: rgba(246, 248, 251, 0.94); + --muted: rgba(205, 214, 226, 0.68); + --border: rgba(155, 171, 195, 0.18); + --primary: #2ba1ff; + --primary-2: #22c55e; + --danger: #ff5d73; + --ok: #22c55e; + --shadow: 0 18px 48px rgba(0, 0, 0, 0.28); + font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, 'Apple Color Emoji', + 'Segoe UI Emoji'; line-height: 1.5; font-synthesis: none; text-rendering: optimizeLegibility; @@ -25,55 +27,65 @@ box-sizing: border-box; } -/* Hide scrollbars globally (still scrollable). */ -* { - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE/Edge legacy */ -} - -*::-webkit-scrollbar { - width: 0; - height: 0; -} - -html, -body { - height: 100%; -} - -body { - margin: 0; - background: radial-gradient(1200px 900px at 20% -25%, rgba(254, 44, 85, 0.18), transparent 60%), - radial-gradient(900px 700px at 90% 10%, rgba(37, 244, 238, 0.12), transparent 55%), var(--bg); - color: var(--text); -} +* { + scrollbar-width: thin; + scrollbar-color: rgba(155, 171, 195, 0.35) transparent; +} + +*::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +*::-webkit-scrollbar-thumb { + background: rgba(155, 171, 195, 0.28); + border-radius: 999px; +} + +*::-webkit-scrollbar-track { + background: transparent; +} + +html, +body { + height: 100%; +} + +body { + margin: 0; + background: + radial-gradient(1000px 720px at 10% -20%, rgba(43, 161, 255, 0.14), transparent 58%), + linear-gradient(180deg, #10141d 0%, var(--bg) 42%, #0b0e14 100%); + color: var(--text); +} a { color: inherit; text-decoration: none; } -a:hover { - text-decoration: underline; -} - -.container { - max-width: 1100px; - margin: 0 auto; - padding: 24px 16px 56px; -} - -.card { - background: var(--panel); - border: 1px solid var(--border); - border-radius: 16px; - padding: 16px; - backdrop-filter: blur(10px); -} - -.card + .card { - margin-top: 14px; -} +a:hover { + color: #dceeff; +} + +.container { + max-width: 1180px; + margin: 0 auto; + padding: 22px 18px 56px; +} + +.card { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 8px; + padding: 18px; + backdrop-filter: blur(14px); + box-shadow: var(--shadow); +} + +.card + .card { + margin-top: 16px; +} .grid { display: grid; @@ -106,11 +118,12 @@ a:hover { color: var(--muted); } -.title { - font-size: 18px; - font-weight: 700; - margin: 0 0 6px; -} +.title { + font-size: 18px; + font-weight: 800; + margin: 0 0 6px; + letter-spacing: 0; +} .subtle { font-size: 13px; @@ -124,53 +137,56 @@ label { margin-bottom: 6px; } -input, -textarea, -select { - width: 100%; - background: rgba(255, 255, 255, 0.06); - border: 1px solid var(--border); - border-radius: 12px; - color: var(--text); - padding: 10px 12px; - outline: none; -} +input, +textarea, +select { + width: 100%; + background: rgba(255, 255, 255, 0.055); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + padding: 11px 12px; + outline: none; + transition: border-color 140ms ease, box-shadow 140ms ease, background 140ms ease; +} textarea { min-height: 88px; resize: vertical; } -input:focus, -textarea:focus, -select:focus { - border-color: rgba(124, 92, 255, 0.8); - box-shadow: 0 0 0 3px rgba(124, 92, 255, 0.22); -} - -button { - appearance: none; - border: 1px solid var(--border); - background: rgba(255, 255, 255, 0.08); - color: var(--text); - border-radius: 12px; - padding: 10px 14px; - cursor: pointer; - transition: 120ms ease; -} +input:focus, +textarea:focus, +select:focus { + border-color: rgba(43, 161, 255, 0.72); + box-shadow: 0 0 0 3px rgba(43, 161, 255, 0.18); + background: rgba(255, 255, 255, 0.075); +} + +button { + appearance: none; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.08); + color: var(--text); + border-radius: 8px; + padding: 10px 14px; + cursor: pointer; + transition: 120ms ease; + font: inherit; +} button:hover { background: rgba(255, 255, 255, 0.12); } -button.primary { - border-color: rgba(254, 44, 85, 0.55); - background: rgba(254, 44, 85, 0.18); -} - -button.primary:hover { - background: rgba(254, 44, 85, 0.26); -} +button.primary { + border-color: rgba(43, 161, 255, 0.55); + background: rgba(43, 161, 255, 0.18); +} + +button.primary:hover { + background: rgba(43, 161, 255, 0.26); +} button.danger { border-color: rgba(255, 77, 109, 0.65); @@ -186,12 +202,12 @@ button:disabled { cursor: not-allowed; } -.pill { +.pill { display: inline-flex; align-items: center; gap: 8px; - padding: 6px 10px; - border-radius: 999px; + padding: 6px 10px; + border-radius: 8px; border: 1px solid var(--border); background: rgba(255, 255, 255, 0.06); font-size: 13px; @@ -211,14 +227,28 @@ button:disabled { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; } -.pre { +.pre { white-space: pre-wrap; word-break: break-word; font-size: 13px; line-height: 1.45; background: rgba(0, 0, 0, 0.25); border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 12px; - padding: 12px; - margin: 0; -} + border-radius: 8px; + padding: 12px; + margin: 0; +} + +@media (max-width: 760px) { + .container { + padding: 14px 12px 76px; + } + + .card { + padding: 14px; + } + + .row { + gap: 8px; + } +} diff --git a/frontend/src/views/AccountView.vue b/frontend/src/views/AccountView.vue index 177ce25..cc24c85 100644 --- a/frontend/src/views/AccountView.vue +++ b/frontend/src/views/AccountView.vue @@ -349,23 +349,23 @@ watch( width: min(420px, 100%); } -.ghost { - border: 1px solid rgba(255, 255, 255, 0.14); - background: rgba(0, 0, 0, 0.18); - color: rgba(255, 255, 255, 0.86); - border-radius: 12px; - padding: 10px 12px; - cursor: pointer; -} +.ghost { + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.055); + color: var(--text); + border-radius: 8px; + padding: 10px 12px; + cursor: pointer; +} .ghost:hover { background: rgba(255, 255, 255, 0.1); } -.metric { - border: 1px solid rgba(255, 255, 255, 0.12); - background: rgba(255, 255, 255, 0.06); - border-radius: 16px; +.metric { + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.06); + border-radius: 8px; padding: 12px 14px; min-width: 120px; cursor: pointer; @@ -378,10 +378,10 @@ watch( background: rgba(255, 255, 255, 0.1); } -.metric.active { - background: rgba(254, 44, 85, 0.14); - border-color: rgba(254, 44, 85, 0.55); -} +.metric.active { + background: rgba(43, 161, 255, 0.14); + border-color: rgba(43, 161, 255, 0.55); +} .metric.static { cursor: default; @@ -398,18 +398,18 @@ watch( letter-spacing: 0.2px; } -.metric-label { - font-size: 12px; - color: rgba(255, 255, 255, 0.65); -} - -.hint { - color: rgba(255, 255, 255, 0.78); -} - -.hint.bad { - color: rgba(254, 44, 85, 0.92); -} +.metric-label { + font-size: 12px; + color: var(--muted); +} + +.hint { + color: var(--muted); +} + +.hint.bad { + color: var(--danger); +} .drawer-backdrop { position: fixed; @@ -423,12 +423,12 @@ watch( padding: 16px; } -.drawer { +.drawer { width: min(520px, calc(100vw - 18px)); max-height: min(78vh, 720px); - background: rgba(0, 0, 0, 0.65); - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 18px; + background: rgba(13, 18, 29, 0.92); + border: 1px solid var(--border); + border-radius: 8px; overflow: hidden; display: grid; grid-template-rows: auto 1fr; @@ -439,7 +439,7 @@ watch( align-items: center; justify-content: space-between; padding: 14px 14px; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); + border-bottom: 1px solid var(--border); } .drawer-title { @@ -449,8 +449,8 @@ watch( .drawer-x { width: 34px; height: 34px; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 8px; + border: 1px solid var(--border); background: rgba(255, 255, 255, 0.06); color: rgba(255, 255, 255, 0.9); cursor: pointer; @@ -465,14 +465,14 @@ watch( gap: 10px; } -.drawer-hint { - color: rgba(255, 255, 255, 0.78); +.drawer-hint { + color: var(--muted); padding: 12px 0; } -.drawer-hint.bad { - color: rgba(254, 44, 85, 0.92); -} +.drawer-hint.bad { + color: var(--danger); +} .video-grid { display: grid; @@ -492,10 +492,10 @@ watch( } } -.video-card { - border: 1px solid rgba(255, 255, 255, 0.12); - background: rgba(255, 255, 255, 0.05); - border-radius: 16px; +.video-card { + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; overflow: hidden; cursor: pointer; padding: 0; @@ -539,8 +539,8 @@ watch( gap: 12px; align-items: center; padding: 10px 10px; - border-radius: 14px; - border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + border: 1px solid var(--border); background: rgba(255, 255, 255, 0.05); cursor: pointer; } @@ -557,10 +557,10 @@ watch( font-weight: 800; } -.user-id { - font-size: 12px; - color: rgba(255, 255, 255, 0.6); -} +.user-id { + font-size: 12px; + color: var(--muted); +} .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; diff --git a/frontend/src/views/MessageView.vue b/frontend/src/views/MessageView.vue new file mode 100644 index 0000000..6f44b26 --- /dev/null +++ b/frontend/src/views/MessageView.vue @@ -0,0 +1,503 @@ + + +