Skip to content

Commit 55dc455

Browse files
committed
feat(sns): 前端新增朋友圈页面并接入候选匹配
- 新增 /sns 页面:时间线列表、账号切换、隐私模式、复制动态 JSON - 图片预览支持候选匹配切换并保存(localStorage + /api/sns/media_picks) - 聊天页侧边栏增加头像/朋友圈入口,隐私模式开关持久化(chat/sns 共用) - app.vue 增加 --dpr 与 sidebar rail CSS 变量,并在 resize 时刷新 - useApi 补充 sns 相关接口封装
1 parent ba9eb5e commit 55dc455

4 files changed

Lines changed: 1262 additions & 24 deletions

File tree

frontend/app.vue

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,23 @@
1414
// So we detect desktop onMounted and update reactively.
1515
const isDesktop = ref(false)
1616
17+
const updateDprVar = () => {
18+
const dpr = window.devicePixelRatio || 1
19+
document.documentElement.style.setProperty('--dpr', String(dpr))
20+
}
21+
1722
onMounted(() => {
1823
isDesktop.value = !!window?.wechatDesktop
24+
updateDprVar()
25+
window.addEventListener('resize', updateDprVar)
26+
})
27+
28+
onBeforeUnmount(() => {
29+
window.removeEventListener('resize', updateDprVar)
1930
})
2031
2132
const route = useRoute()
22-
const isChatRoute = computed(() => route.path?.startsWith('/chat'))
33+
const isChatRoute = computed(() => route.path?.startsWith('/chat') || route.path?.startsWith('/sns'))
2334
2435
const rootClass = computed(() => {
2536
const base = 'bg-gradient-to-br from-green-50 via-emerald-50 to-green-100'
@@ -34,6 +45,14 @@ const contentClass = computed(() =>
3445
</script>
3546
3647
<style>
48+
:root {
49+
--dpr: 1;
50+
/* Left sidebar rail (chat/sns): icon size + spacing */
51+
--sidebar-rail-step: 48px;
52+
--sidebar-rail-btn: 32px;
53+
--sidebar-rail-icon: 24px;
54+
}
55+
3756
/* Electron 桌面端使用自绘标题栏(frame: false)。
3857
* 页面里如果继续用 Tailwind 的 h-screen/min-h-screen(100vh),会把标题栏高度叠加进去,从而出现外层滚动条。
3958
* 这里把 “screen” 在桌面端视为内容区高度(100%),让标题栏高度自然内嵌在布局里。 */

frontend/composables/useApi.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,46 @@ export const useApi = () => {
179179
return await request(url)
180180
}
181181

182+
// 朋友圈时间线
183+
const listSnsTimeline = async (params = {}) => {
184+
const query = new URLSearchParams()
185+
if (params && params.account) query.set('account', params.account)
186+
if (params && params.limit != null) query.set('limit', String(params.limit))
187+
if (params && params.offset != null) query.set('offset', String(params.offset))
188+
if (params && params.usernames && Array.isArray(params.usernames) && params.usernames.length > 0) {
189+
query.set('usernames', params.usernames.join(','))
190+
} else if (params && params.usernames && typeof params.usernames === 'string') {
191+
query.set('usernames', params.usernames)
192+
}
193+
if (params && params.keyword) query.set('keyword', params.keyword)
194+
const url = '/sns/timeline' + (query.toString() ? `?${query.toString()}` : '')
195+
return await request(url)
196+
}
197+
198+
// 朋友圈图片本地缓存候选(用于错图时手动选择)
199+
const listSnsMediaCandidates = async (params = {}) => {
200+
const query = new URLSearchParams()
201+
if (params && params.account) query.set('account', params.account)
202+
if (params && params.create_time != null) query.set('create_time', String(params.create_time))
203+
if (params && params.width != null) query.set('width', String(params.width))
204+
if (params && params.height != null) query.set('height', String(params.height))
205+
if (params && params.limit != null) query.set('limit', String(params.limit))
206+
if (params && params.offset != null) query.set('offset', String(params.offset))
207+
const url = '/sns/media_candidates' + (query.toString() ? `?${query.toString()}` : '')
208+
return await request(url)
209+
}
210+
211+
// 保存朋友圈图片手动匹配结果(本机)
212+
const saveSnsMediaPicks = async (data = {}) => {
213+
return await request('/sns/media_picks', {
214+
method: 'POST',
215+
body: {
216+
account: data.account || null,
217+
picks: (data && data.picks && typeof data.picks === 'object' && !Array.isArray(data.picks)) ? data.picks : {}
218+
}
219+
})
220+
}
221+
182222
const openChatMediaFolder = async (params = {}) => {
183223
const query = new URLSearchParams()
184224
if (params && params.account) query.set('account', params.account)
@@ -288,6 +328,9 @@ export const useApi = () => {
288328
buildChatSearchIndex,
289329
listChatSearchSenders,
290330
getChatMessagesAround,
331+
listSnsTimeline,
332+
listSnsMediaCandidates,
333+
saveSnsMediaPicks,
291334
openChatMediaFolder,
292335
downloadChatEmoji,
293336
saveMediaKeys,

frontend/pages/chat/[[username]].vue

Lines changed: 107 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,99 @@
11
<template>
22
<div class="h-screen flex overflow-hidden" style="background-color: #EDEDED">
33
<!-- 左侧边栏 -->
4-
<div class="w-16 border-r border-gray-200 flex flex-col" style="background-color: #e8e7e7">
5-
<div class="flex-1 flex flex-col justify-start pt-0">
4+
<div class="border-r border-gray-200 flex flex-col" style="background-color: #e8e7e7; width: 60px; min-width: 60px; max-width: 60px">
5+
<div class="flex-1 flex flex-col justify-start pt-0 gap-0">
6+
<!-- 头像(类似微信侧边栏) -->
7+
<div class="w-full h-[60px] flex items-center justify-center">
8+
<div class="w-[40px] h-[40px] rounded-md overflow-hidden bg-gray-300 flex-shrink-0">
9+
<img v-if="selfAvatarUrl" :src="selfAvatarUrl" alt="avatar" class="w-full h-full object-cover" />
10+
<div
11+
v-else
12+
class="w-full h-full flex items-center justify-center text-white text-xs font-bold"
13+
style="background-color: #4B5563"
14+
>
15+
16+
</div>
17+
</div>
18+
</div>
19+
620
<!-- 聊天图标 (与 oh-my-wechat 一致) -->
7-
<div class="w-16 h-16 flex items-center justify-center chat-tab selected text-[#07b75b]">
8-
<div class="w-7 h-7">
9-
<svg class="w-full h-full" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
10-
<path d="M12 19.8C17.52 19.8 22 15.99 22 11.3C22 6.6 17.52 2.8 12 2.8C6.48 2.8 2 6.6 2 11.3C2 13.29 2.8 15.12 4.15 16.57C4.6 17.05 4.82 17.29 4.92 17.44C5.14 17.79 5.21 17.99 5.23 18.4C5.24 18.59 5.22 18.81 5.16 19.26C5.1 19.75 5.07 19.99 5.13 20.16C5.23 20.49 5.53 20.71 5.87 20.72C6.04 20.72 6.27 20.63 6.72 20.43L8.07 19.86C8.43 19.71 8.61 19.63 8.77 19.59C8.95 19.55 9.04 19.54 9.22 19.54C9.39 19.53 9.64 19.57 10.14 19.65C10.74 19.75 11.37 19.8 12 19.8Z"/>
11-
</svg>
21+
<div class="w-full h-[var(--sidebar-rail-step)] flex items-center justify-center group">
22+
<div class="w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md bg-transparent group-hover:bg-[#E1E1E1] flex items-center justify-center transition-colors">
23+
<div class="w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)] text-[#07b75b]">
24+
<svg class="w-full h-full" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
25+
<path d="M12 19.8C17.52 19.8 22 15.99 22 11.3C22 6.6 17.52 2.8 12 2.8C6.48 2.8 2 6.6 2 11.3C2 13.29 2.8 15.12 4.15 16.57C4.6 17.05 4.82 17.29 4.92 17.44C5.14 17.79 5.21 17.99 5.23 18.4C5.24 18.59 5.22 18.81 5.16 19.26C5.1 19.75 5.07 19.99 5.13 20.16C5.23 20.49 5.53 20.71 5.87 20.72C6.04 20.72 6.27 20.63 6.72 20.43L8.07 19.86C8.43 19.71 8.61 19.63 8.77 19.59C8.95 19.55 9.04 19.54 9.22 19.54C9.39 19.53 9.64 19.57 10.14 19.65C10.74 19.75 11.37 19.8 12 19.8Z" />
26+
</svg>
27+
</div>
28+
</div>
29+
</div>
30+
31+
<!-- 朋友圈图标(Aperture 风格) -->
32+
<div
33+
class="w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
34+
title="朋友圈"
35+
@click="goSns"
36+
>
37+
<div
38+
class="w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent group-hover:bg-[#E1E1E1]"
39+
>
40+
<div class="w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="isSnsRoute ? 'text-[#07b75b]' : 'text-[#5d5d5d]'">
41+
<svg
42+
class="w-full h-full"
43+
viewBox="0 0 24 24"
44+
fill="none"
45+
stroke="currentColor"
46+
stroke-width="1.5"
47+
stroke-linecap="round"
48+
stroke-linejoin="round"
49+
aria-hidden="true"
50+
>
51+
<circle cx="12" cy="12" r="10" />
52+
<line x1="14.31" y1="8" x2="20.05" y2="17.94" />
53+
<line x1="9.69" y1="8" x2="21.17" y2="8" />
54+
<line x1="7.38" y1="12" x2="13.12" y2="2.06" />
55+
<line x1="9.69" y1="16" x2="3.95" y2="6.06" />
56+
<line x1="14.31" y1="16" x2="2.83" y2="16" />
57+
<line x1="16.62" y1="12" x2="10.88" y2="21.94" />
58+
</svg>
59+
</div>
1260
</div>
1361
</div>
1462

1563
<!-- 隐私模式按钮 -->
1664
<div
17-
class="w-16 h-12 flex items-center justify-center cursor-pointer transition-colors"
18-
:class="privacyMode ? 'text-[#03C160]' : 'text-gray-500 hover:text-gray-700'"
65+
class="w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
1966
@click="privacyMode = !privacyMode"
2067
:title="privacyMode ? '关闭隐私模式' : '开启隐私模式'"
2168
>
22-
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
23-
<path v-if="privacyMode" stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
24-
<path v-else stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
25-
<circle v-if="!privacyMode" cx="12" cy="12" r="3" />
26-
</svg>
69+
<div
70+
class="w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent group-hover:bg-[#E1E1E1]"
71+
>
72+
<svg class="w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="privacyMode ? 'text-[#07b75b]' : 'text-[#5d5d5d]'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
73+
<path v-if="privacyMode" stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
74+
<path v-else stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
75+
<circle v-if="!privacyMode" cx="12" cy="12" r="3" />
76+
</svg>
77+
</div>
2778
</div>
2879

2980
<!-- 设置按钮(仅桌面端) -->
3081
<div
3182
v-if="isDesktopEnv"
32-
class="w-16 h-12 flex items-center justify-center cursor-pointer transition-colors text-gray-500 hover:text-gray-700"
83+
class="w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
3384
@click="openDesktopSettings"
3485
title="设置"
3586
>
36-
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
37-
<path
38-
stroke-linecap="round"
39-
stroke-linejoin="round"
40-
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
41-
/>
42-
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
43-
</svg>
87+
<div class="w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent group-hover:bg-[#E1E1E1]">
88+
<svg class="w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)] text-[#5d5d5d]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
89+
<path
90+
stroke-linecap="round"
91+
stroke-linejoin="round"
92+
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
93+
/>
94+
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
95+
</svg>
96+
</div>
4497
</div>
4598
</div>
4699
</div>
@@ -1714,6 +1767,7 @@ useHead({
17141767
})
17151768

17161769
const route = useRoute()
1770+
const isSnsRoute = computed(() => route.path?.startsWith('/sns'))
17171771

17181772
const routeUsername = computed(() => {
17191773
const raw = route.params.username
@@ -1729,6 +1783,24 @@ const selectedContact = ref(null)
17291783

17301784
// 隐私模式
17311785
const privacyMode = ref(false)
1786+
const PRIVACY_MODE_KEY = 'ui.privacy_mode'
1787+
1788+
onMounted(() => {
1789+
if (!process.client) return
1790+
try {
1791+
privacyMode.value = localStorage.getItem(PRIVACY_MODE_KEY) === '1'
1792+
} catch {}
1793+
})
1794+
1795+
watch(
1796+
() => privacyMode.value,
1797+
(v) => {
1798+
if (!process.client) return
1799+
try {
1800+
localStorage.setItem(PRIVACY_MODE_KEY, v ? '1' : '0')
1801+
} catch {}
1802+
}
1803+
)
17321804

17331805
// 桌面端设置(仅 Electron 环境可见)
17341806
const isDesktopEnv = ref(false)
@@ -1880,6 +1952,18 @@ const selectedAccount = ref(null)
18801952

18811953
const availableAccounts = ref([])
18821954

1955+
const sidebarMediaBase = process.client ? 'http://localhost:8000' : ''
1956+
1957+
const selfAvatarUrl = computed(() => {
1958+
const acc = String(selectedAccount.value || '').trim()
1959+
if (!acc) return ''
1960+
return `${sidebarMediaBase}/api/chat/avatar?account=${encodeURIComponent(acc)}&username=${encodeURIComponent(acc)}`
1961+
})
1962+
1963+
const goSns = async () => {
1964+
await navigateTo('/sns')
1965+
}
1966+
18831967
// 实时更新(WCDB DLL + db_storage watcher)
18841968
const realtimeEnabled = ref(false)
18851969
const realtimeAvailable = ref(false)

0 commit comments

Comments
 (0)