mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-06-18 15:54:08 +08:00
feat(ui): 新增浅深色主题切换并统一界面配色
- 新增主题 store 与本地持久化能力,支持侧边栏切换浅色/深色模式 - 将聊天页、会话列表、标题栏、弹窗等配色改为 CSS 变量统一管理 - 适配定位卡片、引用气泡、系统提示等聊天消息组件在不同主题下的可读性 - 同步整理首页、解密页、联系人页、朋友圈页等页面背景与交互样式
This commit is contained in:
+19
-1
@@ -30,12 +30,18 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useThemeStore } from '~/stores/theme'
|
||||
import { useChatAccountsStore } from '~/stores/chatAccounts'
|
||||
import { usePrivacyStore } from '~/stores/privacy'
|
||||
|
||||
const route = useRoute()
|
||||
const desktopUpdate = useDesktopUpdate()
|
||||
const { open: settingsDialogOpen, closeDialog: closeSettingsDialog } = useSettingsDialog()
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
if (process.client) {
|
||||
themeStore.init()
|
||||
}
|
||||
|
||||
// In Electron the server/pre-render doesn't know about `window.wechatDesktop`.
|
||||
// If we render different DOM on server vs client, Vue hydration will keep the
|
||||
@@ -71,6 +77,7 @@ onMounted(() => {
|
||||
const privacy = usePrivacyStore()
|
||||
void chatAccounts.ensureLoaded()
|
||||
privacy.init()
|
||||
themeStore.init()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -78,7 +85,7 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
const rootClass = computed(() => {
|
||||
const base = 'bg-gradient-to-br from-green-50 via-emerald-50 to-green-100'
|
||||
const base = 'theme-app-shell'
|
||||
return isDesktop.value
|
||||
? `wechat-desktop h-screen flex overflow-hidden ${base}`
|
||||
: `h-screen flex overflow-hidden ${base}`
|
||||
@@ -126,4 +133,15 @@ const showSidebar = computed(() => {
|
||||
.wechat-desktop .wechat-desktop-content > .min-h-screen {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.theme-app-shell {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(7, 193, 96, 0.08), transparent 32%),
|
||||
radial-gradient(circle at top right, rgba(16, 174, 239, 0.08), transparent 36%),
|
||||
linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 45%, #dcfce7 100%);
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .theme-app-shell {
|
||||
background: var(--app-shell-bg);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -33,17 +33,17 @@
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
background: var(--scrollbar-track);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||||
background: #a1a1a1;
|
||||
background: var(--scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
/* 会话列表宽度:按物理像素(px)配置,按 dpr 换算为 CSS px */
|
||||
@@ -75,7 +75,25 @@
|
||||
|
||||
.session-list-resizer:hover::after,
|
||||
.session-list-resizer-active::after {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
background: var(--session-list-resizer);
|
||||
}
|
||||
|
||||
.msg-bubble.bubble-tail-r {
|
||||
background-color: var(--chat-bubble-sent) !important;
|
||||
color: var(--chat-bubble-sent-text) !important;
|
||||
}
|
||||
|
||||
.msg-bubble.bubble-tail-l {
|
||||
background-color: var(--chat-bubble-received) !important;
|
||||
color: var(--chat-bubble-received-text) !important;
|
||||
}
|
||||
|
||||
.bubble-tail-r::after {
|
||||
background: var(--chat-bubble-sent);
|
||||
}
|
||||
|
||||
.bubble-tail-l::after {
|
||||
background: var(--chat-bubble-received);
|
||||
}
|
||||
|
||||
/* 消息气泡样式 */
|
||||
@@ -87,7 +105,7 @@
|
||||
|
||||
/* 发送的消息(右侧绿色气泡) */
|
||||
.sent-message {
|
||||
background-color: #95EB69 !important;
|
||||
background-color: var(--chat-bubble-sent) !important;
|
||||
border-radius: var(--message-radius);
|
||||
}
|
||||
|
||||
@@ -99,13 +117,13 @@
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #95EB69;
|
||||
background-color: var(--chat-bubble-sent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 接收的消息(左侧白色气泡) */
|
||||
.received-message {
|
||||
background-color: white !important;
|
||||
background-color: var(--chat-bubble-received) !important;
|
||||
border-radius: var(--message-radius);
|
||||
}
|
||||
|
||||
@@ -117,7 +135,7 @@
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: white;
|
||||
background-color: var(--chat-bubble-received);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@@ -172,7 +190,7 @@
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #95EC69;
|
||||
background-color: var(--chat-bubble-sent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@@ -188,7 +206,7 @@
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: white;
|
||||
background-color: var(--chat-bubble-received);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@@ -216,7 +234,8 @@
|
||||
}
|
||||
|
||||
.wechat-voice-sent {
|
||||
background: #95EC69;
|
||||
background: var(--chat-bubble-sent);
|
||||
color: var(--chat-bubble-sent-text);
|
||||
}
|
||||
|
||||
.wechat-voice-sent::after {
|
||||
@@ -227,12 +246,13 @@
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #95EC69;
|
||||
background: var(--chat-bubble-sent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.wechat-voice-received {
|
||||
background: white;
|
||||
background: var(--chat-bubble-received);
|
||||
color: var(--chat-bubble-received-text);
|
||||
}
|
||||
|
||||
.wechat-voice-received::before {
|
||||
@@ -243,7 +263,7 @@
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: white;
|
||||
background: var(--chat-bubble-received);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@@ -259,7 +279,7 @@
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
color: #1a1a1a;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.wechat-quote-voice-icon {
|
||||
@@ -293,7 +313,7 @@
|
||||
|
||||
.wechat-voice-duration {
|
||||
font-size: 14px;
|
||||
color: #1a1a1a;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.wechat-voice-unread {
|
||||
@@ -315,7 +335,8 @@
|
||||
}
|
||||
|
||||
.wechat-voip-sent {
|
||||
background: #95EC69;
|
||||
background: var(--chat-bubble-sent);
|
||||
color: var(--chat-bubble-sent-text);
|
||||
}
|
||||
|
||||
.wechat-voip-sent::after {
|
||||
@@ -326,12 +347,13 @@
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #95EC69;
|
||||
background: var(--chat-bubble-sent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.wechat-voip-received {
|
||||
background: white;
|
||||
background: var(--chat-bubble-received);
|
||||
color: var(--chat-bubble-received-text);
|
||||
}
|
||||
|
||||
.wechat-voip-received::before {
|
||||
@@ -342,7 +364,7 @@
|
||||
transform: translateY(-50%) rotate(45deg);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: white;
|
||||
background: var(--chat-bubble-received);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@@ -362,7 +384,7 @@
|
||||
|
||||
.wechat-voip-text {
|
||||
font-size: 14px;
|
||||
color: #1a1a1a;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* 统一特殊消息尾巴(红包 / 文件等) */
|
||||
@@ -390,14 +412,14 @@
|
||||
|
||||
.wechat-chat-history-card {
|
||||
width: 210px;
|
||||
background: #ffffff;
|
||||
background: var(--merged-history-bg);
|
||||
border-radius: var(--message-radius);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.wechat-chat-history-card:hover {
|
||||
background: #f5f5f5;
|
||||
background: var(--merged-history-hover);
|
||||
}
|
||||
|
||||
.wechat-chat-history-body {
|
||||
@@ -407,13 +429,13 @@
|
||||
.wechat-chat-history-title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #161616;
|
||||
color: var(--merged-history-title);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.wechat-chat-history-preview {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
color: var(--merged-history-preview);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@@ -439,12 +461,17 @@
|
||||
left: 13px;
|
||||
right: 13px;
|
||||
height: 1.5px;
|
||||
background: #e8e8e8;
|
||||
background: var(--merged-history-divider);
|
||||
}
|
||||
|
||||
.wechat-chat-history-bottom span {
|
||||
font-size: 12px;
|
||||
color: #b2b2b2;
|
||||
color: var(--merged-history-footer);
|
||||
}
|
||||
|
||||
.wechat-quote-preview {
|
||||
background: var(--quote-bubble-bg);
|
||||
color: var(--quote-bubble-text);
|
||||
}
|
||||
|
||||
/* 转账消息样式 - 微信风格 */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div v-if="appStore.apiStatus !== 'connected'"
|
||||
class="fixed top-4 right-4 bg-red-50 border border-red-200 rounded-lg p-4 shadow-lg max-w-sm z-50">
|
||||
<div v-if="appStore.apiStatus !== 'connected'"
|
||||
class="api-status-banner fixed top-4 right-4 bg-red-50 border border-red-200 rounded-lg p-4 shadow-lg max-w-sm z-50"
|
||||
>
|
||||
<div class="flex items-start">
|
||||
<svg class="h-5 w-5 text-red-600 mr-2 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
|
||||
@@ -136,10 +136,19 @@ const openLocation = () => {
|
||||
|
||||
<style scoped>
|
||||
.wechat-location-card-wrap {
|
||||
--location-card-bg: var(--chat-bubble-received);
|
||||
--location-card-text: var(--chat-bubble-received-text);
|
||||
--location-card-muted: var(--chat-sender-name);
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.wechat-location-card-wrap--sent {
|
||||
--location-card-bg: var(--chat-bubble-sent);
|
||||
--location-card-text: var(--chat-bubble-sent-text);
|
||||
--location-card-muted: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.wechat-location-card-wrap--received::before,
|
||||
.wechat-location-card-wrap--sent::after {
|
||||
content: '';
|
||||
@@ -147,7 +156,7 @@ const openLocation = () => {
|
||||
top: 12px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #fff;
|
||||
background: var(--location-card-bg);
|
||||
transform: rotate(45deg);
|
||||
border-radius: 2px;
|
||||
}
|
||||
@@ -165,27 +174,27 @@ const openLocation = () => {
|
||||
overflow: hidden;
|
||||
border-radius: var(--message-radius);
|
||||
border: none;
|
||||
background: #fff;
|
||||
background: var(--location-card-bg);
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.wechat-location-card--sent {
|
||||
background: #fff;
|
||||
background: var(--location-card-bg);
|
||||
}
|
||||
|
||||
.wechat-location-card__text {
|
||||
padding: 10px 12px 8px;
|
||||
background: #fff;
|
||||
background: var(--location-card-bg);
|
||||
}
|
||||
|
||||
.wechat-location-card--sent .wechat-location-card__text {
|
||||
background: #fff;
|
||||
background: var(--location-card-bg);
|
||||
}
|
||||
|
||||
.wechat-location-card__title {
|
||||
color: #111827;
|
||||
color: var(--location-card-text);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
@@ -197,7 +206,7 @@ const openLocation = () => {
|
||||
|
||||
.wechat-location-card__subtitle {
|
||||
margin-top: 4px;
|
||||
color: #9ca3af;
|
||||
color: var(--location-card-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
@@ -206,7 +215,7 @@ const openLocation = () => {
|
||||
}
|
||||
|
||||
.wechat-location-card--sent .wechat-location-card__subtitle {
|
||||
color: #9ca3af;
|
||||
color: var(--location-card-muted);
|
||||
}
|
||||
|
||||
.wechat-location-card__map {
|
||||
|
||||
@@ -60,7 +60,7 @@ const closeWindow = () => {
|
||||
<style scoped>
|
||||
.desktop-titlebar {
|
||||
height: var(--desktop-titlebar-height, 32px);
|
||||
background: #ededed;
|
||||
background: var(--desktop-titlebar-bg);
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-shrink: 0;
|
||||
@@ -92,11 +92,11 @@ const closeWindow = () => {
|
||||
}
|
||||
|
||||
.desktop-titlebar-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
background: var(--desktop-titlebar-hover);
|
||||
}
|
||||
|
||||
.desktop-titlebar-btn:active {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
background: var(--desktop-titlebar-active);
|
||||
}
|
||||
|
||||
.desktop-titlebar-btn-close:hover {
|
||||
@@ -122,7 +122,7 @@ const closeWindow = () => {
|
||||
/* Optical centering: the glyph was anchored to the bottom, so it looked low. */
|
||||
top: 5px;
|
||||
height: 1px;
|
||||
background: #111;
|
||||
background: var(--desktop-titlebar-icon);
|
||||
}
|
||||
|
||||
.desktop-titlebar-icon-maximize::before {
|
||||
@@ -132,7 +132,7 @@ const closeWindow = () => {
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
border: 1px solid #111;
|
||||
border: 1px solid var(--desktop-titlebar-icon);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ const closeWindow = () => {
|
||||
right: 1px;
|
||||
top: 50%;
|
||||
height: 1px;
|
||||
background: #111;
|
||||
background: var(--desktop-titlebar-icon);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div v-if="open && info" class="fixed inset-0 z-[9999] flex items-center justify-center">
|
||||
<div class="absolute inset-0 bg-black/40" @click="onBackdropClick" />
|
||||
|
||||
<div class="relative w-[min(520px,calc(100vw-32px))] rounded-lg bg-white shadow-xl border border-gray-200">
|
||||
<div class="desktop-update-dialog-panel relative w-[min(520px,calc(100vw-32px))] rounded-lg bg-white shadow-xl border border-gray-200">
|
||||
<button
|
||||
class="absolute right-3 top-3 h-8 w-8 rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-700"
|
||||
type="button"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="open"
|
||||
class="fixed inset-0 z-[120] flex items-center justify-center bg-black/40 px-4 py-4 backdrop-blur-md sm:py-8"
|
||||
class="settings-dialog fixed inset-0 z-[120] flex items-center justify-center bg-black/40 px-4 py-4 backdrop-blur-md sm:py-8"
|
||||
@click.self="handleClose"
|
||||
>
|
||||
<div class="flex h-[80vh] min-h-[380px] w-full max-w-[760px] overflow-hidden rounded-[10px] border border-[#e2e2e2] bg-white shadow-2xl">
|
||||
<div class="settings-dialog-panel flex h-[80vh] min-h-[380px] w-full max-w-[760px] overflow-hidden rounded-[10px] border border-[#e2e2e2] bg-white shadow-2xl">
|
||||
<!-- Sidebar -->
|
||||
<aside class="flex w-[180px] shrink-0 flex-col bg-[#fcfcfc] border-r border-[#eeeeee]">
|
||||
<div class="mt-4 mb-2 flex items-center px-4 gap-2">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="border-r border-gray-200 flex flex-col"
|
||||
:style="{ backgroundColor: '#e8e7e7', width: '60px', minWidth: '60px', maxWidth: '60px' }"
|
||||
class="sidebar-rail border-r flex flex-col"
|
||||
>
|
||||
<div class="flex-1 flex flex-col justify-start pt-0 gap-0">
|
||||
<!-- Avatar -->
|
||||
@@ -25,12 +24,12 @@
|
||||
|
||||
<!-- Chat -->
|
||||
<div
|
||||
class="w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
class="sidebar-rail-action w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
title="聊天"
|
||||
@click="goChat"
|
||||
>
|
||||
<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]">
|
||||
<div class="w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="isChatRoute ? 'text-[#07b75b]' : 'text-[#5d5d5d]'">
|
||||
<div class="sidebar-rail-plate w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent">
|
||||
<div class="sidebar-rail-icon w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="{ 'sidebar-rail-icon-active': isChatRoute }">
|
||||
<svg class="w-full h-full" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<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" />
|
||||
</svg>
|
||||
@@ -40,12 +39,12 @@
|
||||
|
||||
<!-- Edits -->
|
||||
<div
|
||||
class="w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
class="sidebar-rail-action w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
title="修改记录"
|
||||
@click="goEdits"
|
||||
>
|
||||
<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]">
|
||||
<div class="w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="isEditsRoute ? 'text-[#07b75b]' : 'text-[#5d5d5d]'">
|
||||
<div class="sidebar-rail-plate w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent">
|
||||
<div class="sidebar-rail-icon w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="{ 'sidebar-rail-icon-active': isEditsRoute }">
|
||||
<svg class="w-full h-full" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 20h9" />
|
||||
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z" />
|
||||
@@ -56,12 +55,12 @@
|
||||
|
||||
<!-- Moments -->
|
||||
<div
|
||||
class="w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
class="sidebar-rail-action w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
title="朋友圈"
|
||||
@click="goSns"
|
||||
>
|
||||
<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]">
|
||||
<div class="w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="isSnsRoute ? 'text-[#07b75b]' : 'text-[#5d5d5d]'">
|
||||
<div class="sidebar-rail-plate w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent">
|
||||
<div class="sidebar-rail-icon w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="{ 'sidebar-rail-icon-active': isSnsRoute }">
|
||||
<svg
|
||||
class="w-full h-full"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -86,12 +85,12 @@
|
||||
|
||||
<!-- Contacts -->
|
||||
<div
|
||||
class="w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
class="sidebar-rail-action w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
title="联系人"
|
||||
@click="goContacts"
|
||||
>
|
||||
<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]">
|
||||
<div class="w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="isContactsRoute ? 'text-[#07b75b]' : 'text-[#5d5d5d]'">
|
||||
<div class="sidebar-rail-plate w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent">
|
||||
<div class="sidebar-rail-icon w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="{ 'sidebar-rail-icon-active': isContactsRoute }">
|
||||
<svg class="w-full h-full" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="10" cy="7" r="4" />
|
||||
@@ -104,12 +103,12 @@
|
||||
|
||||
<!-- Wrapped -->
|
||||
<div
|
||||
class="w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
class="sidebar-rail-action w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
title="年度总结"
|
||||
@click="goWrapped"
|
||||
>
|
||||
<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]">
|
||||
<div class="w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="isWrappedRoute ? 'text-[#07b75b]' : 'text-[#5d5d5d]'">
|
||||
<div class="sidebar-rail-plate w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent">
|
||||
<div class="sidebar-rail-icon w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="{ 'sidebar-rail-icon-active': isWrappedRoute }">
|
||||
<svg
|
||||
class="w-full h-full"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -132,15 +131,15 @@
|
||||
|
||||
<!-- Realtime -->
|
||||
<div
|
||||
class="w-full h-[var(--sidebar-rail-step)] flex items-center justify-center group"
|
||||
class="sidebar-rail-action w-full h-[var(--sidebar-rail-step)] flex items-center justify-center group"
|
||||
:class="realtimeBusy ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'"
|
||||
:title="realtimeTitle"
|
||||
@click="toggleRealtime"
|
||||
>
|
||||
<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]">
|
||||
<div class="sidebar-rail-plate w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent">
|
||||
<svg
|
||||
class="w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]"
|
||||
:class="realtimeEnabled ? 'text-[#07b75b]' : 'text-[#5d5d5d]'"
|
||||
class="sidebar-rail-icon w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]"
|
||||
:class="{ 'sidebar-rail-icon-active': realtimeEnabled }"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -156,12 +155,12 @@
|
||||
|
||||
<!-- Privacy -->
|
||||
<div
|
||||
class="w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
class="sidebar-rail-action w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
@click="privacyStore.toggle"
|
||||
:title="privacyMode ? '关闭隐私模式' : '开启隐私模式'"
|
||||
>
|
||||
<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]">
|
||||
<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">
|
||||
<div class="sidebar-rail-plate w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent">
|
||||
<svg class="sidebar-rail-icon w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="{ 'sidebar-rail-icon-active': privacyMode }" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<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" />
|
||||
<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" />
|
||||
<circle v-if="!privacyMode" cx="12" cy="12" r="3" />
|
||||
@@ -169,15 +168,52 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Theme -->
|
||||
<div
|
||||
class="sidebar-rail-action w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
:title="themeStore.isDark ? '切换浅色模式' : '切换深色模式'"
|
||||
@click="themeStore.toggle"
|
||||
>
|
||||
<div class="sidebar-rail-plate w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent">
|
||||
<svg
|
||||
v-if="themeStore.isDark"
|
||||
class="sidebar-rail-icon sidebar-rail-icon-active w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.6"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="4.5" />
|
||||
<path d="M12 2.5v2.2M12 19.3v2.2M4.93 4.93l1.56 1.56M17.51 17.51l1.56 1.56M2.5 12h2.2M19.3 12h2.2M4.93 19.07l1.56-1.56M17.51 6.49l1.56-1.56" />
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="sidebar-rail-icon w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3c-.08.5-.12 1.01-.12 1.54a8.25 8.25 0 0 0 8.37 8.25c.52 0 1.03-.04 1.54-.12Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto">
|
||||
<!-- Guide -->
|
||||
<div
|
||||
class="w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
class="sidebar-rail-action w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
title="引导页"
|
||||
@click="goGuide"
|
||||
>
|
||||
<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]">
|
||||
<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.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<div class="sidebar-rail-plate w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent">
|
||||
<svg class="sidebar-rail-icon w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M3 10.5L12 3l9 7.5" />
|
||||
<path d="M5 9.5V20h14V9.5" />
|
||||
<path d="M10 20v-6h4v6" />
|
||||
@@ -187,12 +223,12 @@
|
||||
|
||||
<!-- Settings -->
|
||||
<div
|
||||
class="w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
class="sidebar-rail-action w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
@click="goSettings"
|
||||
title="设置"
|
||||
>
|
||||
<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]">
|
||||
<svg class="w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="settingsDialogOpen ? 'text-[#07b75b]' : 'text-[#5d5d5d]'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<div class="sidebar-rail-plate w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent">
|
||||
<svg class="sidebar-rail-icon w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]" :class="{ 'sidebar-rail-icon-active': settingsDialogOpen }" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
@@ -208,10 +244,10 @@
|
||||
|
||||
<div
|
||||
v-if="accountDialogOpen"
|
||||
class="fixed inset-0 z-[130] flex items-center justify-center bg-black/35 px-4"
|
||||
class="account-info-dialog fixed inset-0 z-[130] flex items-center justify-center bg-black/35 px-4"
|
||||
@click.self="closeAccountDialog"
|
||||
>
|
||||
<div class="w-full max-w-[440px] overflow-hidden rounded-[12px] border border-[#e7e7e7] bg-white shadow-2xl">
|
||||
<div class="account-info-dialog-panel w-full max-w-[440px] overflow-hidden rounded-[12px] border border-[#e7e7e7] bg-white shadow-2xl">
|
||||
<div class="flex items-center justify-between border-b border-[#efefef] px-4 py-3">
|
||||
<div class="text-[14px] font-semibold text-[#222]">当前账号信息</div>
|
||||
<button
|
||||
@@ -289,6 +325,7 @@ import { storeToRefs } from 'pinia'
|
||||
import { useChatAccountsStore } from '~/stores/chatAccounts'
|
||||
import { useChatRealtimeStore } from '~/stores/chatRealtime'
|
||||
import { usePrivacyStore } from '~/stores/privacy'
|
||||
import { useThemeStore } from '~/stores/theme'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -298,6 +335,9 @@ const { selectedAccount } = storeToRefs(chatAccounts)
|
||||
const privacyStore = usePrivacyStore()
|
||||
const { privacyMode } = storeToRefs(privacyStore)
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
themeStore.init()
|
||||
|
||||
const realtimeStore = useChatRealtimeStore()
|
||||
const { enabled: realtimeEnabled, available: realtimeAvailable, checking: realtimeChecking, statusError: realtimeStatusError, toggling: realtimeToggling } = storeToRefs(realtimeStore)
|
||||
const { open: settingsDialogOpen, openDialog: openSettingsDialog } = useSettingsDialog()
|
||||
@@ -540,3 +580,30 @@ const toggleRealtime = async () => {
|
||||
await realtimeStore.toggle({ silent: false })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar-rail {
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
max-width: 60px;
|
||||
background-color: var(--sidebar-rail-bg);
|
||||
border-color: var(--sidebar-rail-border);
|
||||
}
|
||||
|
||||
.sidebar-rail-plate {
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.sidebar-rail-action:hover .sidebar-rail-plate {
|
||||
background-color: var(--sidebar-rail-hover);
|
||||
}
|
||||
|
||||
.sidebar-rail-icon {
|
||||
color: var(--sidebar-rail-icon-color);
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.sidebar-rail-icon-active {
|
||||
color: var(--sidebar-rail-icon-active-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -826,51 +826,51 @@
|
||||
<!-- 合并转发聊天记录弹窗 -->
|
||||
<div
|
||||
v-if="chatHistoryModalVisible"
|
||||
class="fixed inset-0 z-50 bg-black/40 flex items-center justify-center"
|
||||
class="chat-history-modal-overlay fixed inset-0 z-50 bg-black/40 flex items-center justify-center"
|
||||
@click="closeChatHistoryModal"
|
||||
>
|
||||
<div
|
||||
class="w-[92vw] max-w-[560px] max-h-[80vh] bg-[#f7f7f7] rounded-xl shadow-xl overflow-hidden flex flex-col"
|
||||
class="chat-history-modal-panel w-[92vw] max-w-[560px] max-h-[80vh] rounded-xl shadow-xl overflow-hidden flex flex-col"
|
||||
@click.stop
|
||||
>
|
||||
<div class="px-4 py-3 bg-[#f7f7f7] border-b border-gray-200 flex items-center justify-between">
|
||||
<div class="chat-history-modal-header px-4 py-3 border-b flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<button
|
||||
v-if="chatHistoryModalStack.length"
|
||||
type="button"
|
||||
class="p-2 rounded hover:bg-black/5 flex-shrink-0"
|
||||
class="chat-history-modal-icon-btn p-2 rounded flex-shrink-0"
|
||||
@click="goBackChatHistoryModal"
|
||||
aria-label="返回"
|
||||
title="返回"
|
||||
>
|
||||
<svg class="w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="chat-history-modal-icon w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="text-sm text-[#161616] truncate">{{ chatHistoryModalTitle || '聊天记录' }}</div>
|
||||
<div class="chat-history-modal-title text-sm truncate">{{ chatHistoryModalTitle || '聊天记录' }}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 rounded hover:bg-black/5"
|
||||
class="chat-history-modal-icon-btn p-2 rounded"
|
||||
@click="closeChatHistoryModal"
|
||||
aria-label="关闭"
|
||||
title="关闭"
|
||||
>
|
||||
<svg class="w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="chat-history-modal-icon w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto bg-[#f7f7f7]">
|
||||
<div v-if="!chatHistoryModalRecords.length" class="text-sm text-gray-500 text-center py-10">
|
||||
<div class="chat-history-modal-body flex-1 overflow-auto">
|
||||
<div v-if="!chatHistoryModalRecords.length" class="chat-history-modal-empty text-sm text-center py-10">
|
||||
没有可显示的聊天记录
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(rec, idx) in chatHistoryModalRecords"
|
||||
:key="rec.id || idx"
|
||||
class="px-4 py-3 flex gap-3 border-b border-gray-100 bg-[#f7f7f7]"
|
||||
class="chat-history-modal-row px-4 py-3 flex gap-3 border-b"
|
||||
>
|
||||
<div class="w-9 h-9 rounded-md overflow-hidden bg-gray-200 flex-shrink-0" :class="{ 'privacy-blur': privacyMode }">
|
||||
<img
|
||||
@@ -892,12 +892,12 @@
|
||||
<div class="min-w-0 flex-1">
|
||||
<div
|
||||
v-if="chatHistoryModalInfo?.isChatRoom && (rec.senderDisplayName || rec.sourcename)"
|
||||
class="text-xs text-gray-500 leading-none truncate mb-1"
|
||||
class="chat-history-modal-sender text-xs leading-none truncate mb-1"
|
||||
>
|
||||
{{ rec.senderDisplayName || rec.sourcename }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="rec.fullTime || rec.sourcetime" class="text-xs text-gray-400 flex-shrink-0 leading-none">
|
||||
<div v-if="rec.fullTime || rec.sourcetime" class="chat-history-modal-time text-xs flex-shrink-0 leading-none">
|
||||
{{ rec.fullTime || rec.sourcetime }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1273,7 +1273,7 @@
|
||||
<!-- 导出弹窗 -->
|
||||
<div v-if="exportModalOpen" class="fixed inset-0 z-[11000] flex items-center justify-center">
|
||||
<div class="absolute inset-0 bg-black/40" @click="closeExportModal"></div>
|
||||
<div class="relative w-[960px] max-w-[95vw] bg-white rounded-lg shadow-xl border border-gray-200 overflow-hidden">
|
||||
<div class="chat-export-modal relative w-[960px] max-w-[95vw] bg-white rounded-lg shadow-xl border border-gray-200 overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-gray-200 flex items-center">
|
||||
<div class="text-base font-medium text-gray-900">导出聊天记录(离线 ZIP)</div>
|
||||
<button class="ml-auto text-gray-400 hover:text-gray-700" type="button" @click="closeExportModal">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="flex-1 flex flex-col min-h-0 min-w-0">
|
||||
<div class="conversation-pane flex-1 flex flex-col min-h-0 min-w-0">
|
||||
<div v-if="selectedContact" class="flex-1 flex flex-col min-h-0 relative">
|
||||
<div class="chat-header">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="text-base font-medium text-gray-900" :class="{ 'privacy-blur': privacyMode }">
|
||||
<h2 class="chat-header-title text-base font-medium" :class="{ 'privacy-blur': privacyMode }">
|
||||
{{ selectedContact ? selectedContact.name : '' }}
|
||||
</h2>
|
||||
</div>
|
||||
@@ -71,7 +71,7 @@
|
||||
<button
|
||||
v-if="showJumpToBottom"
|
||||
type="button"
|
||||
class="absolute bottom-6 right-6 z-20 w-10 h-10 rounded-full bg-white/90 border border-gray-200 shadow hover:bg-white flex items-center justify-center"
|
||||
class="jump-to-bottom-btn absolute bottom-6 right-6 z-20 w-10 h-10 rounded-full border shadow flex items-center justify-center"
|
||||
title="回到最新"
|
||||
@click="scrollToBottom"
|
||||
>
|
||||
@@ -81,15 +81,15 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex-1 flex items-center justify-center">
|
||||
<div v-else class="conversation-empty flex-1 flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="w-20 h-20 mx-auto mb-5 rounded-2xl bg-gradient-to-br from-[#03C160]/10 to-[#03C160]/5 flex items-center justify-center">
|
||||
<svg class="w-10 h-10 text-[#03C160]/60" viewBox="0 0 24 24" fill="currentColor">
|
||||
<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"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-base font-medium text-gray-700 mb-1.5">选择一个会话</h3>
|
||||
<p class="text-sm text-gray-400">
|
||||
<h3 class="conversation-empty-title text-base font-medium mb-1.5">选择一个会话</h3>
|
||||
<p class="conversation-empty-text text-sm">
|
||||
从左侧列表选择联系人查看聊天记录
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="message.quoteTitle || message.quoteContent"
|
||||
class="mt-[5px] px-2 text-xs text-neutral-600 rounded max-w-[404px] max-h-[65px] overflow-hidden flex items-start bg-[#e1e1e1]">
|
||||
class="wechat-quote-preview mt-[5px] px-2 text-xs rounded max-w-[404px] max-h-[65px] overflow-hidden flex items-start">
|
||||
<div class="py-2 min-w-0 flex-1">
|
||||
<div v-if="isQuotedVoice(message)" class="flex items-center gap-1 min-w-0">
|
||||
<span v-if="message.quoteTitle" class="truncate flex-shrink-0">{{ message.quoteTitle }}:</span>
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
:data-create-time="message.createTime"
|
||||
>
|
||||
<div v-if="message.showTimeDivider" class="flex justify-center mb-4">
|
||||
<div class="px-3 py-1 text-xs text-[#9e9e9e]">
|
||||
<div class="message-time-divider px-3 py-1 text-xs">
|
||||
{{ message.timeDivider }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="message.renderType === 'system'" class="flex justify-center">
|
||||
<div class="px-3 py-1 text-xs text-[#9e9e9e]">
|
||||
<div class="message-time-divider px-3 py-1 text-xs">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,7 +110,7 @@
|
||||
:class="[message.isSent ? 'items-end' : 'items-start', { 'privacy-blur': privacyMode }]"
|
||||
@contextmenu="openMediaContextMenu($event, message, 'message')"
|
||||
>
|
||||
<div v-if="message.isGroup && !message.isSent && message.senderDisplayName" class="text-[11px] text-gray-500 mb-1" :class="message.isSent ? 'text-right' : 'text-left'">
|
||||
<div v-if="message.isGroup && !message.isSent && message.senderDisplayName" class="message-sender-name text-[11px] mb-1" :class="message.isSent ? 'text-right' : 'text-left'">
|
||||
{{ message.senderDisplayName }}
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div ref="messageContainerRef" class="flex-1 overflow-y-auto p-4 min-h-0" @scroll="onMessageScroll">
|
||||
<div ref="messageContainerRef" class="message-list flex-1 overflow-y-auto p-4 min-h-0" @scroll="onMessageScroll">
|
||||
<div v-if="selectedContact && hasMoreMessages" class="flex justify-center mb-4">
|
||||
<div
|
||||
class="text-xs px-3 py-1 rounded-md bg-white border border-gray-200 text-gray-700 select-none"
|
||||
class="message-list-load-more text-xs px-3 py-1 rounded-md border select-none"
|
||||
:class="isLoadingMessages ? 'opacity-60' : 'hover:bg-gray-50 cursor-pointer'"
|
||||
@click="!isLoadingMessages && loadMoreMessages()"
|
||||
>
|
||||
@@ -10,13 +10,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoadingMessages && messages.length === 0" class="text-center text-sm text-gray-500 py-6">
|
||||
<div v-if="isLoadingMessages && messages.length === 0" class="message-list-status text-center text-sm py-6">
|
||||
加载中...
|
||||
</div>
|
||||
<div v-else-if="messagesError" class="text-center text-sm text-red-500 py-6 whitespace-pre-wrap">
|
||||
{{ messagesError }}
|
||||
</div>
|
||||
<div v-else-if="messages.length === 0" class="text-center text-sm text-gray-500 py-6">
|
||||
<div v-else-if="messages.length === 0" class="message-list-status text-center text-sm py-6">
|
||||
暂无聊天记录
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="session-list-panel border-r border-gray-200 flex flex-col min-h-0 shrink-0 relative"
|
||||
:style="{ backgroundColor: '#F7F7F7', '--session-list-width': sessionListWidth + 'px' }"
|
||||
class="session-list-panel border-r flex flex-col min-h-0 shrink-0 relative"
|
||||
:style="{ '--session-list-width': sessionListWidth + 'px' }"
|
||||
>
|
||||
<!-- 拖动调整会话列表宽度 -->
|
||||
<div
|
||||
@@ -14,7 +14,7 @@
|
||||
<!-- 聊天列表 -->
|
||||
<div class="h-full flex flex-col min-h-0">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="p-3 border-b border-gray-200" style="background-color: #F7F7F7">
|
||||
<div class="session-list-search p-3 border-b">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="contact-search-wrapper flex-1">
|
||||
<svg class="contact-search-icon" fill="none" stroke="currentColor" viewBox="0 0 16 16">
|
||||
@@ -53,7 +53,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 联系人列表 -->
|
||||
<div class="flex-1 overflow-y-auto min-h-0">
|
||||
<div class="session-list-scroll flex-1 overflow-y-auto min-h-0">
|
||||
<div v-if="isLoadingContacts" class="px-3 py-4 h-full overflow-hidden">
|
||||
<div v-for="i in 15" :key="i" class="flex items-center space-x-3 h-[calc(80px/var(--dpr))]">
|
||||
<div class="w-[calc(45px/var(--dpr))] h-[calc(45px/var(--dpr))] rounded-md bg-gray-200 skeleton-pulse"></div>
|
||||
@@ -63,22 +63,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="contactsError" class="px-3 py-2 text-sm text-red-500 whitespace-pre-wrap">
|
||||
<div v-else-if="contactsError" class="session-list-status px-3 py-2 text-sm text-red-500 whitespace-pre-wrap">
|
||||
{{ contactsError }}
|
||||
</div>
|
||||
<div v-else-if="contacts.length === 0" class="px-3 py-2 text-sm text-gray-500">
|
||||
<div v-else-if="contacts.length === 0" class="session-list-status px-3 py-2 text-sm">
|
||||
暂无会话
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-for="contact in filteredContacts" :key="contact.id"
|
||||
class="px-3 cursor-pointer transition-colors duration-150 border-b border-gray-100 h-[calc(80px/var(--dpr))] flex items-center"
|
||||
:class="contact.isTop
|
||||
? (selectedContact?.id === contact.id
|
||||
? 'bg-[#D2D2D2] hover:bg-[#C7C7C7]'
|
||||
: 'bg-[#EAEAEA] hover:bg-[#DEDEDE]')
|
||||
: (selectedContact?.id === contact.id
|
||||
? 'bg-[#DEDEDE] hover:bg-[#d3d3d3]'
|
||||
: 'hover:bg-[#eaeaea]')"
|
||||
class="session-list-item px-3 cursor-pointer transition-colors duration-150 h-[calc(80px/var(--dpr))] flex items-center"
|
||||
:class="{
|
||||
'session-list-item--top': contact.isTop,
|
||||
'session-list-item--selected': selectedContact?.id === contact.id
|
||||
}"
|
||||
@click="selectContact(contact)">
|
||||
<div class="flex items-center space-x-3 w-full">
|
||||
<!-- 联系人头像 -->
|
||||
@@ -101,12 +98,12 @@
|
||||
<!-- 联系人信息 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-gray-900 truncate" :class="{ 'privacy-blur': privacyMode }">{{ contact.name }}</h3>
|
||||
<h3 class="session-list-item-name text-sm font-medium truncate" :class="{ 'privacy-blur': privacyMode }">{{ contact.name }}</h3>
|
||||
<div class="flex items-center flex-shrink-0 ml-2">
|
||||
<span class="text-xs text-gray-500">{{ contact.lastMessageTime }}</span>
|
||||
<span class="session-list-item-time text-xs">{{ contact.lastMessageTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 truncate mt-0.5 leading-tight" :class="{ 'privacy-blur': privacyMode }">
|
||||
<p class="session-list-item-preview text-xs truncate mt-0.5 leading-tight" :class="{ 'privacy-blur': privacyMode }">
|
||||
<span
|
||||
v-for="(seg, idx) in parseTextWithEmoji(
|
||||
(contact.unreadCount > 0 ? `[${contact.unreadCount > 99 ? '99+' : contact.unreadCount}条] ` : '') +
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
export const UI_THEME_KEY = 'ui.theme'
|
||||
export const UI_THEME_LIGHT = 'light'
|
||||
export const UI_THEME_DARK = 'dark'
|
||||
|
||||
export const normalizeUiTheme = (value, fallback = UI_THEME_LIGHT) => {
|
||||
const normalized = String(value || '').trim().toLowerCase()
|
||||
if (normalized === UI_THEME_DARK) return UI_THEME_DARK
|
||||
if (normalized === UI_THEME_LIGHT) return UI_THEME_LIGHT
|
||||
return fallback === UI_THEME_DARK ? UI_THEME_DARK : UI_THEME_LIGHT
|
||||
}
|
||||
|
||||
export const readUiTheme = (fallback = UI_THEME_LIGHT) => {
|
||||
if (!process.client) return normalizeUiTheme(fallback)
|
||||
try {
|
||||
const raw = localStorage.getItem(UI_THEME_KEY)
|
||||
return normalizeUiTheme(raw, fallback)
|
||||
} catch {
|
||||
return normalizeUiTheme(fallback)
|
||||
}
|
||||
}
|
||||
|
||||
export const writeUiTheme = (theme) => {
|
||||
if (!process.client) return
|
||||
try {
|
||||
localStorage.setItem(UI_THEME_KEY, normalizeUiTheme(theme))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export const applyUiTheme = (theme) => {
|
||||
if (!process.client || typeof document === 'undefined') return
|
||||
const normalized = normalizeUiTheme(theme)
|
||||
const root = document.documentElement
|
||||
root.dataset.theme = normalized
|
||||
root.classList.toggle('theme-dark', normalized === UI_THEME_DARK)
|
||||
root.style.colorScheme = normalized === UI_THEME_DARK ? 'dark' : 'light'
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="h-screen flex overflow-hidden" style="background-color: #EDEDED">
|
||||
<div class="chat-page-shell h-screen flex overflow-hidden">
|
||||
<SessionListPanel :state="chatState" />
|
||||
|
||||
<div class="flex-1 flex flex-col min-h-0" style="background-color: #EDEDED">
|
||||
<div class="chat-page-main flex-1 flex flex-col min-h-0">
|
||||
<div class="flex-1 flex min-h-0">
|
||||
<ConversationPane :state="chatState" />
|
||||
</div>
|
||||
@@ -46,7 +46,7 @@ definePageMeta({
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: '??????? - ????????'
|
||||
title: '聊天记录 - 微信数据库解密工具'
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
@@ -508,7 +508,7 @@ const onAccountChange = async () => {
|
||||
contactsError.value = ''
|
||||
await loadSessionsForSelectedAccount()
|
||||
} catch (error) {
|
||||
contactsError.value = error?.message || '???????'
|
||||
contactsError.value = error?.message || '加载会话失败'
|
||||
} finally {
|
||||
isLoadingContacts.value = false
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="h-screen flex overflow-hidden" style="background-color: #EDEDED">
|
||||
<div class="flex-1 flex flex-col min-h-0" style="background-color: #EDEDED">
|
||||
<div class="contacts-page h-screen flex overflow-hidden" style="background-color: var(--app-shell-bg)">
|
||||
<div class="flex-1 flex flex-col min-h-0" style="background-color: var(--app-shell-bg)">
|
||||
<div class="flex-1 min-h-0 overflow-hidden p-4">
|
||||
<div class="h-full grid grid-cols-1 lg:grid-cols-[400px_minmax(0,1fr)] gap-4">
|
||||
<div class="bg-white border border-gray-200 rounded-lg flex flex-col min-h-0 overflow-hidden">
|
||||
<div class="p-3 border-b border-gray-200" style="background-color: #F7F7F7">
|
||||
<div class="p-3 border-b border-gray-200" style="background-color: var(--app-surface-muted)">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="contact-search-wrapper flex-1" :class="{ 'privacy-blur': privacyMode }">
|
||||
<svg class="contact-search-icon" fill="none" stroke="currentColor" viewBox="0 0 16 16">
|
||||
@@ -80,7 +80,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4 flex flex-col gap-3">
|
||||
<div class="contacts-export-panel bg-white border border-gray-200 rounded-lg p-4 flex flex-col gap-3">
|
||||
<div>
|
||||
<div class="text-base font-medium text-gray-900">导出联系人</div>
|
||||
<div class="text-xs text-gray-500 mt-1">支持 JSON / CSV,默认包含头像链接</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen relative overflow-hidden flex items-center justify-center">
|
||||
<div class="decrypt-result-page min-h-screen relative overflow-hidden flex items-center justify-center">
|
||||
<!-- 网格背景 -->
|
||||
<div class="absolute inset-0 bg-grid-pattern opacity-5 pointer-events-none"></div>
|
||||
|
||||
@@ -171,4 +171,4 @@ onMounted(() => {
|
||||
linear-gradient(90deg, rgba(7, 193, 96, 0.1) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex items-center justify-center py-8">
|
||||
<div class="decrypt-page min-h-screen flex items-center justify-center py-8">
|
||||
|
||||
<div class="max-w-4xl mx-auto px-6 w-full">
|
||||
<!-- 步骤指示器 -->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen relative overflow-hidden flex items-center">
|
||||
<div class="detection-result-page min-h-screen relative overflow-hidden flex items-center">
|
||||
<!-- 网格背景 -->
|
||||
<div class="absolute inset-0 bg-grid-pattern opacity-5 pointer-events-none"></div>
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="h-screen flex overflow-hidden" style="background-color: #EDEDED">
|
||||
<div class="edits-page h-screen flex overflow-hidden" style="background-color: var(--app-shell-bg)">
|
||||
<!-- 左侧:会话列表(与聊天页统一风格) -->
|
||||
<div class="edits-sidebar border-r border-gray-200 flex flex-col">
|
||||
<!-- 搜索栏区域 -->
|
||||
<div class="p-3 border-b border-gray-200" style="background-color: #F7F7F7">
|
||||
<div class="p-3 border-b border-gray-200" style="background-color: var(--app-surface-muted)">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="contact-search-wrapper flex-1">
|
||||
<svg class="contact-search-icon" fill="none" stroke="currentColor" viewBox="0 0 16 16">
|
||||
@@ -137,7 +137,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 内容区 -->
|
||||
<div class="flex-1 overflow-y-auto" style="background-color: #EDEDED">
|
||||
<div class="flex-1 overflow-y-auto" style="background-color: var(--app-shell-bg)">
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="itemsError" class="mx-5 mt-4 text-sm text-red-600 bg-red-50 border border-red-100 rounded-lg px-4 py-3 whitespace-pre-wrap">{{ itemsError }}</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex items-center justify-center relative overflow-hidden">
|
||||
<div class="landing-page min-h-screen flex items-center justify-center relative overflow-hidden">
|
||||
<!-- 网格背景 -->
|
||||
<div class="absolute inset-0 bg-grid-pattern opacity-5"></div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="h-screen flex overflow-hidden" style="background-color: #EDEDED">
|
||||
<div class="sns-page h-screen flex overflow-hidden" style="background-color: var(--app-shell-bg)">
|
||||
<!-- 左侧朋友圈联系人 -->
|
||||
<div class="w-[280px] flex flex-col min-h-0 border-r border-gray-200 bg-[#EDEDED]">
|
||||
<div class="w-[280px] flex flex-col min-h-0 border-r border-gray-200 bg-[#EDEDED]" style="background-color: var(--app-shell-bg)">
|
||||
<div class="p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm font-semibold text-gray-700">朋友圈联系人</div>
|
||||
@@ -104,7 +104,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 右侧朋友圈区域 -->
|
||||
<div class="flex-1 flex flex-col min-h-0" style="background-color: #EDEDED">
|
||||
<div class="flex-1 flex flex-col min-h-0" style="background-color: var(--app-shell-bg)">
|
||||
<div ref="timelineScrollEl" class="flex-1 overflow-auto min-h-0 bg-white" @scroll="onScroll">
|
||||
<div class="max-w-2xl mx-auto px-4 py-4">
|
||||
<div class="relative w-full mb-12 -mt-4 bg-white">
|
||||
|
||||
@@ -303,14 +303,13 @@ const slides = computed(() => {
|
||||
return out
|
||||
})
|
||||
|
||||
const currentBg = computed(() => '#F3FFF8')
|
||||
const currentBg = '#F3FFF8'
|
||||
const deckTrackClass = computed(() => 'z-10')
|
||||
|
||||
const applyViewportBg = () => {
|
||||
if (!import.meta.client) return
|
||||
const bg = currentBg.value
|
||||
document.documentElement.style.backgroundColor = bg
|
||||
document.body.style.backgroundColor = bg
|
||||
document.documentElement.style.backgroundColor = currentBg
|
||||
document.body.style.backgroundColor = currentBg
|
||||
}
|
||||
|
||||
const slideStyle = computed(() => (
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import {
|
||||
UI_THEME_DARK,
|
||||
UI_THEME_LIGHT,
|
||||
applyUiTheme,
|
||||
normalizeUiTheme,
|
||||
readUiTheme,
|
||||
writeUiTheme,
|
||||
} from '~/lib/ui-theme'
|
||||
|
||||
export const useThemeStore = defineStore('theme', () => {
|
||||
const theme = ref(UI_THEME_LIGHT)
|
||||
const initialized = ref(false)
|
||||
|
||||
const isDark = computed(() => theme.value === UI_THEME_DARK)
|
||||
|
||||
const set = (nextTheme) => {
|
||||
theme.value = normalizeUiTheme(nextTheme, UI_THEME_LIGHT)
|
||||
writeUiTheme(theme.value)
|
||||
applyUiTheme(theme.value)
|
||||
}
|
||||
|
||||
const init = () => {
|
||||
if (initialized.value) {
|
||||
applyUiTheme(theme.value)
|
||||
return
|
||||
}
|
||||
initialized.value = true
|
||||
theme.value = readUiTheme(UI_THEME_LIGHT)
|
||||
applyUiTheme(theme.value)
|
||||
}
|
||||
|
||||
const toggle = () => {
|
||||
set(isDark.value ? UI_THEME_LIGHT : UI_THEME_DARK)
|
||||
}
|
||||
|
||||
return {
|
||||
theme,
|
||||
initialized,
|
||||
isDark,
|
||||
init,
|
||||
set,
|
||||
toggle,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user