mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-05-16 07:36:47 +00:00
Merge pull request #40 from Jasonzhu1207/refactor/ui-rebuild
Refactor/UI rebuild
This commit is contained in:
92
.github/workflows/release.yml
vendored
92
.github/workflows/release.yml
vendored
@@ -28,7 +28,23 @@ jobs:
|
||||
node-version: 24
|
||||
cache: "npm"
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
run: npm install --ignore-scripts
|
||||
|
||||
- name: Ensure mac key helpers are executable
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for file in \
|
||||
resources/key/macos/universal/xkey_helper \
|
||||
resources/key/macos/universal/image_scan_helper \
|
||||
resources/key/macos/universal/xkey_helper_macos \
|
||||
resources/key/macos/universal/libwx_key.dylib
|
||||
do
|
||||
if [ -f "$file" ]; then
|
||||
chmod +x "$file"
|
||||
ls -l "$file"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Sync version with tag
|
||||
shell: bash
|
||||
@@ -52,9 +68,9 @@ jobs:
|
||||
set -euo pipefail
|
||||
export ELECTRON_BUILDER_BINARIES_MIRROR="https://github.com/electron-userland/electron-builder-binaries/releases/download/"
|
||||
echo "Using ELECTRON_BUILDER_BINARIES_MIRROR=$ELECTRON_BUILDER_BINARIES_MIRROR"
|
||||
if ! npx electron-builder --mac dmg zip --arm64 --publish always '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}'; then
|
||||
if ! npx electron-builder --mac dmg zip --arm64 --publish always '--config.npmRebuild=false' '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}'; then
|
||||
echo "::warning::DMG packaging failed (hdiutil instability on runner). Retrying with ZIP only."
|
||||
npx electron-builder --mac zip --arm64 --publish always '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}'
|
||||
npx electron-builder --mac zip --arm64 --publish always '--config.npmRebuild=false' '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}'
|
||||
fi
|
||||
|
||||
- name: Inject minimumVersion into latest yml
|
||||
@@ -327,33 +343,49 @@ jobs:
|
||||
retry_cmd 5 3 gh release edit "$TAG" --repo "$REPO" --notes-file release_notes.md
|
||||
|
||||
deploy-aur:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release-linux]
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release-linux]
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- name: Check AUR credentials
|
||||
id: aur-credentials
|
||||
shell: bash
|
||||
env:
|
||||
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
if [ -z "${AUR_SSH_PRIVATE_KEY}" ]; then
|
||||
echo "::notice::AUR_SSH_PRIVATE_KEY is not configured; skipping AUR publish."
|
||||
echo "enabled=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "enabled=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Update PKGBUILD version
|
||||
run: |
|
||||
NEW_VER=$(echo "${{ github.ref_name }}" | sed 's/^v//')
|
||||
sed -i "s/^pkgver=.*/pkgver=${NEW_VER}/" resources/installer/linux/PKGBUILD
|
||||
sed -i "s/^pkgrel=.*/pkgrel=1/" resources/installer/linux/PKGBUILD
|
||||
- name: Checkout code
|
||||
if: steps.aur-credentials.outputs.enabled == 'true'
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Publish AUR package
|
||||
uses: KSXGitHub/github-actions-deploy-aur@master
|
||||
with:
|
||||
pkgname: weflow
|
||||
pkgbuild: resources/installer/linux/PKGBUILD
|
||||
updpkgsums: true
|
||||
assets: |
|
||||
resources/installer/linux/weflow.desktop
|
||||
resources/installer/linux/icon.png
|
||||
resources/installer/linux/.gitignore
|
||||
- name: Update PKGBUILD version
|
||||
if: steps.aur-credentials.outputs.enabled == 'true'
|
||||
run: |
|
||||
NEW_VER=$(echo "${{ github.ref_name }}" | sed 's/^v//')
|
||||
sed -i "s/^pkgver=.*/pkgver=${NEW_VER}/" resources/installer/linux/PKGBUILD
|
||||
sed -i "s/^pkgrel=.*/pkgrel=1/" resources/installer/linux/PKGBUILD
|
||||
|
||||
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
commit_username: H3CoF6
|
||||
commit_email: h3cof6@gmail.com
|
||||
ssh_keyscan_types: ed25519
|
||||
- name: Publish AUR package
|
||||
if: steps.aur-credentials.outputs.enabled == 'true'
|
||||
uses: KSXGitHub/github-actions-deploy-aur@master
|
||||
with:
|
||||
pkgname: weflow
|
||||
pkgbuild: resources/installer/linux/PKGBUILD
|
||||
updpkgsums: true
|
||||
assets: |
|
||||
resources/installer/linux/weflow.desktop
|
||||
resources/installer/linux/icon.png
|
||||
resources/installer/linux/.gitignore
|
||||
|
||||
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
commit_username: H3CoF6
|
||||
commit_email: h3cof6@gmail.com
|
||||
ssh_keyscan_types: ed25519
|
||||
|
||||
@@ -1001,6 +1001,8 @@ function createAgreementWindow() {
|
||||
*/
|
||||
function createSplashWindow(): BrowserWindow {
|
||||
const isDev = !!process.env.VITE_DEV_SERVER_URL
|
||||
const splashThemeId = configService?.get('themeId') || 'cloud-dancer'
|
||||
const splashThemeMode = configService?.get('theme') || 'system'
|
||||
const iconPath = isDev
|
||||
? join(__dirname, '../public/icon.ico')
|
||||
: (process.platform === 'darwin'
|
||||
@@ -1008,7 +1010,7 @@ function createSplashWindow(): BrowserWindow {
|
||||
: join(process.resourcesPath, 'icon.ico'))
|
||||
|
||||
splashWindow = new BrowserWindow({
|
||||
width: 760,
|
||||
width: 680,
|
||||
height: 460,
|
||||
resizable: false,
|
||||
frame: false,
|
||||
@@ -1027,9 +1029,17 @@ function createSplashWindow(): BrowserWindow {
|
||||
})
|
||||
|
||||
if (isDev) {
|
||||
splashWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}splash.html`)
|
||||
const splashUrl = new URL('splash.html', process.env.VITE_DEV_SERVER_URL)
|
||||
splashUrl.searchParams.set('themeId', splashThemeId)
|
||||
splashUrl.searchParams.set('themeMode', splashThemeMode)
|
||||
splashWindow.loadURL(splashUrl.toString())
|
||||
} else {
|
||||
splashWindow.loadFile(join(__dirname, '../dist/splash.html'))
|
||||
splashWindow.loadFile(join(__dirname, '../dist/splash.html'), {
|
||||
query: {
|
||||
themeId: splashThemeId,
|
||||
themeMode: splashThemeMode
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
splashWindow.once('ready-to-show', () => {
|
||||
@@ -1309,9 +1319,6 @@ function createChatHistoryRouteWindow(route: string) {
|
||||
? join(process.resourcesPath, 'icon.icns')
|
||||
: join(process.resourcesPath, 'icon.ico'))
|
||||
|
||||
// 根据系统主题设置窗口背景色
|
||||
const isDark = nativeTheme.shouldUseDarkColors
|
||||
|
||||
const win = new BrowserWindow({
|
||||
width: 600,
|
||||
height: 800,
|
||||
@@ -1326,13 +1333,31 @@ function createChatHistoryRouteWindow(route: string) {
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay: false,
|
||||
show: false,
|
||||
backgroundColor: isDark ? '#1A1A1A' : '#F0F0F0',
|
||||
backgroundColor: '#FFFFFF',
|
||||
autoHideMenuBar: true
|
||||
})
|
||||
setupCustomTitleBarWindow(win)
|
||||
|
||||
win.once('ready-to-show', () => {
|
||||
let hasShown = false
|
||||
let isReadyToShow = false
|
||||
let hasLoadedRoute = false
|
||||
const showChatHistoryWindow = () => {
|
||||
if (hasShown || !isReadyToShow || !hasLoadedRoute || win.isDestroyed()) return
|
||||
hasShown = true
|
||||
win.show()
|
||||
}
|
||||
|
||||
win.webContents.once('did-finish-load', () => {
|
||||
hasLoadedRoute = true
|
||||
setTimeout(showChatHistoryWindow, 30)
|
||||
})
|
||||
win.webContents.once('did-fail-load', () => {
|
||||
hasLoadedRoute = true
|
||||
showChatHistoryWindow()
|
||||
})
|
||||
win.once('ready-to-show', () => {
|
||||
isReadyToShow = true
|
||||
showChatHistoryWindow()
|
||||
})
|
||||
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
|
||||
@@ -4,246 +4,478 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WeFlow</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
<script>
|
||||
(function initSplashMode() {
|
||||
var params = new URLSearchParams(window.location.search || "");
|
||||
var mode = params.get("themeMode") || params.get("mode") || "system";
|
||||
var themeId = params.get("themeId") || "cloud-dancer";
|
||||
var mq = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)");
|
||||
var resolved = mode === "dark" || (mode === "system" && mq && mq.matches) ? "dark" : "light";
|
||||
|
||||
html, body {
|
||||
width: 100%; height: 100%;
|
||||
background: transparent;
|
||||
document.documentElement.setAttribute("data-theme", themeId);
|
||||
document.documentElement.setAttribute("data-theme-mode", mode);
|
||||
document.documentElement.setAttribute("data-mode", resolved);
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
:root {
|
||||
--surface-start: #ffffff;
|
||||
--surface-end: #f8f9fc;
|
||||
--accent: #5b6abf;
|
||||
--accent-rgb: 91, 106, 191;
|
||||
--ambient-glow: rgba(91, 106, 191, 0.08);
|
||||
--text: #1a1b1e;
|
||||
--text-muted: #5f6368;
|
||||
--text-faint: #9aa0a6;
|
||||
--border-subtle: rgba(0, 0, 0, 0.05);
|
||||
--loader-track: rgba(0, 0, 0, 0.06);
|
||||
--shadow-window:
|
||||
0 24px 60px rgba(23, 27, 38, 0.10),
|
||||
0 4px 12px rgba(23, 27, 38, 0.04),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 1);
|
||||
--radius-window: 24px;
|
||||
--ease-ambient: cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
--font: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
[data-mode="dark"] {
|
||||
--surface-start: #14171d;
|
||||
--surface-end: #0b0d10;
|
||||
--accent: #7c8deb;
|
||||
--accent-rgb: 124, 141, 235;
|
||||
--ambient-glow: rgba(124, 141, 235, 0.08);
|
||||
--text: #f0f0f0;
|
||||
--text-muted: #8b92a5;
|
||||
--text-faint: #4e5569;
|
||||
--border-subtle: rgba(255, 255, 255, 0.06);
|
||||
--loader-track: rgba(255, 255, 255, 0.09);
|
||||
--shadow-window:
|
||||
0 24px 80px rgba(0, 0, 0, 0.60),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
--radius-window: 20px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.splash {
|
||||
width: 100%; height: 100%;
|
||||
border-radius: 20px;
|
||||
.splash-shell {
|
||||
width: 600px;
|
||||
height: 380px;
|
||||
max-width: calc(100vw - 64px);
|
||||
max-height: calc(100vh - 64px);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-window);
|
||||
border: 1px solid var(--border-subtle);
|
||||
background: linear-gradient(145deg, var(--surface-start), var(--surface-end));
|
||||
box-shadow: var(--shadow-window);
|
||||
isolation: isolate;
|
||||
animation: windowAppear 800ms var(--ease-ambient) both;
|
||||
}
|
||||
|
||||
/* 品牌区 */
|
||||
.brand {
|
||||
padding: 48px 52px 0;
|
||||
.splash-shell::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
left: -50%;
|
||||
top: -50%;
|
||||
background: radial-gradient(circle at 50% 40%, var(--ambient-glow) 0%, transparent 44%);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.brand-stage {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: -20px;
|
||||
text-align: center;
|
||||
animation: contentIn 560ms var(--ease-ambient) 90ms both;
|
||||
}
|
||||
|
||||
.logo-core {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-bottom: 24px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
border-radius: 20px;
|
||||
animation: logoBreathe 3200ms ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 24px;
|
||||
line-height: 1.18;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--text);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
[data-mode="dark"] .app-name {
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.50);
|
||||
}
|
||||
|
||||
.app-desc {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
[data-mode="dark"] .app-desc {
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
position: absolute;
|
||||
left: 32px;
|
||||
right: 32px;
|
||||
bottom: 24px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
color: var(--text-faint);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
font-variant-numeric: tabular-nums;
|
||||
animation: contentIn 560ms var(--ease-ambient) 170ms both;
|
||||
}
|
||||
|
||||
.progress-text-wrap {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
animation: fadeIn 0.4s ease both;
|
||||
gap: 6px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.logo {
|
||||
width: 56px; height: 56px;
|
||||
border-radius: 14px;
|
||||
|
||||
[data-mode="dark"] .progress-text-wrap {
|
||||
color: var(--text-faint);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 6px rgba(var(--accent-rgb), 0.42);
|
||||
animation: dotPulse 1700ms ease-in-out infinite;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.version {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.app-name {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.app-desc {
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
opacity: 0.6;
|
||||
color: var(--text-faint);
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0;
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.spacer { flex: 1; }
|
||||
|
||||
/* 底部进度区 */
|
||||
.bottom {
|
||||
padding: 0 48px 40px;
|
||||
animation: fadeIn 0.4s ease 0.1s both;
|
||||
[data-mode="dark"] .version {
|
||||
opacity: 0.50;
|
||||
}
|
||||
|
||||
/* 进度条轨道 */
|
||||
.progress-track {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 进度条填充 */
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 扫光:只在有进度时显示,不循环 */
|
||||
.progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.5) 50%, transparent 100%);
|
||||
animation: sweep 1.2s ease-out forwards;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
height: 3px;
|
||||
background: var(--loader-track);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-mode="dark"] .progress-track {
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
border-radius: 0 999px 999px 0;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 18px rgba(var(--accent-rgb), 0.34);
|
||||
overflow: hidden;
|
||||
transition: width 440ms var(--ease-ambient);
|
||||
}
|
||||
|
||||
.progress-fill::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
right: -18px;
|
||||
width: 44px;
|
||||
height: 15px;
|
||||
border-radius: 999px;
|
||||
background: rgba(var(--accent-rgb), 0.34);
|
||||
filter: blur(8px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 等待阶段:进度条末端呼吸光点 */
|
||||
.progress-fill.waiting::before {
|
||||
content: '';
|
||||
.progress-fill::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -1px; right: -2px;
|
||||
width: 6px; height: 4px;
|
||||
border-radius: 50%;
|
||||
background: inherit;
|
||||
filter: blur(2px);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
inset: -1px 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.54), transparent);
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
animation: spectralGlide 1200ms ease-out;
|
||||
}
|
||||
|
||||
.bottom-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.progress-text {
|
||||
font-size: 11px;
|
||||
opacity: 0.38;
|
||||
}
|
||||
.version {
|
||||
font-size: 11px;
|
||||
opacity: 0.25;
|
||||
.progress-fill.waiting::before {
|
||||
opacity: 0.65;
|
||||
animation: leadingGlow 1300ms ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.splash-shell,
|
||||
.brand-stage,
|
||||
.status-row,
|
||||
.logo-image,
|
||||
.status-dot,
|
||||
.progress-fill,
|
||||
.progress-fill::before,
|
||||
.progress-fill::after {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
left: 0 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
@keyframes sweep {
|
||||
0% { opacity: 0; transform: translateX(-100%); }
|
||||
20% { opacity: 1; }
|
||||
80% { opacity: 1; }
|
||||
100% { opacity: 0; transform: translateX(100%); }
|
||||
|
||||
@keyframes windowAppear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.97) translateY(12px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.4; transform: scaleX(1); }
|
||||
50% { opacity: 1; transform: scaleX(1.8); }
|
||||
|
||||
@keyframes contentIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes logoBreathe {
|
||||
0% {
|
||||
opacity: 0.94;
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dotPulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.38;
|
||||
transform: scale(0.84);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.18);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes leadingGlow {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.38;
|
||||
transform: scaleX(0.78);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.86;
|
||||
transform: scaleX(1.28);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spectralGlide {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
22%,
|
||||
66% {
|
||||
opacity: 0.58;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="splash" id="splash">
|
||||
<div class="brand">
|
||||
<img class="logo" src="./logo.png" alt="WeFlow" />
|
||||
<div class="brand-text">
|
||||
<div class="app-name" id="appName">WeFlow</div>
|
||||
<div class="app-desc" id="appDesc">微信聊天记录管理工具</div>
|
||||
<main class="splash-shell" id="splash" role="status" aria-live="polite">
|
||||
<section class="brand-stage" aria-label="WeFlow">
|
||||
<div class="logo-core" aria-hidden="true">
|
||||
<img class="logo-image" src="./logo.png" alt="">
|
||||
</div>
|
||||
|
||||
<h1 class="app-name">WeFlow</h1>
|
||||
<p class="app-desc">微信聊天记录管理工具</p>
|
||||
</section>
|
||||
|
||||
<div class="status-row">
|
||||
<div class="progress-text-wrap">
|
||||
<div class="status-dot" aria-hidden="true"></div>
|
||||
<div class="progress-text" id="progressText">正在预加载会话逻辑...</div>
|
||||
</div>
|
||||
<div class="version" id="versionText"></div>
|
||||
</div>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<div class="bottom">
|
||||
<div class="progress-track" id="progressTrack">
|
||||
<div class="progress-fill" id="progressFill"></div>
|
||||
</div>
|
||||
<div class="bottom-row">
|
||||
<div class="progress-text" id="progressText">正在启动...</div>
|
||||
<div class="version" id="versionText"></div>
|
||||
</div>
|
||||
<div class="progress-track" aria-hidden="true">
|
||||
<div class="progress-fill" id="progressFill"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
var themes = {
|
||||
'cloud-dancer': {
|
||||
light: { primary: '#8B7355', bg: '#F0EEE9', bgEnd: '#E5E1DA', text: '#3d3d3d', desc: '#8B7355' },
|
||||
dark: { primary: '#C9A86C', bg: '#1a1816', bgEnd: '#252220', text: '#F0EEE9', desc: '#C9A86C' }
|
||||
},
|
||||
'corundum-blue': {
|
||||
light: { primary: '#4A6670', bg: '#E8EEF0', bgEnd: '#D8E4E8', text: '#3d3d3d', desc: '#4A6670' },
|
||||
dark: { primary: '#6A9AAA', bg: '#141a1c', bgEnd: '#1e2a2e', text: '#E0EEF2', desc: '#6A9AAA' }
|
||||
},
|
||||
'kiwi-green': {
|
||||
light: { primary: '#7A9A5C', bg: '#E8F0E4', bgEnd: '#D8E8D2', text: '#3d3d3d', desc: '#7A9A5C' },
|
||||
dark: { primary: '#9ABA7C', bg: '#161a14', bgEnd: '#222a1e', text: '#E8F0E4', desc: '#9ABA7C' }
|
||||
},
|
||||
'spicy-red': {
|
||||
light: { primary: '#8B4049', bg: '#F0E8E8', bgEnd: '#E8D8D8', text: '#3d3d3d', desc: '#8B4049' },
|
||||
dark: { primary: '#C06068', bg: '#1a1416', bgEnd: '#261e20', text: '#F2E8EA', desc: '#C06068' }
|
||||
},
|
||||
'teal-water': {
|
||||
light: { primary: '#5A8A8A', bg: '#E4F0F0', bgEnd: '#D2E8E8', text: '#3d3d3d', desc: '#5A8A8A' },
|
||||
dark: { primary: '#7ABAAA', bg: '#121a1a', bgEnd: '#1a2626', text: '#E0F2EE', desc: '#7ABAAA' }
|
||||
},
|
||||
'blossom-dream': {
|
||||
light: { primary: '#D4849A', primaryEnd: '#D4849A', bg: '#FCF9FB', bgMid: '#F8F2F8', bgEnd: '#F2F6FB', text: '#2E2633', desc: '#D4849A' },
|
||||
dark: { primary: '#C670C3', primaryEnd: '#8A60C0', bg: '#120B16', bgMid: '#1A1020', bgEnd: '#0E0B18', text: '#F2EAF4', desc: '#C670C3' }
|
||||
}
|
||||
};
|
||||
var themeModeQuery = null;
|
||||
var systemModeQuery = null;
|
||||
|
||||
function applyTheme(themeId, mode) {
|
||||
var t = themes[themeId] || themes['cloud-dancer'];
|
||||
var isDark = mode === 'dark';
|
||||
if (mode === 'system') isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
var c = isDark ? t.dark : t.light;
|
||||
|
||||
var el = document.getElementById('splash');
|
||||
var fill = document.getElementById('progressFill');
|
||||
|
||||
if (themeId === 'blossom-dream') {
|
||||
if (isDark) {
|
||||
// 深色
|
||||
el.style.background =
|
||||
'radial-gradient(ellipse 60% 50% at 100% 0%, ' + c.primary + '28 0%, transparent 70%), ' +
|
||||
'linear-gradient(150deg, ' + c.bg + ' 0%, ' + c.bgMid + ' 45%, ' + c.bgEnd + ' 100%)';
|
||||
} else {
|
||||
// 浅色
|
||||
el.style.background = 'linear-gradient(150deg, ' + c.bg + ' 0%, ' + c.bgMid + ' 45%, ' + c.bgEnd + ' 100%)';
|
||||
}
|
||||
// 进度条
|
||||
fill.style.background = 'linear-gradient(90deg, ' + c.primary + ' 0%, ' + c.primaryEnd + ' 100%)';
|
||||
} else {
|
||||
if (isDark) {
|
||||
el.style.background =
|
||||
'radial-gradient(ellipse 60% 50% at 100% 0%, ' + c.primary + '22 0%, transparent 70%), ' +
|
||||
'linear-gradient(145deg, ' + c.bg + ' 0%, ' + c.bgEnd + ' 100%)';
|
||||
} else {
|
||||
el.style.background = 'linear-gradient(150deg, ' + c.bg + ' 0%, ' + c.bgEnd + ' 100%)';
|
||||
}
|
||||
fill.style.background = c.primary;
|
||||
}
|
||||
|
||||
document.getElementById('appName').style.color = c.text;
|
||||
document.getElementById('appDesc').style.color = c.desc;
|
||||
document.getElementById('progressText').style.color = c.text;
|
||||
document.getElementById('versionText').style.color = c.text;
|
||||
document.getElementById('progressTrack').style.background = c.primary + (isDark ? '25' : '18');
|
||||
function resolveMode(mode) {
|
||||
if (mode === "dark" || mode === "light") return mode;
|
||||
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
function syncSystemModeListener(mode) {
|
||||
if (!window.matchMedia) return;
|
||||
var nextQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
if (systemModeQuery && systemModeQuery !== nextQuery && systemModeQuery.removeEventListener) {
|
||||
systemModeQuery.removeEventListener("change", handleSystemModeChange);
|
||||
}
|
||||
|
||||
systemModeQuery = nextQuery;
|
||||
themeModeQuery = mode;
|
||||
|
||||
if (mode === "system" && nextQuery.addEventListener) {
|
||||
nextQuery.addEventListener("change", handleSystemModeChange);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSystemModeChange() {
|
||||
if (themeModeQuery === "system") {
|
||||
document.documentElement.setAttribute("data-mode", resolveMode("system"));
|
||||
}
|
||||
}
|
||||
|
||||
function applyTheme(themeId, mode) {
|
||||
var safeThemeId = String(themeId || "cloud-dancer");
|
||||
var safeMode = mode === "light" || mode === "dark" || mode === "system" ? mode : "system";
|
||||
var resolvedMode = resolveMode(safeMode);
|
||||
|
||||
document.documentElement.setAttribute("data-theme", safeThemeId);
|
||||
document.documentElement.setAttribute("data-theme-mode", safeMode);
|
||||
document.documentElement.setAttribute("data-mode", resolvedMode);
|
||||
syncSystemModeListener(safeMode);
|
||||
}
|
||||
|
||||
// percent: 实际进度值;waiting: 是否处于等待阶段
|
||||
function updateProgress(percent, text, waiting) {
|
||||
var fill = document.getElementById('progressFill');
|
||||
var label = document.getElementById('progressText');
|
||||
var fill = document.getElementById("progressFill");
|
||||
var label = document.getElementById("progressText");
|
||||
var safePercent = Math.max(0, Math.min(100, Number(percent) || 0));
|
||||
|
||||
if (fill) {
|
||||
fill.style.width = percent + '%';
|
||||
fill.style.width = safePercent + "%";
|
||||
if (waiting) {
|
||||
fill.classList.add('waiting');
|
||||
fill.classList.add("waiting");
|
||||
} else {
|
||||
fill.classList.remove('waiting');
|
||||
// 触发扫光:重置动画
|
||||
fill.style.animation = 'none';
|
||||
fill.classList.remove("waiting");
|
||||
fill.style.animation = "none";
|
||||
fill.offsetHeight;
|
||||
fill.style.animation = '';
|
||||
fill.style.animation = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (label && text) label.textContent = text;
|
||||
}
|
||||
|
||||
function setVersion(ver) {
|
||||
var el = document.getElementById('versionText');
|
||||
if (el) el.textContent = 'v' + ver;
|
||||
function setVersion(version) {
|
||||
var el = document.getElementById("versionText");
|
||||
if (!el) return;
|
||||
var text = String(version || "").trim();
|
||||
el.textContent = text ? "v" + text.replace(/^v/i, "") : "";
|
||||
}
|
||||
|
||||
applyTheme('cloud-dancer', 'light');
|
||||
(function bootstrapSplash() {
|
||||
var params = new URLSearchParams(window.location.search || "");
|
||||
applyTheme(params.get("themeId") || "cloud-dancer", params.get("themeMode") || "system");
|
||||
updateProgress(0, "", false);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
98
src/App.scss
98
src/App.scss
@@ -3,56 +3,15 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-primary);
|
||||
animation: appFadeIn 0.35s ease-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 繁花如梦:底色层(::before)+ 光晕层(::after)分离,避免 blur 吃掉边缘
|
||||
[data-theme="blossom-dream"] .app-container {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// ::before 纯底色,不模糊
|
||||
[data-theme="blossom-dream"] .app-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: -2;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
// ::after 光晕层,模糊叠加在底色上
|
||||
[data-theme="blossom-dream"] .app-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
background:
|
||||
radial-gradient(ellipse 55% 45% at 15% 20%, var(--blossom-pink) 0%, transparent 70%),
|
||||
radial-gradient(ellipse 50% 40% at 85% 75%, var(--blossom-peach) 0%, transparent 65%),
|
||||
radial-gradient(ellipse 45% 50% at 80% 10%, var(--blossom-blue) 0%, transparent 60%);
|
||||
filter: blur(80px);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
// 深色模式光晕更克制
|
||||
[data-theme="blossom-dream"][data-mode="dark"] .app-container::after {
|
||||
background:
|
||||
radial-gradient(ellipse 55% 45% at 15% 20%, var(--blossom-pink) 0%, transparent 70%),
|
||||
radial-gradient(ellipse 50% 40% at 85% 75%, var(--blossom-purple) 0%, transparent 65%),
|
||||
radial-gradient(ellipse 45% 50% at 80% 10%, var(--blossom-blue) 0%, transparent 60%);
|
||||
filter: blur(100px);
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.window-drag-region {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 150px; // 预留系统最小化/最大化/关闭按钮区域
|
||||
right: 150px;
|
||||
height: 40px;
|
||||
-webkit-app-region: drag;
|
||||
pointer-events: auto;
|
||||
@@ -68,8 +27,9 @@
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 24px;
|
||||
padding: 24px 32px;
|
||||
position: relative;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.export-keepalive-page {
|
||||
@@ -84,18 +44,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes appFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新提示条
|
||||
// ---- Update banner ----
|
||||
.update-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -107,7 +56,7 @@
|
||||
|
||||
.update-text {
|
||||
flex: 1;
|
||||
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -124,7 +73,7 @@
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
@@ -143,7 +92,7 @@
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
transition: opacity 0.15s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
@@ -178,29 +127,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 用户协议弹窗
|
||||
// ---- Agreement modal ----
|
||||
.agreement-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.agreement-modal {
|
||||
width: 520px;
|
||||
max-height: 80vh;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.agreement-header {
|
||||
@@ -241,8 +192,8 @@
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 160, 0, 0.35);
|
||||
background: rgba(255, 160, 0, 0.12);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
color: var(--text-primary);
|
||||
|
||||
strong {
|
||||
@@ -291,19 +242,6 @@
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-footer {
|
||||
@@ -347,21 +285,21 @@
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: var(--border-color);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
color: var(--on-primary);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
transition: opacity 0.15s;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
|
||||
@@ -150,7 +150,7 @@ function App() {
|
||||
}
|
||||
}, [isOnboardingWindow, isNotificationWindow, isAnnualReportWindow, isDualReportWindow])
|
||||
|
||||
// 应用主题
|
||||
// 应用主题 (accent color + light/dark mode)
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const applyMode = (mode: ThemeMode, systemDark?: boolean) => {
|
||||
|
||||
@@ -4,28 +4,29 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-height: 28px;
|
||||
min-height: 32px;
|
||||
padding: 4px 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-analysis-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
gap: 4px;
|
||||
padding: 4px 8px 4px 4px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
@@ -33,12 +34,13 @@
|
||||
.chat-analysis-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
color: var(--text-tertiary);
|
||||
|
||||
.chat-analysis-breadcrumb-separator {
|
||||
opacity: 0.6;
|
||||
opacity: 0.5;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,25 +51,27 @@
|
||||
.chat-analysis-current-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: color 0.2s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
.current {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
svg {
|
||||
transition: transform 0.2s ease;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -78,34 +82,33 @@
|
||||
|
||||
.chat-analysis-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 10px);
|
||||
top: calc(100% + 6px);
|
||||
right: 0;
|
||||
min-width: 120px;
|
||||
padding: 6px;
|
||||
background: var(--card-bg);
|
||||
padding: 4px;
|
||||
background: var(--bg-secondary-solid, var(--bg-secondary));
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.12);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-md);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.chat-analysis-menu-item {
|
||||
width: 100%;
|
||||
display: block;
|
||||
padding: 9px 12px;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -457,3 +457,130 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UI rebuild polish for the modal variant used by ExportPage.
|
||||
.export-defaults-settings-form.layout-split {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
||||
.form-group {
|
||||
grid-template-columns: minmax(176px, 0.82fr) minmax(0, 1.18fr);
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--bg-secondary) 82%, var(--bg-primary));
|
||||
}
|
||||
|
||||
.form-group:first-child,
|
||||
.form-group:last-child {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.form-copy {
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 3px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.select-field,
|
||||
.settings-time-range-field,
|
||||
.log-toggle-line,
|
||||
.media-default-grid,
|
||||
.concurrency-inline-options {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.select-trigger,
|
||||
.settings-time-range-trigger {
|
||||
border-radius: 12px;
|
||||
background: var(--bg-primary);
|
||||
min-height: 42px;
|
||||
padding: 9px 12px;
|
||||
}
|
||||
|
||||
.log-toggle-line {
|
||||
border-radius: 12px;
|
||||
background: var(--bg-primary);
|
||||
min-height: 42px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.concurrency-inline-options {
|
||||
grid-template-columns: repeat(6, minmax(38px, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.concurrency-option {
|
||||
min-width: 0;
|
||||
min-height: 36px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.format-setting-group {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.format-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.format-card {
|
||||
min-height: 68px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.format-label,
|
||||
.format-desc {
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.media-default-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(78px, 1fr));
|
||||
gap: 8px;
|
||||
|
||||
label {
|
||||
min-height: 36px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.export-defaults-settings-form.layout-split {
|
||||
.form-group {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.format-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(156px, 1fr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Bot, User } from 'lucide-react'
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
role: 'user' | 'ai';
|
||||
content: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface MessageBubbleProps {
|
||||
message: ChatMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化后的消息气泡组件
|
||||
* 使用 React.memo 避免不必要的重新渲染
|
||||
*/
|
||||
export const MessageBubble = React.memo<MessageBubbleProps>(({ message }) => {
|
||||
return (
|
||||
<div className={`message-row ${message.role}`}>
|
||||
<div className="avatar">
|
||||
{message.role === 'ai' ? <Bot size={24} /> : <User size={24} />}
|
||||
</div>
|
||||
<div className="bubble">
|
||||
<div className="content">{message.content}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}, (prevProps, nextProps) => {
|
||||
// 自定义比较函数:只有内容或ID变化时才重新渲染
|
||||
return prevProps.message.content === nextProps.message.content &&
|
||||
prevProps.message.id === nextProps.message.id
|
||||
})
|
||||
|
||||
MessageBubble.displayName = 'MessageBubble'
|
||||
@@ -1,14 +1,17 @@
|
||||
// Redesigned sidebar — premium feel with left accent bar, refined spacing
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
width: var(--sidebar-width, 260px);
|
||||
background: var(--bg-sidebar, var(--bg-secondary));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 0;
|
||||
transition: width 0.25s ease;
|
||||
padding: 0;
|
||||
transition: width 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--border-color);
|
||||
|
||||
&.collapsed {
|
||||
width: 64px;
|
||||
width: 68px;
|
||||
|
||||
.sidebar-user-card-wrap {
|
||||
margin: 0 8px 8px;
|
||||
@@ -21,28 +24,166 @@
|
||||
.user-meta {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-menu-caret {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu,
|
||||
.sidebar-footer {
|
||||
.nav-menu {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 0 8px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-badge:not(.icon-badge) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
gap: 0;
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Navigation ----
|
||||
.nav-menu {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
padding: 12px 10px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 9px 14px;
|
||||
border-radius: 10px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
white-space: nowrap;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
margin: 1px 0;
|
||||
|
||||
// Left accent bar for active state
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) scaleY(0);
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
border-radius: 0 2px 2px 0;
|
||||
background: var(--primary);
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
|
||||
&::before {
|
||||
transform: translateY(-50%) scaleY(1);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.nav-icon-with-badge {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-badge {
|
||||
margin-left: auto;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
padding: 0 6px;
|
||||
background: #ef4444;
|
||||
color: #ffffff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.nav-badge.icon-badge {
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
right: -10px;
|
||||
margin-left: 0;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
font-size: 10px;
|
||||
box-shadow: 0 0 0 2px var(--bg-sidebar, var(--bg-secondary));
|
||||
}
|
||||
|
||||
// ---- Footer ----
|
||||
.sidebar-footer {
|
||||
padding: 4px 10px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 8px;
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
// ---- User card ----
|
||||
.sidebar-user-card-wrap {
|
||||
position: relative;
|
||||
margin: 0 12px 10px;
|
||||
margin: 0 10px 10px;
|
||||
--sidebar-user-menu-width: 172px;
|
||||
}
|
||||
|
||||
@@ -55,16 +196,16 @@
|
||||
z-index: 12;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
background: var(--bg-secondary-solid, var(--bg-primary));
|
||||
background: var(--bg-secondary-solid, var(--bg-secondary));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 6px;
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12);
|
||||
gap: 2px;
|
||||
padding: 4px;
|
||||
box-shadow: var(--shadow-md);
|
||||
opacity: 0;
|
||||
transform: translateY(8px) scale(0.95);
|
||||
transform: translateY(6px) scale(0.97);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
|
||||
&.open {
|
||||
opacity: 1;
|
||||
@@ -76,10 +217,10 @@
|
||||
.sidebar-user-menu-item {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
padding: 9px 10px;
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -87,54 +228,53 @@
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: #d93025;
|
||||
color: #ef4444;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 59, 48, 0.08);
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-user-card {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
background: var(--bg-secondary);
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 56px;
|
||||
min-height: 52px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
|
||||
border: none;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(99, 102, 241, 0.32);
|
||||
background: var(--bg-tertiary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
&.menu-open {
|
||||
border-color: rgba(99, 102, 241, 0.44);
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.12);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-hover));
|
||||
background: var(--primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 0 2px var(--bg-sidebar, var(--bg-secondary));
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
@@ -144,7 +284,7 @@
|
||||
|
||||
span {
|
||||
color: var(--on-primary);
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
@@ -164,7 +304,7 @@
|
||||
}
|
||||
|
||||
.user-wxid {
|
||||
margin-top: 2px;
|
||||
margin-top: 1px;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
white-space: nowrap;
|
||||
@@ -175,129 +315,10 @@
|
||||
.user-menu-caret {
|
||||
color: var(--text-tertiary);
|
||||
display: inline-flex;
|
||||
transition: transform 0.2s ease, color 0.2s ease;
|
||||
transition: transform 0.15s ease;
|
||||
|
||||
&.open {
|
||||
transform: rotate(180deg);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 9999px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--primary);
|
||||
color: var(--on-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-icon-with-badge {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-badge {
|
||||
margin-left: auto;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
padding: 0 6px;
|
||||
background: #ff3b30;
|
||||
color: #ffffff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
box-shadow: 0 0 0 2px rgba(255, 59, 48, 0.18);
|
||||
}
|
||||
|
||||
.nav-badge.icon-badge {
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
right: -10px;
|
||||
margin-left: 0;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
font-size: 10px;
|
||||
box-shadow: 0 0 0 2px var(--bg-secondary);
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 0 12px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 12px;
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
// 繁花如梦主题:侧边栏毛玻璃 + 激活项用主品牌色
|
||||
[data-theme="blossom-dream"] .sidebar {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
[data-theme="blossom-dream"][data-mode="dark"] .sidebar {
|
||||
background: rgba(34, 30, 36, 0.75);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
// 激活项:主品牌色纵向微渐变
|
||||
[data-theme="blossom-dream"] .nav-item.active {
|
||||
background: linear-gradient(180deg, #D4849A 0%, #C4748A 100%);
|
||||
}
|
||||
|
||||
// 深色激活项:用藕粉色,背景深灰底 + 粉色文字/图标(高阶玩法)
|
||||
[data-theme="blossom-dream"][data-mode="dark"] .nav-item.active {
|
||||
background: rgba(209, 158, 187, 0.15);
|
||||
color: #D19EBB;
|
||||
border: 1px solid rgba(209, 158, 187, 0.2);
|
||||
}
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px 16px;
|
||||
background: rgba(15, 23, 42, 0.38);
|
||||
background: rgba(15, 23, 42, 0.28);
|
||||
}
|
||||
|
||||
.contact-sns-dialog {
|
||||
width: min(760px, 100%);
|
||||
max-height: min(86vh, 860px);
|
||||
border-radius: 14px;
|
||||
width: min(720px, 100%);
|
||||
max-height: min(84vh, 820px);
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary-solid, #ffffff);
|
||||
box-shadow: 0 22px 46px rgba(0, 0, 0, 0.24);
|
||||
box-shadow: 0 18px 34px rgba(0, 0, 0, 0.18);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -29,7 +29,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 14px 16px;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
}
|
||||
|
||||
.contact-sns-dialog-avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 10px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-hover));
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
@@ -69,7 +69,7 @@
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -79,7 +79,7 @@
|
||||
|
||||
.contact-sns-dialog-username {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -88,7 +88,7 @@
|
||||
|
||||
.contact-sns-dialog-stats {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
@@ -111,9 +111,9 @@
|
||||
border-radius: 8px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-secondary);
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
padding: 0 9px;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
@@ -134,8 +134,8 @@
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
width: 248px;
|
||||
max-height: calc((28px * 15) + 16px);
|
||||
width: 228px;
|
||||
max-height: calc((26px * 15) + 16px);
|
||||
overflow-y: auto;
|
||||
border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--border-color));
|
||||
border-radius: 10px;
|
||||
@@ -220,26 +220,20 @@
|
||||
}
|
||||
|
||||
.contact-sns-dialog-tip {
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--border-color) 88%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-primary) 78%, var(--bg-secondary));
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
word-break: break-word;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.contact-sns-dialog-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 12px 16px 14px;
|
||||
padding: 10px 12px 12px;
|
||||
}
|
||||
|
||||
.contact-sns-dialog-posts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.contact-sns-dialog-posts-list .post-header-actions {
|
||||
@@ -247,9 +241,9 @@
|
||||
}
|
||||
|
||||
.contact-sns-dialog-status {
|
||||
padding: 20px 12px;
|
||||
padding: 16px 10px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
|
||||
&.empty {
|
||||
@@ -264,8 +258,8 @@
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
border-radius: 10px;
|
||||
padding: 9px 18px;
|
||||
font-size: 13px;
|
||||
padding: 8px 14px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
@@ -282,15 +276,15 @@
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.contact-sns-dialog-overlay {
|
||||
padding: 12px 8px;
|
||||
padding: 10px 8px;
|
||||
}
|
||||
|
||||
.contact-sns-dialog {
|
||||
width: min(100vw - 16px, 760px);
|
||||
width: min(100vw - 16px, 720px);
|
||||
max-height: calc(100vh - 24px);
|
||||
|
||||
.contact-sns-dialog-header {
|
||||
padding: 12px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.contact-sns-dialog-header-actions {
|
||||
@@ -300,18 +294,13 @@
|
||||
.contact-sns-dialog-rank-btn {
|
||||
height: 26px;
|
||||
padding: 0 8px;
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.contact-sns-dialog-rank-panel {
|
||||
width: min(78vw, 232px);
|
||||
}
|
||||
|
||||
.contact-sns-dialog-tip {
|
||||
padding: 10px 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.contact-sns-dialog-body {
|
||||
padding: 10px 10px 12px;
|
||||
}
|
||||
|
||||
@@ -538,10 +538,6 @@ export function ContactSnsTimelineDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="contact-sns-dialog-tip">
|
||||
在微信桌面客户端中打开这个人的朋友圈浏览,可快速把其朋友圈同步到这里。若你在乎这个人,一定要试试~
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="contact-sns-dialog-body"
|
||||
onScroll={handleBodyScroll}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import { Search, User, X, Loader2, CheckSquare, Square, Download } from 'lucide-react'
|
||||
import { Virtuoso } from 'react-virtuoso'
|
||||
import { Avatar } from '../Avatar'
|
||||
|
||||
interface Contact {
|
||||
@@ -51,10 +52,14 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
||||
onClearSelectedContacts,
|
||||
onExportSelectedContacts
|
||||
}) => {
|
||||
const filteredContacts = contacts.filter(c =>
|
||||
(c.displayName || '').toLowerCase().includes(contactSearch.toLowerCase()) ||
|
||||
c.username.toLowerCase().includes(contactSearch.toLowerCase())
|
||||
)
|
||||
const filteredContacts = React.useMemo(() => {
|
||||
const keyword = contactSearch.trim().toLowerCase()
|
||||
if (!keyword) return contacts
|
||||
return contacts.filter(c =>
|
||||
(c.displayName || '').toLowerCase().includes(keyword) ||
|
||||
c.username.toLowerCase().includes(keyword)
|
||||
)
|
||||
}, [contacts, contactSearch])
|
||||
const selectedContactLookup = React.useMemo(
|
||||
() => new Set(selectedContactUsernames),
|
||||
[selectedContactUsernames]
|
||||
@@ -85,10 +90,52 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
||||
return '没有找到联系人'
|
||||
}
|
||||
|
||||
const renderContactRow = React.useCallback((_: number, contact: Contact) => {
|
||||
const isPostCountReady = contact.postCountStatus === 'ready'
|
||||
const isSelected = selectedContactLookup.has(contact.username)
|
||||
const isActive = activeContactUsername === contact.username
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`contact-row${isSelected ? ' is-selected' : ''}${isActive ? ' is-active' : ''}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`contact-select-btn${isSelected ? ' checked' : ''}`}
|
||||
onClick={() => onToggleContactSelected(contact)}
|
||||
title={isSelected ? `取消选择 ${contact.displayName}` : `选择 ${contact.displayName}`}
|
||||
aria-pressed={isSelected}
|
||||
>
|
||||
{isSelected ? <CheckSquare size={14} /> : <Square size={14} />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="contact-main-btn"
|
||||
onClick={() => onOpenContactTimeline(contact)}
|
||||
title={`查看 ${contact.displayName} 的朋友圈`}
|
||||
>
|
||||
<Avatar src={contact.avatarUrl} name={contact.displayName} size={28} shape="rounded" />
|
||||
<div className="contact-meta">
|
||||
<span className="contact-name">{contact.displayName}</span>
|
||||
</div>
|
||||
<div className="contact-post-count-wrap">
|
||||
{isPostCountReady ? (
|
||||
<span className="contact-post-count">{Math.max(0, Math.floor(Number(contact.postCount || 0)))}条</span>
|
||||
) : (
|
||||
<span className="contact-post-count-loading" title="统计中">
|
||||
<Loader2 size={12} className="spinning" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}, [activeContactUsername, onOpenContactTimeline, onToggleContactSelected, selectedContactLookup])
|
||||
|
||||
return (
|
||||
<aside className="sns-filter-panel">
|
||||
<div className="filter-header">
|
||||
<h3>筛选条件</h3>
|
||||
<h3>筛选</h3>
|
||||
{(searchKeyword || contactSearch) && (
|
||||
<button className="reset-all-btn" onClick={clearFilters} title="重置所有筛选">
|
||||
<RefreshCw size={14} />
|
||||
@@ -101,12 +148,12 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
||||
<div className="filter-widget search-widget">
|
||||
<div className="widget-header">
|
||||
<Search size={14} />
|
||||
<span>关键词搜索</span>
|
||||
<span>关键词</span>
|
||||
</div>
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索动态内容..."
|
||||
placeholder="搜索动态"
|
||||
value={searchKeyword}
|
||||
onChange={e => setSearchKeyword(e.target.value)}
|
||||
/>
|
||||
@@ -130,7 +177,7 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
||||
<div className="contact-search-bar">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="查找好友..."
|
||||
placeholder="查找联系人"
|
||||
value={contactSearch}
|
||||
onChange={e => setContactSearch(e.target.value)}
|
||||
/>
|
||||
@@ -162,53 +209,17 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="contact-interaction-hint">
|
||||
点左侧可多选下载,点右侧可查看单人详情
|
||||
</div>
|
||||
|
||||
<div className="contact-list-scroll">
|
||||
{filteredContacts.map(contact => {
|
||||
const isPostCountReady = contact.postCountStatus === 'ready'
|
||||
const isSelected = selectedContactLookup.has(contact.username)
|
||||
const isActive = activeContactUsername === contact.username
|
||||
return (
|
||||
<div
|
||||
key={contact.username}
|
||||
className={`contact-row${isSelected ? ' is-selected' : ''}${isActive ? ' is-active' : ''}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`contact-select-btn${isSelected ? ' checked' : ''}`}
|
||||
onClick={() => onToggleContactSelected(contact)}
|
||||
title={isSelected ? `取消选择 ${contact.displayName}` : `选择 ${contact.displayName}`}
|
||||
aria-pressed={isSelected}
|
||||
>
|
||||
{isSelected ? <CheckSquare size={16} /> : <Square size={16} />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="contact-main-btn"
|
||||
onClick={() => onOpenContactTimeline(contact)}
|
||||
title={`查看 ${contact.displayName} 的朋友圈`}
|
||||
>
|
||||
<Avatar src={contact.avatarUrl} name={contact.displayName} size={36} shape="rounded" />
|
||||
<div className="contact-meta">
|
||||
<span className="contact-name">{contact.displayName}</span>
|
||||
</div>
|
||||
<div className="contact-post-count-wrap">
|
||||
{isPostCountReady ? (
|
||||
<span className="contact-post-count">{Math.max(0, Math.floor(Number(contact.postCount || 0)))}条</span>
|
||||
) : (
|
||||
<span className="contact-post-count-loading" title="统计中">
|
||||
<Loader2 size={13} className="spinning" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{filteredContacts.length === 0 && (
|
||||
{filteredContacts.length > 0 ? (
|
||||
<Virtuoso
|
||||
className="contact-list-virtuoso"
|
||||
data={filteredContacts}
|
||||
computeItemKey={(_, contact) => contact.username}
|
||||
fixedItemHeight={40}
|
||||
itemContent={renderContactRow}
|
||||
overscan={320}
|
||||
/>
|
||||
) : (
|
||||
<div className="empty-state">{getEmptyStateText()}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -493,7 +493,7 @@ export const SnsPostItem: React.FC<SnsPostItemProps> = ({ post, onPreview, onDeb
|
||||
<Avatar
|
||||
src={post.avatarUrl}
|
||||
name={post.nickname}
|
||||
size={48}
|
||||
size={36}
|
||||
shape="rounded"
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
.title-bar {
|
||||
height: 41px;
|
||||
background: var(--bg-secondary);
|
||||
height: 48px;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-right: 8px;
|
||||
-webkit-app-region: drag;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
@@ -14,12 +13,6 @@
|
||||
z-index: 2101;
|
||||
}
|
||||
|
||||
// 繁花如梦:标题栏毛玻璃
|
||||
[data-theme="blossom-dream"] .title-bar {
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.title-brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -33,16 +26,15 @@
|
||||
}
|
||||
|
||||
.titles {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.title-sidebar-toggle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
@@ -52,11 +44,11 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
@@ -64,26 +56,26 @@
|
||||
.title-window-controls {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 2px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.title-window-control-btn {
|
||||
width: 28px;
|
||||
width: 36px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text-tertiary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -107,14 +99,14 @@
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -124,8 +116,8 @@
|
||||
}
|
||||
|
||||
&.live-play-btn.active {
|
||||
background: rgba(var(--primary-rgb, 76, 132, 255), 0.16);
|
||||
color: var(--primary, #4c84ff);
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 加载和错误状态
|
||||
// Loading and error states
|
||||
.loading-container,
|
||||
.error-container {
|
||||
display: flex;
|
||||
@@ -23,7 +23,7 @@
|
||||
color: var(--text-secondary);
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
animation: analyticsSpin 1s linear infinite;
|
||||
}
|
||||
|
||||
p.loading-status {
|
||||
@@ -33,13 +33,12 @@
|
||||
}
|
||||
|
||||
.progress-bar-wrapper {
|
||||
width: 300px;
|
||||
height: 8px;
|
||||
width: 280px;
|
||||
height: 4px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
@@ -47,9 +46,9 @@
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background: var(--primary-gradient);
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 0 10px rgba(139, 115, 85, 0.3);
|
||||
background: var(--primary);
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.progress-percent {
|
||||
@@ -65,57 +64,82 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
@keyframes analyticsSpin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
// Page scroll content
|
||||
.page-scroll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.page-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 统计卡片
|
||||
// Stats overview cards
|
||||
.stats-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
gap: 14px;
|
||||
padding: 18px 16px;
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--primary-light);
|
||||
border-radius: 12px;
|
||||
border-radius: 10px;
|
||||
color: var(--primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 2px;
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
@@ -125,23 +149,23 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
padding: 10px 14px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
|
||||
svg {
|
||||
color: var(--text-tertiary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Charts
|
||||
.charts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
@@ -155,30 +179,30 @@
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 16px;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// Rankings
|
||||
.rankings-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-primary);
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.rank {
|
||||
@@ -196,13 +220,13 @@
|
||||
|
||||
&.top {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
color: var(--on-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.contact-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
|
||||
@@ -228,8 +252,8 @@
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -239,24 +263,21 @@
|
||||
&.medal-1 {
|
||||
background: linear-gradient(135deg, #ffd700, #ffb800);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 4px rgba(255, 184, 0, 0.4);
|
||||
}
|
||||
|
||||
&.medal-2 {
|
||||
background: linear-gradient(135deg, #c0c0c0, #a8a8a8);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 4px rgba(168, 168, 168, 0.4);
|
||||
}
|
||||
|
||||
&.medal-3 {
|
||||
background: linear-gradient(135deg, #cd7f32, #b87333);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 4px rgba(184, 115, 51, 0.4);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,7 +286,7 @@
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
|
||||
.contact-name {
|
||||
@@ -284,14 +305,14 @@
|
||||
}
|
||||
|
||||
.message-count {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式
|
||||
// Responsive
|
||||
@media (max-width: 1200px) {
|
||||
.stats-overview {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@@ -312,11 +333,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 排除好友弹窗
|
||||
// Exclude friends modal
|
||||
.exclude-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -325,13 +346,13 @@
|
||||
}
|
||||
|
||||
.exclude-modal {
|
||||
width: 560px;
|
||||
width: 520px;
|
||||
max-width: calc(100vw - 48px);
|
||||
background: var(--card-bg);
|
||||
background: var(--bg-secondary-solid, var(--bg-secondary));
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.exclude-modal-header {
|
||||
display: flex;
|
||||
@@ -342,6 +363,7 @@
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
@@ -349,14 +371,14 @@
|
||||
.modal-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: var(--bg-tertiary);
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
color: var(--text-tertiary);
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
@@ -370,7 +392,7 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-primary);
|
||||
margin-bottom: 12px;
|
||||
@@ -399,7 +421,7 @@
|
||||
}
|
||||
|
||||
.exclude-modal-body {
|
||||
max-height: 420px;
|
||||
max-height: 380px;
|
||||
overflow: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
@@ -419,7 +441,7 @@
|
||||
.exclude-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.exclude-item {
|
||||
@@ -427,23 +449,23 @@
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.15s;
|
||||
background: var(--bg-primary);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: rgba(7, 193, 96, 0.4);
|
||||
background: rgba(7, 193, 96, 0.08);
|
||||
border-color: rgba(16, 163, 127, 0.3);
|
||||
background: rgba(16, 163, 127, 0.06);
|
||||
}
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
accent-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,7 +477,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.exclude-name {
|
||||
@@ -479,7 +501,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 16px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.exclude-footer-left {
|
||||
|
||||
@@ -1,146 +1,116 @@
|
||||
.analytics-entry-page {
|
||||
.analytics-welcome-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.analytics-welcome-container {
|
||||
.analytics-welcome-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 0;
|
||||
padding: 40px;
|
||||
background: var(--bg-primary);
|
||||
padding: 40px 24px;
|
||||
animation: welcomeFadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
.analytics-welcome-content {
|
||||
text-align: center;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.analytics-welcome-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin: 0 auto 20px;
|
||||
background: var(--primary-light);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.analytics-welcome-content h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 10px;
|
||||
color: var(--text-primary);
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
overflow-y: auto;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
&.analytics-welcome-container--mode {
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(7, 193, 96, 0.06), transparent 48%),
|
||||
var(--bg-primary);
|
||||
.analytics-welcome-content p {
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 32px;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.analytics-welcome-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.analytics-welcome-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px 18px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
color: var(--text-secondary);
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--text-tertiary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
|
||||
.icon-wrapper {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 24px;
|
||||
background: rgba(7, 193, 96, 0.1);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #07c160;
|
||||
|
||||
svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 40px;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.action-cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 30px 20px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: center;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
border-color: #07c160;
|
||||
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.1);
|
||||
|
||||
.card-icon {
|
||||
color: #07c160;
|
||||
background: rgba(7, 193, 96, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 12px;
|
||||
background: var(--bg-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.analytics-welcome-container {
|
||||
padding: 28px 18px;
|
||||
.analytics-welcome-card-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
.action-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.analytics-welcome-card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.analytics-welcome-card-meta {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 540px) {
|
||||
.analytics-welcome-actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
@keyframes welcomeFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
||||
@@ -6,12 +6,6 @@ import './AnalyticsWelcomePage.scss'
|
||||
|
||||
function AnalyticsWelcomePage() {
|
||||
const navigate = useNavigate()
|
||||
// 检查是否有任何缓存数据加载或基本的存储状态表明它已准备好。
|
||||
// 实际上,如果 store 没有持久化,`isLoaded` 可能会在应用刷新时重置。
|
||||
// 如果用户点击“加载缓存”但缓存为空,AnalyticsPage 的逻辑(loadData 不带 force)将尝试从后端缓存加载。
|
||||
// 如果后端缓存也为空,则会重新计算。
|
||||
|
||||
// 我们也可以检查 `lastLoadTime` 来显示“上次更新:xxx”(如果已持久化)。
|
||||
const { lastLoadTime } = useAnalyticsStore()
|
||||
|
||||
const handleLoadCache = () => {
|
||||
@@ -28,35 +22,37 @@ function AnalyticsWelcomePage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="analytics-entry-page">
|
||||
<div className="analytics-welcome-shell">
|
||||
<ChatAnalysisHeader currentMode="private" />
|
||||
|
||||
<div className="analytics-welcome-container analytics-welcome-container--mode">
|
||||
<div className="welcome-content">
|
||||
<div className="icon-wrapper">
|
||||
<BarChart2 size={40} />
|
||||
<div className="analytics-welcome-body">
|
||||
<div className="analytics-welcome-content">
|
||||
<div className="analytics-welcome-icon">
|
||||
<BarChart2 size={32} />
|
||||
</div>
|
||||
<h1>私聊数据分析</h1>
|
||||
<p>
|
||||
WeFlow 可以分析你的好友聊天记录,生成详细的统计报表。<br />
|
||||
你可以选择加载上次的分析结果,或者重新开始一次新的私聊分析。
|
||||
分析你的好友聊天记录,生成详细统计报表。<br />
|
||||
选择加载上次结果或开始新分析。
|
||||
</p>
|
||||
|
||||
<div className="action-cards">
|
||||
<button onClick={handleLoadCache}>
|
||||
<div className="card-icon">
|
||||
<History size={24} />
|
||||
<div className="analytics-welcome-actions">
|
||||
<button className="analytics-welcome-card" onClick={handleLoadCache} type="button">
|
||||
<History size={20} />
|
||||
<div className="analytics-welcome-card-text">
|
||||
<span className="analytics-welcome-card-title">加载缓存</span>
|
||||
<span className="analytics-welcome-card-meta">
|
||||
上次更新: {formatLastTime(lastLoadTime)}
|
||||
</span>
|
||||
</div>
|
||||
<h3>加载缓存</h3>
|
||||
<span>查看上次分析结果<br />(上次更新: {formatLastTime(lastLoadTime)})</span>
|
||||
</button>
|
||||
|
||||
<button onClick={handleNewAnalysis}>
|
||||
<div className="card-icon">
|
||||
<RefreshCcw size={24} />
|
||||
<button className="analytics-welcome-card" onClick={handleNewAnalysis} type="button">
|
||||
<RefreshCcw size={20} />
|
||||
<div className="analytics-welcome-card-text">
|
||||
<span className="analytics-welcome-card-title">新的分析</span>
|
||||
<span className="analytics-welcome-card-meta">重新扫描并计算数据</span>
|
||||
</div>
|
||||
<h3>新的分析</h3>
|
||||
<span>重新扫描并计算数据<br />(可能需要几分钟)</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
min-height: 100%;
|
||||
text-align: center;
|
||||
padding: 40px 24px;
|
||||
animation: reportFadeIn 0.35s ease-out;
|
||||
}
|
||||
|
||||
.annual-report-page.report-route-transitioning > :not(.report-launch-overlay) {
|
||||
@@ -20,40 +21,43 @@
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 32px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 12px;
|
||||
margin: 0 0 10px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
font-size: 15px;
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 48px;
|
||||
margin: 0 0 40px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.page-desc.load-summary {
|
||||
margin: 0 0 28px;
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
|
||||
.page-desc.load-summary.complete {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
// ---- Load telemetry ----
|
||||
.load-telemetry {
|
||||
width: min(760px, 100%);
|
||||
padding: 12px 14px;
|
||||
margin: 0 0 28px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent);
|
||||
background: color-mix(in srgb, var(--card-bg) 92%, transparent);
|
||||
width: min(620px, 100%);
|
||||
padding: 12px 16px;
|
||||
margin: 0 0 24px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--card-bg);
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
|
||||
p {
|
||||
margin: 4px 0;
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
@@ -62,31 +66,32 @@
|
||||
}
|
||||
|
||||
.load-telemetry.loading {
|
||||
border-color: color-mix(in srgb, var(--primary) 30%, var(--border-color));
|
||||
border-color: color-mix(in srgb, var(--primary) 25%, var(--border-color));
|
||||
}
|
||||
|
||||
.load-telemetry.complete {
|
||||
border-color: color-mix(in srgb, var(--primary) 40%, var(--border-color));
|
||||
border-color: color-mix(in srgb, var(--primary) 35%, var(--border-color));
|
||||
}
|
||||
|
||||
.load-telemetry.compact {
|
||||
margin: 12px 0 0;
|
||||
width: min(560px, 100%);
|
||||
width: min(500px, 100%);
|
||||
}
|
||||
|
||||
// ---- Report sections ----
|
||||
.report-sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
width: min(760px, 100%);
|
||||
gap: 20px;
|
||||
width: min(620px, 100%);
|
||||
}
|
||||
|
||||
.report-section {
|
||||
width: 100%;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 20px;
|
||||
padding: 28px;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -95,57 +100,57 @@
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
margin: 8px 0 0;
|
||||
font-size: 14px;
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.section-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
gap: 5px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--primary) 12%, transparent);
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
border: 1px solid color-mix(in srgb, var(--primary) 30%, transparent);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.section-hint {
|
||||
margin: 12px 0 0;
|
||||
margin: 10px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
// ---- Year cards ----
|
||||
.year-grid-with-status {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.year-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
max-width: 600px;
|
||||
margin-bottom: 48px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.report-section .year-grid {
|
||||
@@ -169,7 +174,7 @@
|
||||
}
|
||||
|
||||
.year-load-status.complete {
|
||||
color: color-mix(in srgb, var(--primary) 80%, var(--text-secondary));
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.dot-ellipsis {
|
||||
@@ -187,32 +192,33 @@
|
||||
}
|
||||
|
||||
.year-card {
|
||||
width: 120px;
|
||||
height: 100px;
|
||||
width: 88px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--card-bg);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: all 0.15s ease;
|
||||
gap: 2px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
border-color: var(--text-tertiary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.72;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary-light);
|
||||
box-shadow: 0 0 0 1px var(--primary);
|
||||
|
||||
.year-number {
|
||||
color: var(--primary);
|
||||
@@ -220,45 +226,41 @@
|
||||
}
|
||||
|
||||
.year-number {
|
||||
font-size: 32px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.year-label {
|
||||
font-size: 14px;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Generate button ----
|
||||
.generate-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px 40px;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, color-mix(in srgb, var(--primary) 80%, #000) 100%);
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 12px 24px;
|
||||
background: var(--primary);
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
border-radius: 10px;
|
||||
color: var(--on-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 4px 16px color-mix(in srgb, var(--primary) 30%, transparent);
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px color-mix(in srgb, var(--primary) 40%, transparent);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -267,13 +269,18 @@
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: var(--card-bg);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: none;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--bg-hover);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Launch overlay ----
|
||||
.report-launch-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -281,9 +288,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: color-mix(in srgb, var(--bg-primary) 78%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-primary) 80%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
animation: report-launch-overlay-in 420ms ease-out both;
|
||||
animation: report-launch-overlay-in 350ms ease-out both;
|
||||
}
|
||||
|
||||
.launch-core {
|
||||
@@ -293,13 +300,13 @@
|
||||
gap: 10px;
|
||||
text-align: center;
|
||||
color: var(--text-primary);
|
||||
animation: report-launch-core-in 420ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
||||
animation: report-launch-core-in 350ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
||||
}
|
||||
|
||||
.launch-title {
|
||||
margin: 4px 0 0;
|
||||
font-size: 18px;
|
||||
font-weight: 650;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.launch-subtitle {
|
||||
@@ -312,6 +319,7 @@
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
// ---- Animations ----
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
@@ -329,27 +337,34 @@
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
filter: blur(8px);
|
||||
filter: blur(6px);
|
||||
transform: scale(0.985);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes report-launch-overlay-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes report-launch-core-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(18px) scale(0.96);
|
||||
transform: translateY(14px) scale(0.97);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reportFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.backup-page {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 24px;
|
||||
padding: 20px;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
@@ -10,13 +10,13 @@
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
font-size: 22px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
.backup-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -38,17 +38,17 @@
|
||||
.resource-options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin: -8px 0 18px;
|
||||
margin: -6px 0 14px;
|
||||
|
||||
label {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
min-height: 36px;
|
||||
padding: 8px 10px;
|
||||
min-height: 34px;
|
||||
padding: 7px 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -69,7 +69,7 @@
|
||||
.secondary-btn {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 9px 12px;
|
||||
padding: 8px 11px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -100,19 +100,19 @@
|
||||
}
|
||||
|
||||
.backup-status-band {
|
||||
min-height: 88px;
|
||||
min-height: 76px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 18px;
|
||||
padding: 16px 0;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--primary);
|
||||
@@ -142,8 +142,8 @@
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
margin-top: 12px;
|
||||
height: 6px;
|
||||
margin-top: 10px;
|
||||
height: 5px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
@@ -160,7 +160,7 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.summary-item,
|
||||
@@ -168,8 +168,8 @@
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
padding: 14px;
|
||||
min-height: 74px;
|
||||
padding: 12px;
|
||||
min-height: 66px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
@@ -185,14 +185,14 @@
|
||||
|
||||
strong {
|
||||
color: var(--text-primary);
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
}
|
||||
|
||||
.backup-detail {
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 18px;
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.detail-heading {
|
||||
@@ -200,11 +200,11 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
span {
|
||||
@@ -217,7 +217,7 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
div {
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -246,7 +246,7 @@
|
||||
.db-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.db-row {
|
||||
@@ -255,7 +255,7 @@
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 9px 0;
|
||||
padding: 8px 0;
|
||||
font-size: 13px;
|
||||
|
||||
span {
|
||||
|
||||
234
src/pages/Chat/ChatHeader.tsx
Normal file
234
src/pages/Chat/ChatHeader.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Aperture,
|
||||
BarChart3,
|
||||
Calendar,
|
||||
Download,
|
||||
Image as ImageIcon,
|
||||
Info,
|
||||
Loader2,
|
||||
Mic,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Users
|
||||
} from 'lucide-react'
|
||||
import { Avatar } from '../../components/Avatar'
|
||||
import type { ChatSession } from '../../types/models'
|
||||
import type { BatchVoiceTaskType } from '../../stores/batchTranscribeStore'
|
||||
|
||||
export interface ChatHeaderProps {
|
||||
session: ChatSession
|
||||
isGroupChat: boolean
|
||||
standaloneSessionWindow: boolean
|
||||
showGroupMembersPanel: boolean
|
||||
showJumpPopover: boolean
|
||||
showInSessionSearch: boolean
|
||||
showDetailPanel: boolean
|
||||
shouldHideStandaloneDetailButton: boolean
|
||||
isPrivateSnsSupported: boolean
|
||||
isExportActionBusy: boolean
|
||||
isCurrentSessionExporting: boolean
|
||||
isPreparingExportDialog: boolean
|
||||
isBatchTranscribing: boolean
|
||||
runningBatchVoiceTaskType?: BatchVoiceTaskType
|
||||
isBatchDecrypting: boolean
|
||||
isRefreshingMessages: boolean
|
||||
isLoadingMessages: boolean
|
||||
currentSessionId?: string | null
|
||||
jumpCalendarWrapRef: React.RefObject<HTMLDivElement | null>
|
||||
onGroupAnalytics: () => void
|
||||
onToggleGroupMembersPanel: () => void
|
||||
onExportCurrentSession: () => void
|
||||
onOpenSnsTimeline: () => void
|
||||
onBatchTranscribe: () => void
|
||||
onBatchDecrypt: () => void
|
||||
onToggleJumpPopover: () => void
|
||||
onToggleInSessionSearch: () => void
|
||||
onRefreshMessages: () => void
|
||||
onToggleDetailPanel: () => void
|
||||
}
|
||||
|
||||
function ChatHeader({
|
||||
session,
|
||||
isGroupChat,
|
||||
standaloneSessionWindow,
|
||||
showGroupMembersPanel,
|
||||
showJumpPopover,
|
||||
showInSessionSearch,
|
||||
showDetailPanel,
|
||||
shouldHideStandaloneDetailButton,
|
||||
isPrivateSnsSupported,
|
||||
isExportActionBusy,
|
||||
isCurrentSessionExporting,
|
||||
isPreparingExportDialog,
|
||||
isBatchTranscribing,
|
||||
runningBatchVoiceTaskType,
|
||||
isBatchDecrypting,
|
||||
isRefreshingMessages,
|
||||
isLoadingMessages,
|
||||
currentSessionId,
|
||||
jumpCalendarWrapRef,
|
||||
onGroupAnalytics,
|
||||
onToggleGroupMembersPanel,
|
||||
onExportCurrentSession,
|
||||
onOpenSnsTimeline,
|
||||
onBatchTranscribe,
|
||||
onBatchDecrypt,
|
||||
onToggleJumpPopover,
|
||||
onToggleInSessionSearch,
|
||||
onRefreshMessages,
|
||||
onToggleDetailPanel
|
||||
}: ChatHeaderProps) {
|
||||
const sessionName = session.displayName || session.username
|
||||
const exportTitle = isCurrentSessionExporting
|
||||
? '导出中'
|
||||
: isPreparingExportDialog
|
||||
? '正在准备导出模块'
|
||||
: '导出当前会话'
|
||||
const batchVoiceTitle = isBatchTranscribing
|
||||
? `${runningBatchVoiceTaskType === 'decrypt' ? '批量语音解密' : '批量转写'}中,可在导出页任务中心查看进度`
|
||||
: '批量语音处理'
|
||||
|
||||
return (
|
||||
<div className="message-header">
|
||||
<Avatar
|
||||
src={session.avatarUrl}
|
||||
name={sessionName}
|
||||
size={40}
|
||||
className={isGroupChat ? 'group session-avatar' : 'session-avatar'}
|
||||
/>
|
||||
<div className="header-info">
|
||||
<h3>{sessionName}</h3>
|
||||
{isGroupChat && <div className="header-subtitle">群聊</div>}
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
{!standaloneSessionWindow && isGroupChat && (
|
||||
<button className="icon-btn group-analytics-btn" onClick={onGroupAnalytics} title="群聊分析">
|
||||
<BarChart3 size={18} />
|
||||
</button>
|
||||
)}
|
||||
{isGroupChat && (
|
||||
<button
|
||||
className={`icon-btn group-members-btn ${showGroupMembersPanel ? 'active' : ''}`}
|
||||
onClick={onToggleGroupMembersPanel}
|
||||
title="群成员"
|
||||
>
|
||||
<Users size={18} />
|
||||
</button>
|
||||
)}
|
||||
{!standaloneSessionWindow && (
|
||||
<button
|
||||
className={`icon-btn export-session-btn${isExportActionBusy ? ' exporting' : ''}`}
|
||||
onClick={onExportCurrentSession}
|
||||
disabled={!currentSessionId || isExportActionBusy}
|
||||
title={exportTitle}
|
||||
>
|
||||
{isExportActionBusy ? <Loader2 size={18} className="spin" /> : <Download size={18} />}
|
||||
</button>
|
||||
)}
|
||||
{!standaloneSessionWindow && isPrivateSnsSupported && (
|
||||
<button
|
||||
className="icon-btn chat-sns-timeline-btn"
|
||||
onClick={onOpenSnsTimeline}
|
||||
disabled={!currentSessionId}
|
||||
title="查看朋友圈"
|
||||
>
|
||||
<Aperture size={18} />
|
||||
</button>
|
||||
)}
|
||||
{!standaloneSessionWindow && (
|
||||
<button
|
||||
className={`icon-btn batch-transcribe-btn${isBatchTranscribing ? ' transcribing' : ''}`}
|
||||
onClick={onBatchTranscribe}
|
||||
disabled={!currentSessionId}
|
||||
title={batchVoiceTitle}
|
||||
>
|
||||
{isBatchTranscribing ? <Loader2 size={18} className="spin" /> : <Mic size={18} />}
|
||||
</button>
|
||||
)}
|
||||
{!standaloneSessionWindow && (
|
||||
<button
|
||||
className={`icon-btn batch-decrypt-btn${isBatchDecrypting ? ' transcribing' : ''}`}
|
||||
onClick={onBatchDecrypt}
|
||||
disabled={!currentSessionId}
|
||||
title={isBatchDecrypting ? '批量解密中' : '批量解密图片'}
|
||||
>
|
||||
{isBatchDecrypting ? <Loader2 size={18} className="spin" /> : <ImageIcon size={18} />}
|
||||
</button>
|
||||
)}
|
||||
<div className="jump-calendar-anchor" ref={jumpCalendarWrapRef}>
|
||||
<button
|
||||
className={`icon-btn jump-to-time-btn ${showJumpPopover ? 'active' : ''}`}
|
||||
onClick={onToggleJumpPopover}
|
||||
title="跳转到指定时间"
|
||||
>
|
||||
<Calendar size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className={`icon-btn in-session-search-btn ${showInSessionSearch ? 'active' : ''}`}
|
||||
onClick={onToggleInSessionSearch}
|
||||
disabled={!currentSessionId}
|
||||
title="搜索会话消息"
|
||||
>
|
||||
<Search size={18} />
|
||||
</button>
|
||||
<button
|
||||
className="icon-btn refresh-messages-btn"
|
||||
onClick={onRefreshMessages}
|
||||
disabled={isRefreshingMessages || isLoadingMessages}
|
||||
title="刷新消息"
|
||||
>
|
||||
<RefreshCw size={18} className={isRefreshingMessages ? 'spin' : ''} />
|
||||
</button>
|
||||
{!shouldHideStandaloneDetailButton && (
|
||||
<button
|
||||
className={`icon-btn detail-btn ${showDetailPanel ? 'active' : ''}`}
|
||||
onClick={onToggleDetailPanel}
|
||||
title="会话详情"
|
||||
>
|
||||
<Info size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function areEqual(prev: ChatHeaderProps, next: ChatHeaderProps) {
|
||||
return (
|
||||
prev.session.username === next.session.username &&
|
||||
prev.session.displayName === next.session.displayName &&
|
||||
prev.session.avatarUrl === next.session.avatarUrl &&
|
||||
prev.isGroupChat === next.isGroupChat &&
|
||||
prev.standaloneSessionWindow === next.standaloneSessionWindow &&
|
||||
prev.showGroupMembersPanel === next.showGroupMembersPanel &&
|
||||
prev.showJumpPopover === next.showJumpPopover &&
|
||||
prev.showInSessionSearch === next.showInSessionSearch &&
|
||||
prev.showDetailPanel === next.showDetailPanel &&
|
||||
prev.shouldHideStandaloneDetailButton === next.shouldHideStandaloneDetailButton &&
|
||||
prev.isPrivateSnsSupported === next.isPrivateSnsSupported &&
|
||||
prev.isExportActionBusy === next.isExportActionBusy &&
|
||||
prev.isCurrentSessionExporting === next.isCurrentSessionExporting &&
|
||||
prev.isPreparingExportDialog === next.isPreparingExportDialog &&
|
||||
prev.isBatchTranscribing === next.isBatchTranscribing &&
|
||||
prev.runningBatchVoiceTaskType === next.runningBatchVoiceTaskType &&
|
||||
prev.isBatchDecrypting === next.isBatchDecrypting &&
|
||||
prev.isRefreshingMessages === next.isRefreshingMessages &&
|
||||
prev.isLoadingMessages === next.isLoadingMessages &&
|
||||
prev.currentSessionId === next.currentSessionId &&
|
||||
prev.jumpCalendarWrapRef === next.jumpCalendarWrapRef &&
|
||||
prev.onGroupAnalytics === next.onGroupAnalytics &&
|
||||
prev.onToggleGroupMembersPanel === next.onToggleGroupMembersPanel &&
|
||||
prev.onExportCurrentSession === next.onExportCurrentSession &&
|
||||
prev.onOpenSnsTimeline === next.onOpenSnsTimeline &&
|
||||
prev.onBatchTranscribe === next.onBatchTranscribe &&
|
||||
prev.onBatchDecrypt === next.onBatchDecrypt &&
|
||||
prev.onToggleJumpPopover === next.onToggleJumpPopover &&
|
||||
prev.onToggleInSessionSearch === next.onToggleInSessionSearch &&
|
||||
prev.onRefreshMessages === next.onRefreshMessages &&
|
||||
prev.onToggleDetailPanel === next.onToggleDetailPanel
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ChatHeader, areEqual)
|
||||
139
src/pages/Chat/ChatMessageBubble.tsx
Normal file
139
src/pages/Chat/ChatMessageBubble.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React from 'react'
|
||||
import { Check } from 'lucide-react'
|
||||
import { Avatar } from '../../components/Avatar'
|
||||
import type { ChatSession, Message } from '../../types/models'
|
||||
|
||||
export interface ChatMessageBubbleProps {
|
||||
message: Message
|
||||
messageKey: string
|
||||
session: ChatSession
|
||||
showTime?: boolean
|
||||
timeText?: string
|
||||
isSent: boolean
|
||||
isSystem: boolean
|
||||
isEmoji?: boolean
|
||||
isImage?: boolean
|
||||
isVideo?: boolean
|
||||
isVoice?: boolean
|
||||
emojiHasAsset?: boolean
|
||||
emojiError?: boolean
|
||||
avatarUrl?: string
|
||||
isGroupChat?: boolean
|
||||
resolvedSenderName?: string
|
||||
isSelectionMode?: boolean
|
||||
isSelected?: boolean
|
||||
onContextMenu?: (event: React.MouseEvent, message: Message) => void
|
||||
onToggleSelection?: (messageKey: string, isShiftKey?: boolean) => void
|
||||
children: React.ReactNode
|
||||
portal?: React.ReactNode
|
||||
}
|
||||
|
||||
function SelectionCheckbox({ checked, side }: { checked?: boolean; side: 'left' | 'right' }) {
|
||||
return (
|
||||
<div className={`chat-selection-checkbox ${side} ${checked ? 'checked' : ''}`}>
|
||||
{checked && <Check size={14} strokeWidth={3} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ChatMessageBubble({
|
||||
message,
|
||||
messageKey,
|
||||
session,
|
||||
showTime,
|
||||
timeText,
|
||||
isSent,
|
||||
isSystem,
|
||||
isEmoji,
|
||||
isImage,
|
||||
isVideo,
|
||||
isVoice,
|
||||
emojiHasAsset,
|
||||
emojiError,
|
||||
avatarUrl,
|
||||
isGroupChat,
|
||||
resolvedSenderName,
|
||||
isSelectionMode,
|
||||
isSelected,
|
||||
onContextMenu,
|
||||
onToggleSelection,
|
||||
children,
|
||||
portal
|
||||
}: ChatMessageBubbleProps) {
|
||||
const bubbleClass = isSystem ? 'system' : (isSent ? 'sent' : 'received')
|
||||
const avatarName = !isSent
|
||||
? (isGroupChat ? (resolvedSenderName || '?') : (session.displayName || session.username))
|
||||
: '我'
|
||||
|
||||
return (
|
||||
<>
|
||||
{showTime && timeText && (
|
||||
<div className="time-divider">
|
||||
<span>{timeText}</span>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`message-wrapper-with-selection ${isSelectionMode ? 'selectable' : ''}`}
|
||||
data-sent={isSent ? 'true' : 'false'}
|
||||
onClick={(event) => {
|
||||
if (!isSelectionMode) return
|
||||
event.stopPropagation()
|
||||
onToggleSelection?.(messageKey, event.shiftKey)
|
||||
}}
|
||||
>
|
||||
{isSelectionMode && !isSent && <SelectionCheckbox checked={isSelected} side="left" />}
|
||||
|
||||
<div
|
||||
className={`message-bubble ${bubbleClass} ${isEmoji && emojiHasAsset && !emojiError ? 'emoji' : ''} ${isImage ? 'image' : ''} ${isVideo ? 'video' : ''} ${isVoice ? 'voice' : ''}`}
|
||||
onContextMenu={(event) => onContextMenu?.(event, message)}
|
||||
>
|
||||
<div className="bubble-avatar">
|
||||
<Avatar src={avatarUrl} name={avatarName} size={36} className="bubble-avatar" />
|
||||
</div>
|
||||
<div className="bubble-body">
|
||||
{isGroupChat && !isSent && (
|
||||
<div className="sender-name">
|
||||
{resolvedSenderName || '群成员'}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isSelectionMode && isSent && <SelectionCheckbox checked={isSelected} side="right" />}
|
||||
{portal}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function areEqual(prev: ChatMessageBubbleProps, next: ChatMessageBubbleProps) {
|
||||
return (
|
||||
prev.message === next.message &&
|
||||
prev.messageKey === next.messageKey &&
|
||||
prev.session.username === next.session.username &&
|
||||
prev.session.displayName === next.session.displayName &&
|
||||
prev.session.avatarUrl === next.session.avatarUrl &&
|
||||
prev.showTime === next.showTime &&
|
||||
prev.timeText === next.timeText &&
|
||||
prev.isSent === next.isSent &&
|
||||
prev.isSystem === next.isSystem &&
|
||||
prev.isEmoji === next.isEmoji &&
|
||||
prev.isImage === next.isImage &&
|
||||
prev.isVideo === next.isVideo &&
|
||||
prev.isVoice === next.isVoice &&
|
||||
prev.emojiHasAsset === next.emojiHasAsset &&
|
||||
prev.emojiError === next.emojiError &&
|
||||
prev.avatarUrl === next.avatarUrl &&
|
||||
prev.isGroupChat === next.isGroupChat &&
|
||||
prev.resolvedSenderName === next.resolvedSenderName &&
|
||||
prev.isSelectionMode === next.isSelectionMode &&
|
||||
prev.isSelected === next.isSelected &&
|
||||
prev.onContextMenu === next.onContextMenu &&
|
||||
prev.onToggleSelection === next.onToggleSelection &&
|
||||
prev.children === next.children &&
|
||||
prev.portal === next.portal
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ChatMessageBubble, areEqual)
|
||||
@@ -1,4 +1,4 @@
|
||||
.chat-analytics-hub-page {
|
||||
.analytics-hub {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -6,118 +6,125 @@
|
||||
padding: 40px 24px;
|
||||
}
|
||||
|
||||
.chat-analytics-hub-content {
|
||||
width: min(860px, 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
.analytics-hub-inner {
|
||||
width: min(560px, 100%);
|
||||
animation: analyticsHubFadeIn 0.35s ease-out;
|
||||
}
|
||||
|
||||
.chat-analytics-hub-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
// ---- Hero ----
|
||||
.analytics-hub-hero {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.chat-analytics-hub-content h1 {
|
||||
margin: 20px 0 12px;
|
||||
font-size: 32px;
|
||||
line-height: 1.2;
|
||||
.analytics-hub-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 10px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.chat-analytics-hub-desc {
|
||||
max-width: 620px;
|
||||
margin: 0 0 32px;
|
||||
.analytics-hub-desc {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.chat-analytics-hub-grid {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.chat-analytics-entry-card {
|
||||
// ---- Perspectives list ----
|
||||
.analytics-hub-perspectives {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.analytics-hub-perspectives-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 0 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.analytics-hub-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-height: 260px;
|
||||
padding: 28px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 20px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(7, 193, 96, 0.08) 0%, rgba(7, 193, 96, 0.02) 100%),
|
||||
var(--card-bg);
|
||||
padding: 14px 16px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
text-align: left;
|
||||
transition: background 0.15s ease;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: rgba(7, 193, 96, 0.35);
|
||||
box-shadow: 0 20px 36px rgba(7, 193, 96, 0.12);
|
||||
}
|
||||
background: var(--bg-hover);
|
||||
|
||||
.entry-card-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(7, 193, 96, 0.12);
|
||||
color: #07c160;
|
||||
|
||||
&.group {
|
||||
background: rgba(24, 119, 242, 0.12);
|
||||
color: #1877f2;
|
||||
.analytics-hub-row-arrow {
|
||||
transform: translateX(3px);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.entry-card-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.analytics-hub-row-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.entry-card-cta {
|
||||
margin-top: auto;
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
&--group {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.chat-analytics-hub-grid {
|
||||
grid-template-columns: 1fr;
|
||||
[data-mode="dark"] .analytics-hub-row-icon--group {
|
||||
background: rgba(96, 165, 250, 0.12);
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.analytics-hub-row-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.analytics-hub-row-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.analytics-hub-row-desc {
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.analytics-hub-row-arrow {
|
||||
color: var(--text-tertiary);
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
@keyframes analyticsHubFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArrowRight, BarChart3, MessageSquare, Users } from 'lucide-react'
|
||||
import { ArrowRight, MessageSquare, Users } from 'lucide-react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import './ChatAnalyticsHubPage.scss'
|
||||
|
||||
@@ -6,49 +6,46 @@ function ChatAnalyticsHubPage() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className="chat-analytics-hub-page">
|
||||
<div className="chat-analytics-hub-content">
|
||||
<div className="chat-analytics-hub-badge">
|
||||
<BarChart3 size={16} />
|
||||
<span>聊天分析</span>
|
||||
<div className="analytics-hub">
|
||||
<div className="analytics-hub-inner">
|
||||
<div className="analytics-hub-hero">
|
||||
<h1 className="analytics-hub-title">聊天分析</h1>
|
||||
<p className="analytics-hub-desc">
|
||||
选择你要进入的分析视角,深入了解关系网络、活跃时段与消息趋势。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h1>选择你要进入的分析视角</h1>
|
||||
<p className="chat-analytics-hub-desc">
|
||||
私聊分析更适合看好友聊天统计和趋势,群聊分析则用于查看群成员、发言排行和活跃时段。
|
||||
</p>
|
||||
<div className="analytics-hub-perspectives">
|
||||
<div className="analytics-hub-perspectives-label">视角</div>
|
||||
|
||||
<div className="chat-analytics-hub-grid">
|
||||
<button
|
||||
type="button"
|
||||
className="chat-analytics-entry-card"
|
||||
className="analytics-hub-row"
|
||||
onClick={() => navigate('/analytics/private')}
|
||||
>
|
||||
<div className="entry-card-icon">
|
||||
<MessageSquare size={24} />
|
||||
<div className="analytics-hub-row-icon">
|
||||
<MessageSquare size={18} />
|
||||
</div>
|
||||
<div className="entry-card-header">
|
||||
<h2>私聊分析</h2>
|
||||
<ArrowRight size={18} />
|
||||
<div className="analytics-hub-row-body">
|
||||
<div className="analytics-hub-row-title">私聊分析</div>
|
||||
<div className="analytics-hub-row-desc">查看好友聊天统计、消息趋势、活跃时段与联系人排名。</div>
|
||||
</div>
|
||||
<p>查看好友聊天统计、消息趋势、活跃时段与联系人排名。</p>
|
||||
<span className="entry-card-cta">进入私聊分析</span>
|
||||
<ArrowRight size={16} className="analytics-hub-row-arrow" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="chat-analytics-entry-card"
|
||||
className="analytics-hub-row"
|
||||
onClick={() => navigate('/analytics/group')}
|
||||
>
|
||||
<div className="entry-card-icon group">
|
||||
<Users size={24} />
|
||||
<div className="analytics-hub-row-icon analytics-hub-row-icon--group">
|
||||
<Users size={18} />
|
||||
</div>
|
||||
<div className="entry-card-header">
|
||||
<h2>群聊分析</h2>
|
||||
<ArrowRight size={18} />
|
||||
<div className="analytics-hub-row-body">
|
||||
<div className="analytics-hub-row-title">群聊分析</div>
|
||||
<div className="analytics-hub-row-desc">查看群成员信息、发言排行、活跃时段和媒体内容统计。</div>
|
||||
</div>
|
||||
<p>查看群成员信息、发言排行、活跃时段和媒体内容统计。</p>
|
||||
<span className="entry-card-cta">进入群聊分析</span>
|
||||
<ArrowRight size={16} className="analytics-hub-row-arrow" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--bg-primary) 96%, white) 0%, var(--bg-primary) 100%);
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-primary);
|
||||
|
||||
.history-list {
|
||||
flex: 1;
|
||||
@@ -149,7 +149,7 @@
|
||||
.nested-chat-record-card {
|
||||
min-width: 220px;
|
||||
max-width: 320px;
|
||||
background: color-mix(in srgb, var(--bg-secondary) 97%, #f5f7fb);
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.chat-page {
|
||||
.chat-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
@@ -666,7 +666,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.emoji {
|
||||
&.emoji,
|
||||
&.image,
|
||||
&.video {
|
||||
.bubble-content {
|
||||
background: transparent !important;
|
||||
padding: 0;
|
||||
@@ -2012,6 +2014,15 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.voice {
|
||||
.bubble-content {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bubble-avatar {
|
||||
@@ -2067,7 +2078,12 @@
|
||||
.message-bubble .bubble-content:has(> .solitaire-message),
|
||||
.message-bubble .bubble-content:has(> .official-message),
|
||||
.message-bubble .bubble-content:has(> .channel-video-card),
|
||||
.message-bubble .bubble-content:has(> .location-message) {
|
||||
.message-bubble .bubble-content:has(> .location-message),
|
||||
.message-bubble .bubble-content:has(> .voice-stack),
|
||||
.message-bubble .bubble-content:has(> .video-thumb-wrapper),
|
||||
.message-bubble .bubble-content:has(> .video-placeholder),
|
||||
.message-bubble .bubble-content:has(> .video-loading),
|
||||
.message-bubble .bubble-content:has(> .video-unavailable) {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
@@ -2448,13 +2464,221 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 引用消息样式
|
||||
// ═══════════════════════════════════════════
|
||||
// Ambient Reply System — "Spectral Thread"
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
// Wrapper for the entire ambient reply UI
|
||||
.ambient-reply-wrapper {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
margin-bottom: 4px;
|
||||
|
||||
// Reply anchor — always visible, minimal
|
||||
.reply-anchor {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 6px 2px 4px;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
opacity: 0.68;
|
||||
transition: opacity 0.35s ease, background-color 0.35s ease;
|
||||
user-select: none;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&.jumpable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--primary) 45%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .reply-anchor {
|
||||
opacity: 1;
|
||||
background: color-mix(in srgb, var(--primary) 8%, transparent);
|
||||
}
|
||||
|
||||
.reply-anchor-icon {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
color: var(--primary);
|
||||
opacity: 0.86;
|
||||
flex-shrink: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover .reply-anchor-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.reply-anchor-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--primary);
|
||||
letter-spacing: 0.01em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reply-anchor-sep {
|
||||
font-size: 10px;
|
||||
color: var(--text-tertiary);
|
||||
margin: 0 1px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reply-anchor-excerpt {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
max-width: 200px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
opacity: 0.92;
|
||||
|
||||
// Hide inline emoji images in anchor excerpt
|
||||
.inline-emoji {
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Ghost preview — appears on hover, frosted glass
|
||||
&.preview-below {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 0;
|
||||
|
||||
.reply-ghost {
|
||||
top: calc(100% + 6px);
|
||||
bottom: auto;
|
||||
transform: translateY(-4px) scale(0.98);
|
||||
}
|
||||
|
||||
&:hover .reply-ghost {
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
&.preview-above {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.reply-ghost {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
max-width: 320px;
|
||||
min-width: 160px;
|
||||
padding: 8px 12px 12px 12px;
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--bg-primary) 75%, transparent);
|
||||
backdrop-filter: blur(20px) saturate(1.3);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.3);
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
opacity: 0;
|
||||
transform: translateY(4px) scale(0.98);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.35s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
z-index: 50;
|
||||
|
||||
// Gradient accent bar on left edge
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
width: 2.5px;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(to bottom, color-mix(in srgb, var(--primary) 40%, transparent), transparent);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .reply-ghost {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.reply-ghost-sender {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--primary);
|
||||
opacity: 0.75;
|
||||
margin-bottom: 3px;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.reply-ghost-text {
|
||||
font-size: 12.5px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.94;
|
||||
padding-left: 6px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
|
||||
.inline-emoji {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sent message — adjust ghost position to right
|
||||
.message-bubble.sent .ambient-reply-wrapper {
|
||||
.reply-ghost {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.reply-anchor-name {
|
||||
color: color-mix(in srgb, var(--on-primary) 96%, var(--primary));
|
||||
}
|
||||
|
||||
.reply-anchor-excerpt {
|
||||
color: color-mix(in srgb, var(--on-primary) 86%, var(--primary));
|
||||
}
|
||||
|
||||
.reply-anchor-sep {
|
||||
color: color-mix(in srgb, var(--on-primary) 68%, var(--primary));
|
||||
}
|
||||
|
||||
.reply-anchor-icon {
|
||||
color: color-mix(in srgb, var(--on-primary) 90%, var(--primary));
|
||||
}
|
||||
|
||||
&:hover .reply-anchor {
|
||||
background: color-mix(in srgb, var(--on-primary) 10%, transparent);
|
||||
}
|
||||
|
||||
.reply-ghost {
|
||||
background: color-mix(in srgb, var(--bg-primary) 80%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
// Received message — ghost to left
|
||||
.message-bubble.received .ambient-reply-wrapper {
|
||||
.reply-ghost {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
// Legacy .quoted-message — used by SettingsPage quote-layout preview widget
|
||||
.quoted-message {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border-left: 2px solid var(--primary);
|
||||
padding: 6px 10px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
|
||||
.quoted-sender {
|
||||
color: var(--primary);
|
||||
@@ -2468,38 +2692,9 @@
|
||||
|
||||
.quoted-text {
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
|
||||
.quoted-type-label {
|
||||
font-style: italic;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.quoted-emoji-image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
vertical-align: middle;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自己发送的消息中的引用样式
|
||||
.message-bubble.sent .quoted-message {
|
||||
background: color-mix(in srgb, var(--on-primary) 12%, var(--primary));
|
||||
border-left-color: color-mix(in srgb, var(--on-primary) 36%, var(--primary));
|
||||
|
||||
.quoted-sender {
|
||||
color: color-mix(in srgb, var(--on-primary) 92%, var(--primary));
|
||||
}
|
||||
|
||||
.quoted-text {
|
||||
color: color-mix(in srgb, var(--on-primary) 80%, var(--primary));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 气泡内容区域(包含名字和内容)
|
||||
.bubble-body {
|
||||
display: flex;
|
||||
@@ -2514,14 +2709,6 @@
|
||||
|
||||
.bubble-content {
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&.quote-layout-top .quoted-message {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&.quote-layout-bottom .quoted-message {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 时间分隔
|
||||
@@ -2730,13 +2917,9 @@
|
||||
width: clamp(280px, 25vw, 360px);
|
||||
min-width: 280px;
|
||||
max-width: 360px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--card-bg) 94%, #fff 6%) 0%,
|
||||
var(--card-bg) 100%
|
||||
);
|
||||
background: var(--bg-primary);
|
||||
border-left: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent);
|
||||
box-shadow: -14px 0 28px rgba(0, 0, 0, 0.07);
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -2749,7 +2932,7 @@
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 14px 14px 12px;
|
||||
background: color-mix(in srgb, var(--card-bg) 92%, #fff 8%);
|
||||
background: var(--bg-primary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@@ -2850,7 +3033,7 @@
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-secondary) 84%, transparent);
|
||||
background: var(--bg-secondary);
|
||||
animation: detailCardEnter 0.24s ease both;
|
||||
|
||||
.detail-overview-avatar {
|
||||
@@ -2891,7 +3074,7 @@
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-secondary) 86%, transparent);
|
||||
background: var(--bg-secondary);
|
||||
animation: detailCardEnter 0.24s ease both;
|
||||
|
||||
.section-title {
|
||||
@@ -2917,7 +3100,7 @@
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
background: color-mix(in srgb, var(--card-bg) 84%, transparent);
|
||||
background: var(--card-bg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5301,3 +5484,509 @@
|
||||
.in-session-search-btn.active {
|
||||
color: var(--accent-color, #07c160);
|
||||
}
|
||||
|
||||
// Modern chat surface overrides for the refactored Chat components.
|
||||
.chat-page {
|
||||
gap: 0;
|
||||
background: var(--bg-sidebar);
|
||||
|
||||
&:not(.standalone) {
|
||||
.message-area {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.session-sidebar {
|
||||
background: var(--bg-sidebar);
|
||||
border-right: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--text-tertiary) 18%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.message-area {
|
||||
background: var(--bg-primary);
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.chat-page.standalone {
|
||||
background: var(--bg-primary);
|
||||
|
||||
.session-sidebar {
|
||||
background: var(--bg-sidebar);
|
||||
border-right: 0;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.message-area {
|
||||
background: var(--bg-primary);
|
||||
|
||||
.message-header {
|
||||
min-height: 64px;
|
||||
padding: 12px 22px;
|
||||
border-bottom: 0;
|
||||
background: color-mix(in srgb, var(--bg-primary) 88%, transparent);
|
||||
backdrop-filter: blur(14px);
|
||||
box-shadow: 0 1px 0 color-mix(in srgb, var(--border-color) 70%, transparent);
|
||||
|
||||
.session-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.header-actions .icon-btn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-list {
|
||||
background: var(--bg-primary);
|
||||
padding: 18px clamp(16px, 3vw, 48px) 28px;
|
||||
padding-bottom: calc(28px + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-header {
|
||||
min-height: 64px;
|
||||
padding: 12px 22px;
|
||||
border-bottom: 0;
|
||||
background: color-mix(in srgb, var(--bg-primary) 88%, transparent);
|
||||
backdrop-filter: blur(14px);
|
||||
box-shadow: 0 1px 0 color-mix(in srgb, var(--border-color) 70%, transparent);
|
||||
|
||||
.session-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
min-width: 0;
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.25;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-content-wrapper {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.message-list {
|
||||
background: var(--bg-primary);
|
||||
padding: 18px clamp(16px, 3vw, 48px) 28px;
|
||||
padding-bottom: calc(28px + env(safe-area-inset-bottom));
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.message-wrapper {
|
||||
padding-bottom: 14px;
|
||||
|
||||
&.sent {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&.received {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.message-wrapper-with-selection {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
cursor: default;
|
||||
|
||||
&[data-sent="true"] {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&[data-sent="false"] {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&.selectable {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-selection-checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 6px;
|
||||
border: 1.5px solid color-mix(in srgb, var(--text-tertiary) 55%, transparent);
|
||||
background: transparent;
|
||||
color: var(--on-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 9px;
|
||||
|
||||
&.left {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
&.right {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
gap: 10px;
|
||||
max-width: min(76%, 760px);
|
||||
|
||||
.bubble-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.bubble-body {
|
||||
min-width: 0;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.bubble-content {
|
||||
border-radius: 18px;
|
||||
padding: 10px 14px;
|
||||
line-height: 1.56;
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&.sent {
|
||||
.bubble-content {
|
||||
background: var(--primary);
|
||||
color: var(--on-primary);
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.bubble-body {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
&.received {
|
||||
.bubble-content {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border-radius: 18px;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.bubble-body {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
&.system {
|
||||
max-width: min(86%, 680px);
|
||||
|
||||
.bubble-avatar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bubble-content {
|
||||
background: color-mix(in srgb, var(--bg-tertiary) 70%, transparent);
|
||||
color: var(--text-tertiary);
|
||||
border-radius: 999px;
|
||||
padding: 6px 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&.emoji,
|
||||
&.image,
|
||||
&.video {
|
||||
max-width: min(82%, 760px);
|
||||
|
||||
.bubble-content {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.voice.sent .bubble-content {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.voice .bubble-content {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.message-bubble .bubble-content:has(> .link-message),
|
||||
.message-bubble .bubble-content:has(> .card-message),
|
||||
.message-bubble .bubble-content:has(> .chat-record-message),
|
||||
.message-bubble .bubble-content:has(> .solitaire-message),
|
||||
.message-bubble .bubble-content:has(> .official-message),
|
||||
.message-bubble .bubble-content:has(> .channel-video-card),
|
||||
.message-bubble .bubble-content:has(> .location-message),
|
||||
.message-bubble .bubble-content:has(> .hongbao-message),
|
||||
.message-bubble .bubble-content:has(> .transfer-message),
|
||||
.message-bubble .bubble-content:has(> .gift-message),
|
||||
.message-bubble .bubble-content:has(> .miniapp-message),
|
||||
.message-bubble .bubble-content:has(> .file-message),
|
||||
.message-bubble .bubble-content:has(> .voice-stack),
|
||||
.message-bubble .bubble-content:has(> .video-thumb-wrapper),
|
||||
.message-bubble .bubble-content:has(> .video-placeholder),
|
||||
.message-bubble .bubble-content:has(> .video-loading),
|
||||
.message-bubble .bubble-content:has(> .video-unavailable) {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.sender-name {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
// Ambient Reply dark mode / alternate adjustments handled via CSS variables
|
||||
|
||||
.link-message,
|
||||
.card-message,
|
||||
.chat-record-message,
|
||||
.solitaire-message,
|
||||
.official-message,
|
||||
.channel-video-card,
|
||||
.location-message,
|
||||
.miniapp-message,
|
||||
.file-message {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.link-message,
|
||||
.appmsg-rich-card {
|
||||
width: min(320px, calc(100vw - 112px));
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: color-mix(in srgb, var(--primary) 35%, var(--border-color));
|
||||
}
|
||||
|
||||
.link-thumb,
|
||||
.link-thumb-placeholder {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-bubble.sent .link-message,
|
||||
.message-bubble.sent .card-message,
|
||||
.message-bubble.sent .miniapp-message,
|
||||
.message-bubble.sent .appmsg-rich-card {
|
||||
background: var(--card-bg);
|
||||
border-color: var(--border-color);
|
||||
|
||||
.card-name,
|
||||
.miniapp-title,
|
||||
.link-title {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-label,
|
||||
.miniapp-label,
|
||||
.link-desc,
|
||||
.appmsg-url-line {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.hongbao-message,
|
||||
.transfer-message,
|
||||
.gift-message {
|
||||
width: min(280px, calc(100vw - 112px));
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 14px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.hongbao-message,
|
||||
.transfer-message {
|
||||
padding: 14px;
|
||||
|
||||
.hongbao-icon,
|
||||
.transfer-icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hongbao-icon {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.transfer-icon {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.hongbao-info,
|
||||
.transfer-info {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.hongbao-greeting,
|
||||
.transfer-amount {
|
||||
color: var(--text-primary);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.hongbao-label,
|
||||
.transfer-desc,
|
||||
.transfer-memo,
|
||||
.transfer-label {
|
||||
color: var(--text-secondary);
|
||||
opacity: 1;
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.transfer-message.received .transfer-icon {
|
||||
background: #64748b;
|
||||
}
|
||||
|
||||
.gift-message {
|
||||
padding: 12px;
|
||||
|
||||
.gift-info {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.gift-wish,
|
||||
.gift-price {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.gift-label {
|
||||
color: var(--text-secondary);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
[data-mode="dark"] {
|
||||
.hongbao-message,
|
||||
.transfer-message,
|
||||
.gift-message {
|
||||
background: var(--card-bg);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.message-bubble.received .bubble-content,
|
||||
.message-bubble.voice.sent .bubble-content {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-to-bottom {
|
||||
bottom: 20px;
|
||||
background: color-mix(in srgb, var(--bg-primary) 92%, transparent);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: var(--shadow-sm);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.message-header {
|
||||
padding-inline: 14px;
|
||||
|
||||
.header-actions {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-list {
|
||||
padding-inline: 14px;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
max-width: 88%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
onSingleExportDialogStatus,
|
||||
requestExportSessionStatus
|
||||
} from '../services/exportBridge'
|
||||
import ChatHeader from './Chat/ChatHeader'
|
||||
import ChatMessageBubble from './Chat/ChatMessageBubble'
|
||||
import '../styles/batchTranscribe.scss'
|
||||
import './ChatPage.scss'
|
||||
|
||||
@@ -53,6 +55,17 @@ interface PendingFootprintJumpPayload {
|
||||
createTime: number
|
||||
}
|
||||
|
||||
interface QuotedMessageJumpTarget {
|
||||
sourceMessageKey: string
|
||||
sourceCreateTime: number
|
||||
sessionId: string
|
||||
localId?: number
|
||||
serverId?: string
|
||||
createTime?: number
|
||||
senderUsername?: string
|
||||
content?: string
|
||||
}
|
||||
|
||||
type GlobalMsgSearchPhase = 'idle' | 'seed' | 'backfill' | 'done'
|
||||
type GlobalMsgSearchResult = Message & { sessionId: string }
|
||||
|
||||
@@ -62,7 +75,7 @@ interface GlobalMsgPrefixCacheEntry {
|
||||
completed: boolean
|
||||
}
|
||||
|
||||
type QuoteLayout = configService.QuoteLayout
|
||||
|
||||
|
||||
const GLOBAL_MSG_PER_SESSION_LIMIT = 10
|
||||
const GLOBAL_MSG_SEED_LIMIT = 120
|
||||
@@ -674,6 +687,26 @@ function cleanMessageContent(content: string): string {
|
||||
return content.trim()
|
||||
}
|
||||
|
||||
function normalizeMessageIdToken(value: unknown): string {
|
||||
const raw = String(value ?? '').trim()
|
||||
if (!raw) return ''
|
||||
if (!/^\d+$/.test(raw)) return raw
|
||||
return raw.replace(/^0+(?=\d)/, '')
|
||||
}
|
||||
|
||||
function parsePositiveInteger(value: unknown): number | undefined {
|
||||
const raw = String(value ?? '').trim()
|
||||
if (!raw) return undefined
|
||||
const parsed = Number(raw)
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return undefined
|
||||
return Math.floor(parsed)
|
||||
}
|
||||
|
||||
function normalizeQuotedComparableText(value: unknown): string {
|
||||
const text = cleanMessageContent(String(value ?? '')).replace(/\s+/g, ' ').trim()
|
||||
return text.length > 160 ? text.slice(0, 160) : text
|
||||
}
|
||||
|
||||
const CHAT_SESSION_LIST_CACHE_TTL_MS = 24 * 60 * 60 * 1000
|
||||
const CHAT_SESSION_PREVIEW_CACHE_TTL_MS = 24 * 60 * 60 * 1000
|
||||
const CHAT_SESSION_PREVIEW_LIMIT_PER_SESSION = 30
|
||||
@@ -1112,6 +1145,15 @@ interface LoadMessagesOptions {
|
||||
inSessionJumpRequestSeq?: number
|
||||
}
|
||||
|
||||
type LoadMessagesFn = (
|
||||
sessionId: string,
|
||||
offset?: number,
|
||||
startTime?: number,
|
||||
endTime?: number,
|
||||
ascending?: boolean,
|
||||
options?: LoadMessagesOptions
|
||||
) => Promise<void>
|
||||
|
||||
// 全局头像加载队列管理器已移至 src/utils/AvatarLoadQueue.ts
|
||||
import { avatarLoadQueue } from '../utils/AvatarLoadQueue'
|
||||
import { Avatar } from '../components/Avatar'
|
||||
@@ -1468,6 +1510,7 @@ function ChatPage(props: ChatPageProps) {
|
||||
const [groupMemberSearchKeyword, setGroupMemberSearchKeyword] = useState('')
|
||||
const [copiedField, setCopiedField] = useState<string | null>(null)
|
||||
const [highlightedMessageKeys, setHighlightedMessageKeys] = useState<string[]>([])
|
||||
const [quoteLayout, setQuoteLayout] = useState<configService.QuoteLayout>('quote-top')
|
||||
const [isRefreshingSessions, setIsRefreshingSessions] = useState(false)
|
||||
const [foldedView, setFoldedView] = useState(false) // 是否在"折叠的群聊"视图
|
||||
const [bizView, setBizView] = useState(false) // 是否在"公众号"视图
|
||||
@@ -1569,6 +1612,8 @@ function ChatPage(props: ChatPageProps) {
|
||||
const [globalMsgSearchError, setGlobalMsgSearchError] = useState<string | null>(null)
|
||||
const pendingInSessionSearchRef = useRef<PendingInSessionSearchPayload | null>(null)
|
||||
const pendingFootprintJumpRef = useRef<PendingFootprintJumpPayload | null>(null)
|
||||
const pendingQuotedMessageJumpRef = useRef<QuotedMessageJumpTarget | null>(null)
|
||||
const loadMessagesRef = useRef<LoadMessagesFn | null>(null)
|
||||
const pendingGlobalMsgSearchReplayRef = useRef<string | null>(null)
|
||||
const globalMsgPrefixCacheRef = useRef<GlobalMsgPrefixCacheEntry | null>(null)
|
||||
|
||||
@@ -3022,6 +3067,33 @@ function ChatPage(props: ChatPageProps) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false
|
||||
const loadQuoteLayout = () => {
|
||||
void configService.getQuoteLayout()
|
||||
.then((layout) => {
|
||||
if (!canceled) setQuoteLayout(layout)
|
||||
})
|
||||
.catch(() => {
|
||||
if (!canceled) setQuoteLayout('quote-top')
|
||||
})
|
||||
}
|
||||
|
||||
loadQuoteLayout()
|
||||
const handleFocus = () => loadQuoteLayout()
|
||||
const handleQuoteLayoutChanged = (event: Event) => {
|
||||
const layout = (event as CustomEvent<configService.QuoteLayout>).detail
|
||||
setQuoteLayout(layout === 'quote-bottom' ? 'quote-bottom' : 'quote-top')
|
||||
}
|
||||
window.addEventListener('focus', handleFocus)
|
||||
window.addEventListener('quote-layout-changed', handleQuoteLayoutChanged)
|
||||
return () => {
|
||||
canceled = true
|
||||
window.removeEventListener('focus', handleFocus)
|
||||
window.removeEventListener('quote-layout-changed', handleQuoteLayoutChanged)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
void (async () => {
|
||||
@@ -3740,6 +3812,8 @@ function ChatPage(props: ChatPageProps) {
|
||||
}
|
||||
}
|
||||
|
||||
loadMessagesRef.current = loadMessages
|
||||
|
||||
const handleJumpDateSelect = useCallback((date: Date, options: { sessionId?: string; switchRequestSeq?: number } = {}) => {
|
||||
const targetSessionId = String(options.sessionId || currentSessionRef.current || currentSessionId || '').trim()
|
||||
if (!targetSessionId) return
|
||||
@@ -4849,6 +4923,136 @@ function ChatPage(props: ChatPageProps) {
|
||||
}, 2500)
|
||||
}, [])
|
||||
|
||||
const findQuotedTargetInMessages = useCallback((target: QuotedMessageJumpTarget): { index: number; message: Message } | null => {
|
||||
if (messages.length === 0) return null
|
||||
|
||||
const targetServerId = normalizeMessageIdToken(target.serverId)
|
||||
const targetLocalId = typeof target.localId === 'number' && target.localId > 0 ? target.localId : undefined
|
||||
const targetCreateTime = typeof target.createTime === 'number' && target.createTime > 0 ? target.createTime : undefined
|
||||
const targetSender = String(target.senderUsername || '').trim()
|
||||
const targetContent = normalizeQuotedComparableText(target.content)
|
||||
const sourceIndex = target.sourceMessageKey
|
||||
? messages.findIndex((item) => getMessageKey(item) === target.sourceMessageKey)
|
||||
: -1
|
||||
|
||||
const orderedIndices: number[] = []
|
||||
const usedIndices = new Set<number>()
|
||||
const pushIndex = (index: number) => {
|
||||
if (index < 0 || index >= messages.length || usedIndices.has(index)) return
|
||||
usedIndices.add(index)
|
||||
orderedIndices.push(index)
|
||||
}
|
||||
|
||||
if (sourceIndex > 0) {
|
||||
for (let index = sourceIndex - 1; index >= 0; index--) {
|
||||
pushIndex(index)
|
||||
}
|
||||
}
|
||||
for (let index = 0; index < messages.length; index++) {
|
||||
pushIndex(index)
|
||||
}
|
||||
|
||||
let best: { index: number; message: Message; score: number } | null = null
|
||||
for (const index of orderedIndices) {
|
||||
const item = messages[index]
|
||||
const itemKey = getMessageKey(item)
|
||||
if (itemKey === target.sourceMessageKey) continue
|
||||
|
||||
const itemServerId = normalizeMessageIdToken(item.serverIdRaw ?? item.serverId)
|
||||
const serverMatch = Boolean(targetServerId && itemServerId && itemServerId === targetServerId)
|
||||
const localIdMatch = Boolean(targetLocalId && Number(item.localId || 0) === targetLocalId)
|
||||
const itemCreateTime = Number(item.createTime || 0)
|
||||
const timeDelta = targetCreateTime ? Math.abs(itemCreateTime - targetCreateTime) : Number.POSITIVE_INFINITY
|
||||
const exactTimeMatch = Boolean(targetCreateTime && timeDelta <= 1)
|
||||
const nearTimeMatch = Boolean(targetCreateTime && timeDelta <= 300)
|
||||
const senderMatch = Boolean(targetSender && String(item.senderUsername || '').trim() === targetSender)
|
||||
const itemText = targetContent
|
||||
? normalizeQuotedComparableText(item.parsedContent || item.rawContent || item.content || '')
|
||||
: ''
|
||||
const contentMatch = Boolean(
|
||||
targetContent &&
|
||||
itemText &&
|
||||
(itemText.includes(targetContent) || targetContent.includes(itemText))
|
||||
)
|
||||
|
||||
const strongMatch = Boolean(
|
||||
serverMatch ||
|
||||
localIdMatch ||
|
||||
(exactTimeMatch && (senderMatch || contentMatch)) ||
|
||||
(exactTimeMatch && !targetSender && !targetContent)
|
||||
)
|
||||
if (!strongMatch) continue
|
||||
|
||||
const score =
|
||||
(localIdMatch ? 100 : 0) +
|
||||
(serverMatch ? 90 : 0) +
|
||||
(exactTimeMatch ? 35 : (nearTimeMatch ? 8 : 0)) +
|
||||
(senderMatch ? 12 : 0) +
|
||||
(contentMatch ? 12 : 0)
|
||||
|
||||
if (!best || score > best.score) {
|
||||
best = { index, message: item, score }
|
||||
if (score >= 125) break
|
||||
}
|
||||
}
|
||||
|
||||
return best ? { index: best.index, message: best.message } : null
|
||||
}, [messages, getMessageKey])
|
||||
|
||||
const scrollToResolvedMessage = useCallback((resolved: { index: number; message: Message }, behavior: 'auto' | 'smooth' = 'smooth') => {
|
||||
const key = getMessageKey(resolved.message)
|
||||
flashNewMessages([key])
|
||||
requestAnimationFrame(() => {
|
||||
if (messageVirtuosoRef.current) {
|
||||
messageVirtuosoRef.current.scrollToIndex({
|
||||
index: resolved.index,
|
||||
align: 'center',
|
||||
behavior
|
||||
})
|
||||
}
|
||||
})
|
||||
}, [flashNewMessages, getMessageKey])
|
||||
|
||||
const handleJumpToQuotedMessage = useCallback((target: QuotedMessageJumpTarget) => {
|
||||
const targetSessionId = String(currentSessionRef.current || currentSessionId || target.sessionId || '').trim()
|
||||
if (!targetSessionId) return
|
||||
|
||||
const normalizedTarget: QuotedMessageJumpTarget = {
|
||||
...target,
|
||||
sessionId: targetSessionId
|
||||
}
|
||||
const resolved = findQuotedTargetInMessages(normalizedTarget)
|
||||
if (resolved) {
|
||||
pendingQuotedMessageJumpRef.current = null
|
||||
scrollToResolvedMessage(resolved)
|
||||
return
|
||||
}
|
||||
|
||||
pendingQuotedMessageJumpRef.current = normalizedTarget
|
||||
const targetTime = Number(normalizedTarget.createTime || 0)
|
||||
if (!targetTime) return
|
||||
|
||||
const requestSeq = inSessionResultJumpRequestSeqRef.current + 1
|
||||
inSessionResultJumpRequestSeqRef.current = requestSeq
|
||||
setCurrentOffset(0)
|
||||
setJumpStartTime(0)
|
||||
setJumpEndTime(targetTime + 1)
|
||||
suppressAutoLoadLaterRef.current = true
|
||||
void loadMessagesRef.current?.(targetSessionId, 0, 0, targetTime + 1, false, {
|
||||
forceInitialLimit: 120,
|
||||
inSessionJumpRequestSeq: requestSeq
|
||||
})
|
||||
}, [currentSessionId, findQuotedTargetInMessages, scrollToResolvedMessage])
|
||||
|
||||
useEffect(() => {
|
||||
const pending = pendingQuotedMessageJumpRef.current
|
||||
if (!pending) return
|
||||
const resolved = findQuotedTargetInMessages(pending)
|
||||
if (!resolved) return
|
||||
pendingQuotedMessageJumpRef.current = null
|
||||
scrollToResolvedMessage(resolved, 'auto')
|
||||
}, [messages, findQuotedTargetInMessages, scrollToResolvedMessage])
|
||||
|
||||
const handleInSessionResultJump = useCallback((msg: Message) => {
|
||||
const targetTime = Number(msg.createTime || 0)
|
||||
const targetSessionId = String(currentSessionRef.current || currentSessionId || '').trim()
|
||||
@@ -6644,9 +6848,11 @@ function ChatPage(props: ChatPageProps) {
|
||||
myAvatarUrl={myAvatarUrl}
|
||||
myWxid={myWxid}
|
||||
isGroupChat={isCurrentSessionGroup}
|
||||
quoteLayout={quoteLayout}
|
||||
autoTranscribeVoiceEnabled={autoTranscribeVoiceEnabled}
|
||||
onRequireModelDownload={handleRequireModelDownload}
|
||||
onContextMenu={handleContextMenu}
|
||||
onJumpToQuotedMessage={handleJumpToQuotedMessage}
|
||||
isSelectionMode={isSelectionMode}
|
||||
messageKey={messageKey}
|
||||
isSelected={selectedMessages.has(messageKey)}
|
||||
@@ -6663,9 +6869,11 @@ function ChatPage(props: ChatPageProps) {
|
||||
myAvatarUrl,
|
||||
myWxid,
|
||||
isCurrentSessionGroup,
|
||||
quoteLayout,
|
||||
autoTranscribeVoiceEnabled,
|
||||
handleRequireModelDownload,
|
||||
handleContextMenu,
|
||||
handleJumpToQuotedMessage,
|
||||
isSelectionMode,
|
||||
selectedMessages,
|
||||
handleToggleSelection
|
||||
@@ -6988,155 +7196,62 @@ function ChatPage(props: ChatPageProps) {
|
||||
<BizMessageArea account={selectedBizAccount} />
|
||||
) : currentSession ? (
|
||||
<>
|
||||
<div className="message-header">
|
||||
<Avatar
|
||||
src={currentSession.avatarUrl}
|
||||
name={currentSession.displayName || currentSession.username}
|
||||
size={40}
|
||||
className={isCurrentSessionGroup ? 'group session-avatar' : 'session-avatar'}
|
||||
<ChatHeader
|
||||
session={currentSession}
|
||||
isGroupChat={isCurrentSessionGroup}
|
||||
standaloneSessionWindow={standaloneSessionWindow}
|
||||
showGroupMembersPanel={showGroupMembersPanel}
|
||||
showJumpPopover={showJumpPopover}
|
||||
showInSessionSearch={showInSessionSearch}
|
||||
showDetailPanel={showDetailPanel}
|
||||
shouldHideStandaloneDetailButton={shouldHideStandaloneDetailButton}
|
||||
isPrivateSnsSupported={isCurrentSessionPrivateSnsSupported}
|
||||
isExportActionBusy={isExportActionBusy}
|
||||
isCurrentSessionExporting={isCurrentSessionExporting}
|
||||
isPreparingExportDialog={isPreparingExportDialog}
|
||||
isBatchTranscribing={isBatchTranscribing}
|
||||
runningBatchVoiceTaskType={runningBatchVoiceTaskType}
|
||||
isBatchDecrypting={isBatchDecrypting}
|
||||
isRefreshingMessages={isRefreshingMessages}
|
||||
isLoadingMessages={isLoadingMessages}
|
||||
currentSessionId={currentSessionId}
|
||||
jumpCalendarWrapRef={jumpCalendarWrapRef}
|
||||
onGroupAnalytics={handleGroupAnalytics}
|
||||
onToggleGroupMembersPanel={toggleGroupMembersPanel}
|
||||
onExportCurrentSession={handleExportCurrentSession}
|
||||
onOpenSnsTimeline={openCurrentSessionSnsTimeline}
|
||||
onBatchTranscribe={handleBatchTranscribe}
|
||||
onBatchDecrypt={handleBatchDecrypt}
|
||||
onToggleJumpPopover={handleToggleJumpPopover}
|
||||
onToggleInSessionSearch={handleToggleInSessionSearch}
|
||||
onRefreshMessages={handleRefreshMessages}
|
||||
onToggleDetailPanel={toggleDetailPanel}
|
||||
/>
|
||||
<div className="header-info">
|
||||
<h3>{currentSession.displayName || currentSession.username}</h3>
|
||||
{isCurrentSessionGroup && (
|
||||
<div className="header-subtitle">群聊</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
{!standaloneSessionWindow && isCurrentSessionGroup && (
|
||||
<button
|
||||
className="icon-btn group-analytics-btn"
|
||||
onClick={handleGroupAnalytics}
|
||||
title="群聊分析"
|
||||
>
|
||||
<BarChart3 size={18} />
|
||||
</button>
|
||||
)}
|
||||
{isCurrentSessionGroup && (
|
||||
<button
|
||||
className={`icon-btn group-members-btn ${showGroupMembersPanel ? 'active' : ''}`}
|
||||
onClick={toggleGroupMembersPanel}
|
||||
title="群成员"
|
||||
>
|
||||
<Users size={18} />
|
||||
</button>
|
||||
)}
|
||||
{!standaloneSessionWindow && (
|
||||
<button
|
||||
className={`icon-btn export-session-btn${isExportActionBusy ? ' exporting' : ''}`}
|
||||
onClick={handleExportCurrentSession}
|
||||
disabled={!currentSessionId || isExportActionBusy}
|
||||
title={isCurrentSessionExporting ? '导出中' : isPreparingExportDialog ? '正在准备导出模块' : '导出当前会话'}
|
||||
>
|
||||
{isExportActionBusy ? (
|
||||
<Loader2 size={18} className="spin" />
|
||||
) : (
|
||||
<Download size={18} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{!standaloneSessionWindow && isCurrentSessionPrivateSnsSupported && (
|
||||
<button
|
||||
className="icon-btn chat-sns-timeline-btn"
|
||||
onClick={openCurrentSessionSnsTimeline}
|
||||
disabled={!currentSessionId}
|
||||
title="查看对方朋友圈"
|
||||
>
|
||||
<Aperture size={18} />
|
||||
</button>
|
||||
)}
|
||||
{!standaloneSessionWindow && (
|
||||
<button
|
||||
className={`icon-btn batch-transcribe-btn${isBatchTranscribing ? ' transcribing' : ''}`}
|
||||
onClick={handleBatchTranscribe}
|
||||
disabled={!currentSessionId}
|
||||
title={isBatchTranscribing
|
||||
? `${runningBatchVoiceTaskType === 'decrypt' ? '批量语音解密' : '批量转写'}中,可在导出页任务中心查看进度`
|
||||
: '批量语音处理(解密/转文字)'}
|
||||
>
|
||||
{isBatchTranscribing ? (
|
||||
<Loader2 size={18} className="spin" />
|
||||
) : (
|
||||
<Mic size={18} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{!standaloneSessionWindow && (
|
||||
<button
|
||||
className={`icon-btn batch-decrypt-btn${isBatchDecrypting ? ' transcribing' : ''}`}
|
||||
onClick={handleBatchDecrypt}
|
||||
disabled={!currentSessionId}
|
||||
title={isBatchDecrypting
|
||||
? '批量解密中,可在导出页任务中心查看进度'
|
||||
: '批量解密图片'}
|
||||
>
|
||||
{isBatchDecrypting ? (
|
||||
<Loader2 size={18} className="spin" />
|
||||
) : (
|
||||
<ImageIcon size={18} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<div className="jump-calendar-anchor" ref={jumpCalendarWrapRef}>
|
||||
<button
|
||||
className={`icon-btn jump-to-time-btn ${showJumpPopover ? 'active' : ''}`}
|
||||
onClick={handleToggleJumpPopover}
|
||||
title="跳转到指定时间"
|
||||
>
|
||||
<Calendar size={18} />
|
||||
</button>
|
||||
</div>
|
||||
{showJumpPopover && createPortal(
|
||||
<div
|
||||
ref={jumpPopoverPortalRef}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: jumpPopoverPosition.top,
|
||||
left: jumpPopoverPosition.left,
|
||||
zIndex: 3600
|
||||
}}
|
||||
>
|
||||
<JumpToDatePopover
|
||||
isOpen={showJumpPopover}
|
||||
currentDate={jumpPopoverDate}
|
||||
onClose={() => setShowJumpPopover(false)}
|
||||
onSelect={handleJumpDateSelect}
|
||||
messageDates={messageDates}
|
||||
hasLoadedMessageDates={hasLoadedMessageDates}
|
||||
messageDateCounts={messageDateCounts}
|
||||
loadingDates={loadingDates}
|
||||
loadingDateCounts={loadingDateCounts}
|
||||
style={{ position: 'static', top: 'auto', right: 'auto' }}
|
||||
/>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
<button
|
||||
className={`icon-btn in-session-search-btn ${showInSessionSearch ? 'active' : ''}`}
|
||||
onClick={handleToggleInSessionSearch}
|
||||
disabled={!currentSessionId}
|
||||
title="搜索会话消息"
|
||||
{showJumpPopover && createPortal(
|
||||
<div
|
||||
ref={jumpPopoverPortalRef}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: jumpPopoverPosition.top,
|
||||
left: jumpPopoverPosition.left,
|
||||
zIndex: 3600
|
||||
}}
|
||||
>
|
||||
<Search size={18} />
|
||||
</button>
|
||||
<button
|
||||
className="icon-btn refresh-messages-btn"
|
||||
onClick={handleRefreshMessages}
|
||||
disabled={isRefreshingMessages || isLoadingMessages}
|
||||
title="刷新消息"
|
||||
>
|
||||
<RefreshCw size={18} className={isRefreshingMessages ? 'spin' : ''} />
|
||||
</button>
|
||||
{!shouldHideStandaloneDetailButton && (
|
||||
<button
|
||||
className={`icon-btn detail-btn ${showDetailPanel ? 'active' : ''}`}
|
||||
onClick={toggleDetailPanel}
|
||||
title="会话详情"
|
||||
>
|
||||
<Info size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<JumpToDatePopover
|
||||
isOpen={showJumpPopover}
|
||||
currentDate={jumpPopoverDate}
|
||||
onClose={() => setShowJumpPopover(false)}
|
||||
onSelect={handleJumpDateSelect}
|
||||
messageDates={messageDates}
|
||||
hasLoadedMessageDates={hasLoadedMessageDates}
|
||||
messageDateCounts={messageDateCounts}
|
||||
loadingDates={loadingDates}
|
||||
loadingDateCounts={loadingDateCounts}
|
||||
style={{ position: 'static', top: 'auto', right: 'auto' }}
|
||||
/>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{isPreparingExportDialog && exportPrepareHint && (
|
||||
<div className="export-prepare-hint" role="status" aria-live="polite">
|
||||
@@ -8310,9 +8425,11 @@ function MessageBubble({
|
||||
myAvatarUrl,
|
||||
myWxid,
|
||||
isGroupChat,
|
||||
quoteLayout,
|
||||
autoTranscribeVoiceEnabled,
|
||||
onRequireModelDownload,
|
||||
onContextMenu,
|
||||
onJumpToQuotedMessage,
|
||||
isSelectionMode,
|
||||
isSelected,
|
||||
onToggleSelection
|
||||
@@ -8324,9 +8441,11 @@ function MessageBubble({
|
||||
myAvatarUrl?: string;
|
||||
myWxid?: string;
|
||||
isGroupChat?: boolean;
|
||||
quoteLayout: configService.QuoteLayout;
|
||||
autoTranscribeVoiceEnabled?: boolean;
|
||||
onRequireModelDownload?: (sessionId: string, messageId: string) => void;
|
||||
onContextMenu?: (e: React.MouseEvent, message: Message) => void;
|
||||
onJumpToQuotedMessage?: (target: QuotedMessageJumpTarget) => void;
|
||||
isSelectionMode?: boolean;
|
||||
isSelected?: boolean;
|
||||
onToggleSelection?: (messageKey: string, isShiftKey?: boolean) => void;
|
||||
@@ -8343,7 +8462,6 @@ function MessageBubble({
|
||||
const [senderAvatarUrl, setSenderAvatarUrl] = useState<string | undefined>(undefined)
|
||||
const [senderName, setSenderName] = useState<string | undefined>(undefined)
|
||||
const [quotedSenderName, setQuotedSenderName] = useState<string | undefined>(undefined)
|
||||
const [quoteLayout, setQuoteLayout] = useState<QuoteLayout>('quote-top')
|
||||
const [solitaireExpanded, setSolitaireExpanded] = useState(false)
|
||||
const senderProfileRequestSeqRef = useRef(0)
|
||||
const [emojiError, setEmojiError] = useState(false)
|
||||
@@ -9492,17 +9610,7 @@ function MessageBubble({
|
||||
myWxid
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
void configService.getQuoteLayout().then((layout) => {
|
||||
if (!cancelled) setQuoteLayout(layout)
|
||||
}).catch(() => {
|
||||
if (!cancelled) setQuoteLayout('quote-top')
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
// quoteLayout config removed - Ambient Reply uses a single fixed layout
|
||||
|
||||
const locationMessageMeta = useMemo(() => {
|
||||
if (message.localType !== 48) return null
|
||||
@@ -9539,31 +9647,99 @@ function MessageBubble({
|
||||
// 是否有引用消息
|
||||
const hasQuote = quotedContent.length > 0
|
||||
const displayQuotedSenderName = quotedSenderName || quotedSenderFallbackName
|
||||
const renderBubbleWithQuote = useCallback((quotedNode: React.ReactNode, messageNode: React.ReactNode) => {
|
||||
const quoteFirst = quoteLayout !== 'quote-bottom'
|
||||
return (
|
||||
<div className={`bubble-content ${quoteFirst ? 'quote-layout-top' : 'quote-layout-bottom'}`}>
|
||||
{quoteFirst ? (
|
||||
<>
|
||||
{quotedNode}
|
||||
{messageNode}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{messageNode}
|
||||
{quotedNode}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}, [quoteLayout])
|
||||
const quotedJumpTarget = useMemo<QuotedMessageJumpTarget | null>(() => {
|
||||
if (!hasQuote) return null
|
||||
|
||||
const renderQuotedMessageBlock = useCallback((contentNode: React.ReactNode) => (
|
||||
<div className="quoted-message">
|
||||
{displayQuotedSenderName && <span className="quoted-sender">{displayQuotedSenderName}</span>}
|
||||
<span className="quoted-text">{contentNode}</span>
|
||||
const quotedServerId = normalizeMessageIdToken(
|
||||
queryAppMsgText('refermsg > svrid') ||
|
||||
queryAppMsgText('refermsg > msgsvrid') ||
|
||||
queryAppMsgText('refermsg > newmsgid') ||
|
||||
queryAppMsgText('refermsg > msgid')
|
||||
)
|
||||
const quotedCreateTime = parsePositiveInteger(
|
||||
queryAppMsgText('refermsg > createtime') ||
|
||||
queryAppMsgText('refermsg > create_time') ||
|
||||
queryAppMsgText('refermsg > createTime')
|
||||
)
|
||||
const quotedLocalId = parsePositiveInteger(
|
||||
queryAppMsgText('refermsg > localid') ||
|
||||
queryAppMsgText('refermsg > local_id') ||
|
||||
queryAppMsgText('refermsg > localId')
|
||||
)
|
||||
const normalizedQuotedContent = normalizeQuotedComparableText(quotedContent)
|
||||
|
||||
if (!quotedServerId && !quotedCreateTime && !quotedLocalId && !normalizedQuotedContent) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
sourceMessageKey: messageKey,
|
||||
sourceCreateTime: Number(message.createTime || 0),
|
||||
sessionId: session.username,
|
||||
localId: quotedLocalId,
|
||||
serverId: quotedServerId || undefined,
|
||||
createTime: quotedCreateTime,
|
||||
senderUsername: quotedSenderUsername || undefined,
|
||||
content: normalizedQuotedContent || undefined
|
||||
}
|
||||
}, [hasQuote, message.createTime, messageKey, queryAppMsgText, quotedContent, quotedSenderUsername, session.username])
|
||||
const handleQuotedJumpClick = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (isSelectionMode) return
|
||||
if (!quotedJumpTarget || !onJumpToQuotedMessage) return
|
||||
event.stopPropagation()
|
||||
onJumpToQuotedMessage(quotedJumpTarget)
|
||||
}, [isSelectionMode, onJumpToQuotedMessage, quotedJumpTarget])
|
||||
const handleQuotedJumpKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') return
|
||||
if (isSelectionMode) return
|
||||
if (!quotedJumpTarget || !onJumpToQuotedMessage) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
onJumpToQuotedMessage(quotedJumpTarget)
|
||||
}, [isSelectionMode, onJumpToQuotedMessage, quotedJumpTarget])
|
||||
const isQuoteBelow = quoteLayout === 'quote-bottom'
|
||||
const renderBubbleWithQuote = useCallback((quotedNode: React.ReactNode, messageNode: React.ReactNode) => (
|
||||
<div className={`bubble-content ${isQuoteBelow ? 'quote-layout-bottom' : 'quote-layout-top'}`}>
|
||||
{isQuoteBelow ? (
|
||||
<>
|
||||
{messageNode}
|
||||
{quotedNode}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{quotedNode}
|
||||
{messageNode}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
), [displayQuotedSenderName])
|
||||
), [isQuoteBelow])
|
||||
|
||||
// Ambient Reply: render reply-anchor + ghost preview
|
||||
const renderQuotedMessageBlock = useCallback((contentNode: React.ReactNode) => (
|
||||
<div className={`ambient-reply-wrapper ${isQuoteBelow ? 'preview-below' : 'preview-above'}`}>
|
||||
{/* Reply anchor - always visible, subtle */}
|
||||
<div
|
||||
className={`reply-anchor ${quotedJumpTarget ? 'jumpable' : ''}`}
|
||||
role={quotedJumpTarget && !isSelectionMode ? 'button' : undefined}
|
||||
tabIndex={quotedJumpTarget && !isSelectionMode ? 0 : undefined}
|
||||
onClick={handleQuotedJumpClick}
|
||||
onKeyDown={handleQuotedJumpKeyDown}
|
||||
>
|
||||
<svg className="reply-anchor-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="9 14 4 9 9 4" />
|
||||
<path d="M20 20v-7a4 4 0 0 0-4-4H4" />
|
||||
</svg>
|
||||
{displayQuotedSenderName && <span className="reply-anchor-name">{displayQuotedSenderName}</span>}
|
||||
<span className="reply-anchor-sep">·</span>
|
||||
<span className="reply-anchor-excerpt">{contentNode}</span>
|
||||
</div>
|
||||
{/* Ghost preview - appears on hover */}
|
||||
<div className="reply-ghost">
|
||||
{displayQuotedSenderName && <div className="reply-ghost-sender">{displayQuotedSenderName}</div>}
|
||||
<div className="reply-ghost-text">{contentNode}</div>
|
||||
</div>
|
||||
</div>
|
||||
), [displayQuotedSenderName, handleQuotedJumpClick, handleQuotedJumpKeyDown, isQuoteBelow, isSelectionMode, quotedJumpTarget])
|
||||
|
||||
const handlePlayVideo = useCallback(async () => {
|
||||
if (!videoInfo?.videoUrl) return
|
||||
@@ -10696,115 +10872,58 @@ function MessageBubble({
|
||||
return <div className="bubble-content">{renderTextWithEmoji(cleanedParsedContent)}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showTime && (
|
||||
<div className="time-divider">
|
||||
<span>{formatTime(message.createTime)}</span>
|
||||
const systemAlertPortal = systemAlert ? createPortal(
|
||||
<div className="modal-overlay" onClick={() => setSystemAlert(null)} style={{ zIndex: 99999 }}>
|
||||
<div className="delete-confirm-card" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '400px' }}>
|
||||
<div className="confirm-icon">
|
||||
<AlertCircle size={32} color="var(--danger)" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`message-wrapper-with-selection ${isSelectionMode ? 'selectable' : ''}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
width: '100%',
|
||||
justifyContent: isSent ? 'flex-end' : 'flex-start',
|
||||
cursor: isSelectionMode ? 'pointer' : 'default'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (isSelectionMode) {
|
||||
e.stopPropagation()
|
||||
onToggleSelection?.(messageKey, e.shiftKey)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isSelectionMode && !isSent && (
|
||||
<div className={`checkbox ${isSelected ? 'checked' : ''}`} style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
borderRadius: '4px',
|
||||
border: isSelected ? 'none' : '2px solid rgba(128,128,128,0.5)',
|
||||
backgroundColor: isSelected ? 'var(--primary)' : 'transparent',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white',
|
||||
marginRight: '12px',
|
||||
marginTop: '10px', // Align with avatar top
|
||||
flexShrink: 0
|
||||
}}>
|
||||
{isSelected && <Check size={14} strokeWidth={3} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`message-bubble ${bubbleClass} ${isEmoji && message.emojiCdnUrl && !emojiError ? 'emoji' : ''} ${isImage ? 'image' : ''} ${isVoice ? 'voice' : ''}`}
|
||||
onContextMenu={(e) => onContextMenu?.(e, message)}
|
||||
>
|
||||
<div className="bubble-avatar">
|
||||
<Avatar
|
||||
src={avatarUrl}
|
||||
name={!isSent ? (isGroupChat ? (resolvedSenderName || '?') : (session.displayName || session.username)) : '我'}
|
||||
size={36}
|
||||
className="bubble-avatar"
|
||||
/>
|
||||
</div>
|
||||
<div className="bubble-body">
|
||||
{/* 群聊中显示发送者名称 */}
|
||||
{isGroupChat && !isSent && (
|
||||
<div className="sender-name">
|
||||
{resolvedSenderName || '群成员'}
|
||||
</div>
|
||||
)}
|
||||
{renderContent()}
|
||||
</div>
|
||||
<div className="confirm-content">
|
||||
<h3>{systemAlert.title}</h3>
|
||||
<p style={{ marginTop: '12px', lineHeight: '1.6', fontSize: '14px', color: 'var(--text-secondary)' }}>
|
||||
{systemAlert.message}
|
||||
</p>
|
||||
</div>
|
||||
<div className="confirm-actions" style={{ justifyContent: 'center', marginTop: '24px' }}>
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={() => setSystemAlert(null)}
|
||||
style={{ padding: '8px 32px' }}
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isSelectionMode && isSent && (
|
||||
<div className={`checkbox ${isSelected ? 'checked' : ''}`} style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
borderRadius: '4px',
|
||||
border: isSelected ? 'none' : '2px solid rgba(128,128,128,0.5)',
|
||||
backgroundColor: isSelected ? 'var(--primary)' : 'transparent',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white',
|
||||
marginLeft: '12px',
|
||||
marginTop: '10px',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
{isSelected && <Check size={14} strokeWidth={3} />}
|
||||
</div>
|
||||
)}
|
||||
{systemAlert && createPortal(
|
||||
<div className="modal-overlay" onClick={() => setSystemAlert(null)} style={{ zIndex: 99999 }}>
|
||||
<div className="delete-confirm-card" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '400px' }}>
|
||||
<div className="confirm-icon">
|
||||
<AlertCircle size={32} color="var(--danger)" />
|
||||
</div>
|
||||
<div className="confirm-content">
|
||||
<h3>{systemAlert.title}</h3>
|
||||
<p style={{ marginTop: '12px', lineHeight: '1.6', fontSize: '14px', color: 'var(--text-secondary)' }}>
|
||||
{systemAlert.message}
|
||||
</p>
|
||||
</div>
|
||||
<div className="confirm-actions" style={{ justifyContent: 'center', marginTop: '24px' }}>
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={() => setSystemAlert(null)}
|
||||
style={{ padding: '8px 32px' }}
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>,
|
||||
document.body
|
||||
) : null
|
||||
|
||||
return (
|
||||
<ChatMessageBubble
|
||||
message={message}
|
||||
messageKey={messageKey}
|
||||
session={session}
|
||||
showTime={showTime}
|
||||
timeText={formatTime(message.createTime)}
|
||||
isSent={isSent}
|
||||
isSystem={isSystem}
|
||||
isEmoji={isEmoji}
|
||||
isImage={isImage}
|
||||
isVideo={isVideo}
|
||||
isVoice={isVoice}
|
||||
emojiHasAsset={Boolean(message.emojiCdnUrl || message.emojiLocalPath)}
|
||||
emojiError={emojiError}
|
||||
avatarUrl={avatarUrl}
|
||||
isGroupChat={isGroupChat}
|
||||
resolvedSenderName={resolvedSenderName}
|
||||
isSelectionMode={isSelectionMode}
|
||||
isSelected={isSelected}
|
||||
onContextMenu={onContextMenu}
|
||||
onToggleSelection={onToggleSelection}
|
||||
portal={systemAlertPortal}
|
||||
>
|
||||
{renderContent()}
|
||||
</ChatMessageBubble>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10815,11 +10934,13 @@ const MemoMessageBubble = React.memo(MessageBubble, (prevProps, nextProps) => {
|
||||
if (prevProps.myAvatarUrl !== nextProps.myAvatarUrl) return false
|
||||
if (prevProps.myWxid !== nextProps.myWxid) return false
|
||||
if (prevProps.isGroupChat !== nextProps.isGroupChat) return false
|
||||
if (prevProps.quoteLayout !== nextProps.quoteLayout) return false
|
||||
if (prevProps.autoTranscribeVoiceEnabled !== nextProps.autoTranscribeVoiceEnabled) return false
|
||||
if (prevProps.isSelectionMode !== nextProps.isSelectionMode) return false
|
||||
if (prevProps.isSelected !== nextProps.isSelected) return false
|
||||
if (prevProps.onRequireModelDownload !== nextProps.onRequireModelDownload) return false
|
||||
if (prevProps.onContextMenu !== nextProps.onContextMenu) return false
|
||||
if (prevProps.onJumpToQuotedMessage !== nextProps.onJumpToQuotedMessage) return false
|
||||
if (prevProps.onToggleSelection !== nextProps.onToggleSelection) return false
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
// 左侧联系人面板
|
||||
.contacts-panel {
|
||||
width: 350px;
|
||||
min-width: 350px;
|
||||
width: 324px;
|
||||
min-width: 324px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid var(--border-color);
|
||||
@@ -19,7 +19,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
h2 {
|
||||
@@ -40,7 +40,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
@@ -67,10 +67,10 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 16px 20px;
|
||||
padding: 10px 14px;
|
||||
margin: 12px 16px;
|
||||
padding: 8px 10px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
transition: border-color 0.2s;
|
||||
|
||||
@@ -117,9 +117,9 @@
|
||||
.type-filters {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
padding: 0 20px 16px;
|
||||
max-width: 300px;
|
||||
gap: 6px;
|
||||
padding: 0 16px 12px;
|
||||
max-width: 292px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
@@ -129,16 +129,16 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
padding: 7px 10px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
input[type="checkbox"] {
|
||||
@@ -167,7 +167,7 @@
|
||||
color: var(--text-primary);
|
||||
|
||||
svg {
|
||||
transform: translateY(-1px);
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
}
|
||||
|
||||
.contacts-count {
|
||||
padding: 0 20px 12px;
|
||||
padding: 0 16px 10px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 0 20px 12px;
|
||||
padding: 0 16px 10px;
|
||||
|
||||
.checkbox-item {
|
||||
font-size: 13px;
|
||||
@@ -249,7 +249,7 @@
|
||||
.issue-card {
|
||||
border: 1px solid color-mix(in srgb, var(--danger, #ef4444) 45%, var(--border-color));
|
||||
background: color-mix(in srgb, var(--danger, #ef4444) 8%, var(--card-bg));
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
color: var(--text-primary);
|
||||
|
||||
@@ -303,7 +303,7 @@
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
@@ -335,7 +335,7 @@
|
||||
.contacts-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 12px 12px;
|
||||
padding: 0 10px 10px;
|
||||
position: relative;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
@@ -358,7 +358,7 @@
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 76px;
|
||||
height: 64px;
|
||||
padding-bottom: 4px;
|
||||
will-change: transform;
|
||||
}
|
||||
@@ -366,12 +366,12 @@
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
height: 72px;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
height: 60px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s;
|
||||
border-radius: 8px;
|
||||
transition: background 0.15s ease;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -402,10 +402,10 @@
|
||||
}
|
||||
|
||||
.contact-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-hover));
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -419,8 +419,8 @@
|
||||
}
|
||||
|
||||
span {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
@@ -484,23 +484,23 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-bottom: 24px;
|
||||
gap: 10px;
|
||||
padding-bottom: 18px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.detail-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-hover));
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
img { width: 100%; height: 100%; object-fit: cover; }
|
||||
span { color: #fff; font-size: 28px; font-weight: 600; }
|
||||
span { color: var(--text-secondary); font-size: 24px; font-weight: 600; }
|
||||
}
|
||||
|
||||
.detail-name {
|
||||
@@ -511,7 +511,7 @@
|
||||
}
|
||||
|
||||
.detail-info-list {
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 18px;
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
@@ -565,15 +565,15 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
padding: 10px 12px;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover { background: var(--primary-hover); }
|
||||
}
|
||||
@@ -600,7 +600,7 @@
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px 24px;
|
||||
padding: 16px 18px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
@@ -613,7 +613,7 @@
|
||||
}
|
||||
|
||||
.setting-section {
|
||||
margin-bottom: 28px;
|
||||
margin-bottom: 22px;
|
||||
|
||||
h3 {
|
||||
font-size: 13px;
|
||||
@@ -621,7 +621,7 @@
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin: 0 0 14px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,10 +631,9 @@
|
||||
|
||||
.select-trigger {
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
padding: 9px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 9999px;
|
||||
/* Rounded pill shape */
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
@@ -643,7 +642,7 @@
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--text-tertiary);
|
||||
@@ -671,14 +670,12 @@
|
||||
right: 0;
|
||||
background: color-mix(in srgb, var(--bg-primary) 85%, var(--bg-secondary));
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
box-shadow: var(--shadow-md);
|
||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.12);
|
||||
z-index: 20;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.select-option {
|
||||
@@ -689,10 +686,10 @@
|
||||
gap: 4px;
|
||||
padding: 10px 12px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
|
||||
@@ -740,9 +737,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 12px;
|
||||
@@ -774,7 +771,7 @@
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
@@ -786,10 +783,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--text-secondary);
|
||||
transition: color 0.2s;
|
||||
@@ -797,7 +790,7 @@
|
||||
}
|
||||
|
||||
.export-action {
|
||||
padding: 20px 24px;
|
||||
padding: 16px 18px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
@@ -807,15 +800,15 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 14px 24px;
|
||||
padding: 12px 16px;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--primary-hover);
|
||||
@@ -841,3 +834,192 @@
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// UI rebuild polish for the contact detail surface.
|
||||
.contacts-page {
|
||||
background: var(--bg-primary);
|
||||
|
||||
.contact-detail-panel {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.contact-detail-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in srgb, var(--text-tertiary) 55%, transparent);
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.contact-detail-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 22px;
|
||||
border-radius: 16px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent);
|
||||
}
|
||||
|
||||
.contact-detail-hero .detail-avatar {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
border-radius: 16px;
|
||||
background: var(--bg-tertiary);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--text-secondary);
|
||||
font-size: 28px;
|
||||
font-weight: 650;
|
||||
}
|
||||
}
|
||||
|
||||
.contact-detail-heading {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
font-weight: 650;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-type {
|
||||
width: fit-content;
|
||||
border-radius: 999px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.contact-action-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.contact-action-row .goto-chat-btn,
|
||||
.contact-action-row .detail-entry-btn {
|
||||
min-height: 44px;
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border-radius: 12px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.contact-action-row .goto-chat-btn {
|
||||
border: 1px solid var(--primary);
|
||||
background: var(--primary);
|
||||
color: var(--on-primary, #fff);
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-hover);
|
||||
border-color: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.contact-action-row .detail-entry-btn {
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
border-color: color-mix(in srgb, var(--primary) 40%, var(--border-color));
|
||||
background: color-mix(in srgb, var(--primary) 7%, var(--bg-secondary));
|
||||
}
|
||||
}
|
||||
|
||||
.contact-detail-section {
|
||||
padding: 18px;
|
||||
border-radius: 16px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.contact-detail-section .detail-info-list {
|
||||
margin: 0;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.contact-detail-section .detail-row {
|
||||
display: grid;
|
||||
grid-template-columns: 76px minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: var(--bg-primary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.contact-detail-section .detail-label {
|
||||
min-width: 0;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.contact-detail-section .detail-value {
|
||||
color: var(--text-primary);
|
||||
line-height: 1.55;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.contact-detail-hero {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ interface ContactEnrichInfo {
|
||||
|
||||
const AVATAR_ENRICH_BATCH_SIZE = 80
|
||||
const SEARCH_DEBOUNCE_MS = 120
|
||||
const VIRTUAL_ROW_HEIGHT = 76
|
||||
const VIRTUAL_ROW_HEIGHT = 64
|
||||
const VIRTUAL_OVERSCAN = 10
|
||||
const DEFAULT_CONTACTS_LOAD_TIMEOUT_MS = 10000
|
||||
const AVATAR_RECHECK_INTERVAL_MS = 24 * 60 * 60 * 1000
|
||||
@@ -635,6 +635,39 @@ function ContactsPage() {
|
||||
return '朋友圈:统计中...'
|
||||
}, [selectedContactSupportsSns, selectedContactSnsCount, snsUserPostCountsStatus])
|
||||
|
||||
const selectedContactTitle = useMemo(() => {
|
||||
if (!selectedContact) return ''
|
||||
return selectedContact.displayName || selectedContact.remark || selectedContact.nickname || selectedContact.username
|
||||
}, [selectedContact])
|
||||
|
||||
const selectedContactSubtitle = useMemo(() => {
|
||||
if (!selectedContact) return ''
|
||||
const parts = [
|
||||
selectedContact.remark && selectedContact.remark !== selectedContactTitle ? `备注 ${selectedContact.remark}` : '',
|
||||
selectedContact.alias ? `微信号 ${selectedContact.alias}` : '',
|
||||
selectedContact.region || ''
|
||||
].filter(Boolean)
|
||||
return parts.join(' · ')
|
||||
}, [selectedContact, selectedContactTitle])
|
||||
|
||||
const selectedContactDetailRows = useMemo(() => {
|
||||
if (!selectedContact) return []
|
||||
return [
|
||||
{ key: 'username', label: '用户名', value: selectedContact.username },
|
||||
{ key: 'nickname', label: '昵称', value: selectedContact.nickname || selectedContact.displayName },
|
||||
selectedContact.remark ? { key: 'remark', label: '备注', value: selectedContact.remark } : null,
|
||||
selectedContact.alias ? { key: 'alias', label: '微信号', value: selectedContact.alias } : null,
|
||||
selectedContact.labels && selectedContact.labels.length > 0
|
||||
? { key: 'labels', label: '标签', value: selectedContact.labels.join('、') }
|
||||
: null,
|
||||
selectedContact.detailDescription
|
||||
? { key: 'signature', label: '个性签名', value: selectedContact.detailDescription }
|
||||
: null,
|
||||
selectedContact.region ? { key: 'region', label: '地区', value: selectedContact.region } : null,
|
||||
{ key: 'type', label: '类型', value: getContactTypeName(selectedContact.type) }
|
||||
].filter((row): row is { key: string; label: string; value: string } => Boolean(row && row.value))
|
||||
}, [selectedContact])
|
||||
|
||||
const openSelectedContactSnsTimeline = useCallback(() => {
|
||||
if (!selectedContact || !selectedContactSupportsSns) return
|
||||
if (snsUserPostCountsStatus === 'idle') {
|
||||
@@ -760,7 +793,7 @@ function ContactsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const getContactTypeName = (type: string) => {
|
||||
function getContactTypeName(type: string) {
|
||||
switch (type) {
|
||||
case 'friend': return '好友'
|
||||
case 'group': return '群聊'
|
||||
@@ -1090,12 +1123,9 @@ function ContactsPage() {
|
||||
</div>
|
||||
</div>
|
||||
) : selectedContact ? (
|
||||
<div className="settings-panel">
|
||||
<div className="panel-header">
|
||||
<h2>联系人详情</h2>
|
||||
</div>
|
||||
<div className="settings-content">
|
||||
<div className="detail-profile">
|
||||
<div className="settings-panel contact-detail-panel">
|
||||
<div className="contact-detail-scroll">
|
||||
<section className="contact-detail-hero">
|
||||
<div className="detail-avatar">
|
||||
{selectedContact.avatarUrl ? (
|
||||
<img src={selectedContact.avatarUrl} alt="" />
|
||||
@@ -1103,53 +1133,50 @@ function ContactsPage() {
|
||||
<span>{getAvatarLetter(selectedContact.displayName)}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="detail-name">{selectedContact.displayName}</div>
|
||||
<div className={`contact-type ${selectedContact.type}`}>
|
||||
{getContactTypeIcon(selectedContact.type)}
|
||||
<span>{getContactTypeName(selectedContact.type)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="detail-info-list">
|
||||
<div className="detail-row"><span className="detail-label">用户名</span><span className="detail-value">{selectedContact.username}</span></div>
|
||||
<div className="detail-row"><span className="detail-label">昵称</span><span className="detail-value">{selectedContact.nickname || selectedContact.displayName}</span></div>
|
||||
{selectedContact.remark && <div className="detail-row"><span className="detail-label">备注</span><span className="detail-value">{selectedContact.remark}</span></div>}
|
||||
{selectedContact.alias && <div className="detail-row"><span className="detail-label">微信号</span><span className="detail-value">{selectedContact.alias}</span></div>}
|
||||
{selectedContact.labels && selectedContact.labels.length > 0 && (
|
||||
<div className="detail-row"><span className="detail-label">标签</span><span className="detail-value">{selectedContact.labels.join('、')}</span></div>
|
||||
)}
|
||||
{selectedContact.detailDescription && (
|
||||
<div className="detail-row"><span className="detail-label">个性签名</span><span className="detail-value">{selectedContact.detailDescription}</span></div>
|
||||
)}
|
||||
{selectedContact.region && (
|
||||
<div className="detail-row"><span className="detail-label">地区</span><span className="detail-value">{selectedContact.region}</span></div>
|
||||
)}
|
||||
<div className="detail-row"><span className="detail-label">类型</span><span className="detail-value">{getContactTypeName(selectedContact.type)}</span></div>
|
||||
{selectedContactSupportsSns && (
|
||||
<div className="detail-row">
|
||||
<span className="detail-label">朋友圈</span>
|
||||
<button
|
||||
type="button"
|
||||
className="detail-entry-btn"
|
||||
onClick={openSelectedContactSnsTimeline}
|
||||
>
|
||||
<Aperture size={14} />
|
||||
<span>{selectedContactSnsEntryLabel}</span>
|
||||
</button>
|
||||
<div className="contact-detail-heading">
|
||||
<div className={`contact-type detail-type ${selectedContact.type}`}>
|
||||
{getContactTypeIcon(selectedContact.type)}
|
||||
<span>{getContactTypeName(selectedContact.type)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h2>{selectedContactTitle}</h2>
|
||||
{selectedContactSubtitle && <p>{selectedContactSubtitle}</p>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button
|
||||
className="goto-chat-btn"
|
||||
onClick={() => {
|
||||
setCurrentSession(selectedContact.username)
|
||||
navigate('/chat')
|
||||
}}
|
||||
>
|
||||
<MessageCircle size={18} />
|
||||
<span>查看聊天记录</span>
|
||||
</button>
|
||||
<section className="contact-action-row" aria-label="联系人操作">
|
||||
<button
|
||||
className="goto-chat-btn"
|
||||
onClick={() => {
|
||||
setCurrentSession(selectedContact.username)
|
||||
navigate('/chat')
|
||||
}}
|
||||
>
|
||||
<MessageCircle size={18} />
|
||||
<span>查看聊天记录</span>
|
||||
</button>
|
||||
{selectedContactSupportsSns && (
|
||||
<button
|
||||
type="button"
|
||||
className="detail-entry-btn"
|
||||
onClick={openSelectedContactSnsTimeline}
|
||||
>
|
||||
<Aperture size={18} />
|
||||
<span>{selectedContactSnsEntryLabel}</span>
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="contact-detail-section">
|
||||
<div className="section-title">基础资料</div>
|
||||
<div className="detail-info-list">
|
||||
{selectedContactDetailRows.map(row => (
|
||||
<div className="detail-row" key={row.key}>
|
||||
<span className="detail-label">{row.label}</span>
|
||||
<span className="detail-value">{row.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.dual-report-page {
|
||||
padding: 32px 28px;
|
||||
color: var(--text-primary);
|
||||
animation: dualFadeIn 0.35s ease-out;
|
||||
}
|
||||
|
||||
.dual-report-page.loading {
|
||||
@@ -22,25 +23,26 @@
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8px 0 0;
|
||||
margin: 6px 0 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.year-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
gap: 5px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--primary) 12%, transparent);
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
border: 1px solid color-mix(in srgb, var(--primary) 30%, transparent);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
@@ -50,11 +52,12 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
padding: 9px 12px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--text-tertiary);
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
@@ -69,7 +72,7 @@
|
||||
.ranking-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
@@ -77,42 +80,41 @@
|
||||
grid-template-columns: auto auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 14px;
|
||||
padding: 10px 14px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: background 0.15s ease;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-1px);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.rank-badge {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--border-color);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
|
||||
&.top {
|
||||
background: color-mix(in srgb, var(--primary) 18%, transparent);
|
||||
color: var(--primary);
|
||||
background: var(--primary);
|
||||
color: var(--on-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: var(--primary-light);
|
||||
@@ -121,6 +123,7 @@
|
||||
justify-content: center;
|
||||
color: var(--primary);
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
@@ -132,11 +135,11 @@
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0; // 允许 flex 子项缩小,配合 ellipsis
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
|
||||
.name {
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
@@ -146,29 +149,29 @@
|
||||
|
||||
.sub {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary); // 从 tertiary 改为 secondary 以增强对比度
|
||||
color: var(--text-tertiary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.meta {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary); // 改为 secondary
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
|
||||
.count {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--primary); // 使用主题色更醒目
|
||||
margin-bottom: 2px;
|
||||
color: var(--primary);
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
opacity: 0.7;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +179,7 @@
|
||||
text-align: center;
|
||||
color: var(--text-tertiary);
|
||||
padding: 40px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.spin {
|
||||
@@ -183,11 +187,17 @@
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes dualFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,14 @@
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
margin: -24px -24px 0;
|
||||
padding: 18px 22px 12px;
|
||||
padding: 16px 18px 12px;
|
||||
background: var(--bg-primary);
|
||||
/* Minimal background matching Footprint */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
animation: exportPageEnter 0.34s ease-out;
|
||||
|
||||
.spin {
|
||||
animation: exportSpin 1s linear infinite;
|
||||
@@ -22,7 +21,7 @@
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 55;
|
||||
animation: exportSectionReveal 0.34s ease both;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.export-top-bar {
|
||||
@@ -38,8 +37,8 @@
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
animation: exportSectionReveal 0.38s ease both;
|
||||
margin-bottom: 6px;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.session-load-detail-entry {
|
||||
@@ -137,7 +136,7 @@
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: 0.2px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.section-info-tooltip {
|
||||
@@ -209,10 +208,10 @@
|
||||
width: min(820px, 100%);
|
||||
max-height: min(78vh, 860px);
|
||||
overflow: hidden;
|
||||
border-radius: 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-primary);
|
||||
box-shadow: 0 22px 46px rgba(0, 0, 0, 0.28);
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: exportModalPopIn 0.24s cubic-bezier(0.2, 0.78, 0.26, 1) both;
|
||||
@@ -530,10 +529,10 @@
|
||||
width: min(760px, 100%);
|
||||
max-height: min(82vh, 900px);
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-primary);
|
||||
box-shadow: 0 22px 46px rgba(0, 0, 0, 0.28);
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: exportModalPopIn 0.24s cubic-bezier(0.2, 0.78, 0.26, 1) both;
|
||||
@@ -559,7 +558,7 @@
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-hover));
|
||||
background: var(--bg-tertiary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -754,29 +753,28 @@
|
||||
--top-inline-control-height: 34px;
|
||||
flex: 0 1 980px;
|
||||
width: min(980px, 100%);
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 75%, var(--bg-primary)) 0%, var(--card-bg) 100%);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 14px 28px rgba(15, 23, 42, 0.08);
|
||||
padding: 13px;
|
||||
border-radius: 8px;
|
||||
box-shadow: none;
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.55fr) minmax(240px, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
animation: exportSectionReveal 0.4s ease both;
|
||||
transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
|
||||
animation: none;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--primary) 30%, var(--border-color));
|
||||
box-shadow: 0 16px 30px rgba(15, 23, 42, 0.1);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2px;
|
||||
letter-spacing: 0;
|
||||
width: 78px;
|
||||
flex: 0 0 78px;
|
||||
line-height: 1.2;
|
||||
@@ -879,7 +877,7 @@
|
||||
|
||||
.more-export-settings-btn {
|
||||
min-height: var(--top-inline-control-height);
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
@@ -889,13 +887,12 @@
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.12s ease, color 0.12s ease, background 0.12s ease, transform 0.12s ease;
|
||||
transition: border-color 0.12s ease, color 0.12s ease, background 0.12s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
background: color-mix(in srgb, var(--primary) 6%, var(--bg-secondary));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -934,8 +931,8 @@
|
||||
max-width: calc(100vw - 40px);
|
||||
background: var(--bg-secondary-solid, #ffffff);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 88%, transparent);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 22px 38px rgba(15, 23, 42, 0.2);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.16);
|
||||
padding: 6px;
|
||||
z-index: 3000;
|
||||
max-height: 260px;
|
||||
@@ -1070,14 +1067,13 @@
|
||||
|
||||
.task-center-card {
|
||||
min-width: 92px;
|
||||
min-height: 42px;
|
||||
min-height: 38px;
|
||||
margin-left: auto;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent);
|
||||
border-radius: 14px;
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 80%, var(--bg-primary)) 0%, var(--card-bg) 100%);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
padding: 10px 12px;
|
||||
padding: 8px 11px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -1087,15 +1083,14 @@
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
align-self: stretch;
|
||||
transition: border-color 0.14s ease, color 0.14s ease, box-shadow 0.14s ease, transform 0.14s ease, background 0.14s ease;
|
||||
animation: exportSectionReveal 0.46s ease both;
|
||||
transition: border-color 0.14s ease, color 0.14s ease, background 0.14s ease;
|
||||
animation: none;
|
||||
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--primary) 48%, var(--border-color));
|
||||
color: var(--primary);
|
||||
transform: translateY(-2px);
|
||||
background: color-mix(in srgb, var(--primary) 7%, var(--card-bg));
|
||||
box-shadow: 0 10px 18px rgba(15, 23, 42, 0.12);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.has-alert {
|
||||
@@ -1120,10 +1115,10 @@
|
||||
width: min(720px, 100%);
|
||||
max-height: min(80vh, 860px);
|
||||
overflow: hidden;
|
||||
border-radius: 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-primary);
|
||||
box-shadow: 0 22px 46px rgba(0, 0, 0, 0.28);
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: exportModalPopIn 0.24s cubic-bezier(0.2, 0.78, 0.26, 1) both;
|
||||
@@ -1162,6 +1157,30 @@
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.export-defaults-modal {
|
||||
width: min(740px, calc(100vw - 40px));
|
||||
max-height: min(82vh, 820px);
|
||||
border-radius: 16px;
|
||||
background: var(--bg-primary);
|
||||
border-color: color-mix(in srgb, var(--border-color) 68%, transparent);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.export-defaults-modal-header {
|
||||
padding: 16px 18px 10px;
|
||||
border-bottom-color: color-mix(in srgb, var(--border-color) 64%, transparent);
|
||||
}
|
||||
|
||||
.export-defaults-modal-body {
|
||||
padding: 12px 18px 14px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.export-defaults-modal-actions {
|
||||
padding: 0 18px 16px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.task-center-card-label {
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
@@ -1180,35 +1199,32 @@
|
||||
justify-content: center;
|
||||
padding: 0 5px;
|
||||
line-height: 1;
|
||||
box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.16);
|
||||
animation: exportTaskBadgePulse 1.2s ease-in-out infinite;
|
||||
box-shadow: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.content-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(142px, 1fr));
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
animation: exportSectionReveal 0.52s ease both;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent);
|
||||
border-radius: 13px;
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 82%, var(--bg-primary)) 0%, var(--card-bg) 100%);
|
||||
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.06);
|
||||
padding: 11px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
box-shadow: none;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease, background 0.16s ease;
|
||||
animation: exportCardReveal 0.42s ease both;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
animation: none;
|
||||
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--primary) 38%, var(--border-color));
|
||||
box-shadow: 0 16px 24px rgba(15, 23, 42, 0.11);
|
||||
transform: translateY(-2px);
|
||||
background: color-mix(in srgb, var(--primary) 4%, var(--card-bg));
|
||||
}
|
||||
|
||||
@@ -1263,8 +1279,8 @@
|
||||
.card-export-btn {
|
||||
margin-top: auto;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 9px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
padding: 7px 10px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
@@ -1281,8 +1297,7 @@
|
||||
|
||||
&.primary:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 14px color-mix(in srgb, var(--primary) 24%, transparent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
@@ -1295,7 +1310,6 @@
|
||||
border-color: color-mix(in srgb, var(--primary) 28%, transparent);
|
||||
color: var(--text-primary);
|
||||
background: color-mix(in srgb, var(--bg-primary) 94%, var(--primary) 6%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@@ -1372,10 +1386,10 @@
|
||||
.task-center-modal {
|
||||
width: min(980px, calc(100vw - 40px));
|
||||
max-height: calc(100vh - 72px);
|
||||
border-radius: 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary-solid, #ffffff);
|
||||
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.24);
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -1677,16 +1691,15 @@
|
||||
flex-direction: column;
|
||||
overflow: visible;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent);
|
||||
border-radius: 14px;
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 72%, var(--bg-primary)) 0%, color-mix(in srgb, var(--card-bg) 90%, var(--bg-primary)) 100%);
|
||||
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.07);
|
||||
animation: exportSectionReveal 0.58s ease both;
|
||||
transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
box-shadow: none;
|
||||
animation: none;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--primary) 20%, var(--border-color));
|
||||
box-shadow: 0 18px 36px rgba(15, 23, 42, 0.09);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1696,24 +1709,24 @@
|
||||
gap: 6px;
|
||||
margin: 8px 12px 0;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
border-radius: 8px;
|
||||
background: rgba(var(--primary-rgb), 0.1);
|
||||
border: 1px solid rgba(var(--primary-rgb), 0.2);
|
||||
color: var(--primary);
|
||||
font-size: 12px;
|
||||
width: fit-content;
|
||||
animation: exportSectionReveal 0.35s ease both;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
padding: 12px 14px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent);
|
||||
background: linear-gradient(180deg, color-mix(in srgb, var(--bg-primary) 88%, var(--card-bg)) 0%, color-mix(in srgb, var(--bg-primary) 92%, var(--bg-secondary)) 100%);
|
||||
background: var(--bg-secondary);
|
||||
transition: border-color 0.16s ease, background 0.16s ease;
|
||||
}
|
||||
|
||||
@@ -1751,7 +1764,7 @@
|
||||
color: var(--text-secondary);
|
||||
min-height: 32px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
@@ -1759,7 +1772,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
.tab-btn-content {
|
||||
display: inline-flex;
|
||||
@@ -1791,7 +1804,7 @@
|
||||
&.active {
|
||||
color: var(--primary);
|
||||
background: color-mix(in srgb, var(--primary) 12%, transparent);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||
box-shadow: none;
|
||||
|
||||
.tab-btn-content span:last-child {
|
||||
background: color-mix(in srgb, var(--primary) 16%, transparent);
|
||||
@@ -1822,11 +1835,10 @@
|
||||
|
||||
.secondary-btn {
|
||||
min-height: 34px;
|
||||
border-radius: 10px;
|
||||
transition: border-color 0.14s ease, background 0.14s ease, color 0.14s ease, transform 0.14s ease;
|
||||
border-radius: 8px;
|
||||
transition: border-color 0.14s ease, background 0.14s ease, color 0.14s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
border-color: color-mix(in srgb, var(--primary) 36%, var(--border-color));
|
||||
color: var(--primary);
|
||||
background: color-mix(in srgb, var(--primary) 8%, var(--bg-secondary));
|
||||
@@ -1842,14 +1854,14 @@
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: color-mix(in srgb, var(--text-tertiary) 4%, transparent);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03) inset;
|
||||
box-shadow: none;
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
max-width: 320px;
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&:focus-within {
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent), 0 1px 3px rgba(0, 0, 0, 0.02) inset;
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary) 24%, transparent);
|
||||
background: color-mix(in srgb, var(--text-tertiary) 6%, transparent);
|
||||
}
|
||||
|
||||
@@ -1874,7 +1886,7 @@
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
@@ -1909,31 +1921,31 @@
|
||||
|
||||
.table-wrap {
|
||||
--contacts-native-scrollbar-compensation: 18px;
|
||||
--contacts-row-height: 76px;
|
||||
--contacts-default-visible-rows: 8;
|
||||
--contacts-row-height: 64px;
|
||||
--contacts-default-visible-rows: 9;
|
||||
--contacts-default-list-height: calc(var(--contacts-row-height) * var(--contacts-default-visible-rows));
|
||||
--contacts-select-col-width: 34px;
|
||||
--contacts-avatar-col-width: 44px;
|
||||
--contacts-inline-padding: 12px;
|
||||
--contacts-column-gap: 10px;
|
||||
--contacts-select-col-width: 30px;
|
||||
--contacts-avatar-col-width: 38px;
|
||||
--contacts-inline-padding: 10px;
|
||||
--contacts-column-gap: 8px;
|
||||
--contacts-name-text-width: 9.5em;
|
||||
--contacts-main-col-width: calc(var(--contacts-avatar-col-width) + var(--contacts-column-gap) + var(--contacts-name-text-width));
|
||||
--contacts-left-sticky-width: calc(var(--contacts-select-col-width) + var(--contacts-main-col-width) + var(--contacts-column-gap));
|
||||
--contacts-message-col-width: 104px;
|
||||
--contacts-media-col-width: 66px;
|
||||
--contacts-action-col-width: 140px;
|
||||
--contacts-actions-sticky-width: 180px;
|
||||
--contacts-table-min-width: 1240px;
|
||||
--contacts-message-col-width: 94px;
|
||||
--contacts-media-col-width: 58px;
|
||||
--contacts-action-col-width: 126px;
|
||||
--contacts-actions-sticky-width: 160px;
|
||||
--contacts-table-min-width: 1120px;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
min-height: 320px;
|
||||
height: auto;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-secondary);
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--text-tertiary) 2%, var(--bg-secondary));
|
||||
@@ -2012,7 +2024,7 @@
|
||||
.issue-card {
|
||||
border: 1px solid color-mix(in srgb, var(--danger, #ef4444) 45%, var(--border-color));
|
||||
background: color-mix(in srgb, var(--danger, #ef4444) 8%, var(--bg-secondary));
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
@@ -2068,12 +2080,11 @@
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
background: color-mix(in srgb, var(--text-tertiary) 16%, transparent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
@@ -2104,7 +2115,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--contacts-column-gap);
|
||||
padding: 10px var(--contacts-inline-padding) 8px;
|
||||
padding: 8px var(--contacts-inline-padding) 6px;
|
||||
min-width: max(100%, var(--contacts-table-min-width));
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--text-tertiary) 6%, transparent);
|
||||
background: var(--contacts-header-bg);
|
||||
@@ -2113,7 +2124,6 @@
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
flex-shrink: 0;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
&.is-draggable {
|
||||
cursor: grab;
|
||||
@@ -2273,12 +2283,11 @@
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: var(--text-primary);
|
||||
background: color-mix(in srgb, var(--text-tertiary) 16%, transparent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@@ -2301,13 +2310,12 @@
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 6px color-mix(in srgb, var(--primary) 20%, transparent);
|
||||
transition: background 0.15s ease;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--primary) 85%, #fff);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 14px color-mix(in srgb, var(--primary) 30%, transparent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.selection-export-count {
|
||||
@@ -2382,19 +2390,18 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--contacts-column-gap);
|
||||
padding: 12px var(--contacts-inline-padding);
|
||||
padding: 8px var(--contacts-inline-padding);
|
||||
min-width: max(100%, var(--contacts-table-min-width));
|
||||
height: 72px;
|
||||
height: 60px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 8px;
|
||||
transition: background 0.15s ease;
|
||||
cursor: default;
|
||||
background: var(--contacts-row-bg);
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--text-tertiary) 6%, transparent);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2421,10 +2428,10 @@
|
||||
}
|
||||
|
||||
.contact-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-hover));
|
||||
width: var(--contacts-avatar-col-width);
|
||||
height: var(--contacts-avatar-col-width);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -2438,8 +2445,8 @@
|
||||
}
|
||||
|
||||
span {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
@@ -2740,7 +2747,7 @@
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-hover));
|
||||
background: var(--bg-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -2752,7 +2759,7 @@
|
||||
}
|
||||
|
||||
span {
|
||||
color: #fff;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -2811,12 +2818,11 @@
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
background: color-mix(in srgb, var(--text-tertiary) 14%, transparent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&.active {
|
||||
@@ -2923,7 +2929,7 @@
|
||||
height: 100%;
|
||||
max-height: calc(100vh - 24px);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 40%, transparent);
|
||||
border-radius: 20px;
|
||||
border-radius: 10px;
|
||||
background: var(--bg-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2949,8 +2955,8 @@
|
||||
.detail-header-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-hover));
|
||||
border-radius: 8px;
|
||||
background: var(--bg-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -3248,13 +3254,12 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.14s ease, background 0.14s ease, color 0.14s ease, transform 0.14s ease;
|
||||
transition: border-color 0.14s ease, background 0.14s ease, color 0.14s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: color-mix(in srgb, var(--primary) 38%, var(--border-color));
|
||||
color: var(--primary);
|
||||
background: color-mix(in srgb, var(--primary) 9%, var(--bg-secondary));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@@ -3321,10 +3326,10 @@
|
||||
.export-session-sns-dialog {
|
||||
width: min(760px, 100%);
|
||||
max-height: min(86vh, 860px);
|
||||
border-radius: 20px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid color-mix(in srgb, var(--text-tertiary) 8%, transparent);
|
||||
background: var(--bg-primary);
|
||||
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.16);
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.16);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -3349,8 +3354,8 @@
|
||||
.sns-dialog-avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-hover));
|
||||
border-radius: 8px;
|
||||
background: var(--bg-tertiary);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
@@ -3739,7 +3744,7 @@
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 18, 28, 0.48);
|
||||
backdrop-filter: blur(7px);
|
||||
backdrop-filter: blur(2px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -3752,9 +3757,9 @@
|
||||
max-height: calc(100vh - 40px);
|
||||
background: var(--card-bg);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 26px 52px rgba(0, 0, 0, 0.3);
|
||||
padding: 16px 16px 14px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.22);
|
||||
padding: 14px 14px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -3789,9 +3794,9 @@
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: 22px;
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.2px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3811,7 +3816,7 @@
|
||||
.close-icon-btn {
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-secondary) 86%, var(--bg-primary));
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
@@ -3819,20 +3824,19 @@
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
transition: border-color 0.16s ease, color 0.16s ease, transform 0.16s ease, background 0.16s ease;
|
||||
transition: border-color 0.16s ease, color 0.16s ease, background 0.16s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--primary) 42%, var(--border-color));
|
||||
color: var(--primary);
|
||||
background: color-mix(in srgb, var(--primary) 7%, var(--bg-primary));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-section {
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent);
|
||||
border-radius: 12px;
|
||||
padding: 13px 14px;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
background: color-mix(in srgb, var(--bg-secondary) 78%, var(--bg-primary));
|
||||
|
||||
h4 {
|
||||
@@ -3840,7 +3844,7 @@
|
||||
font-size: 15px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2px;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
@@ -3924,7 +3928,7 @@
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
|
||||
@@ -4007,8 +4011,8 @@
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 88%, transparent);
|
||||
border-radius: 11px;
|
||||
padding: 10px 11px;
|
||||
border-radius: 8px;
|
||||
padding: 9px 10px;
|
||||
text-align: left;
|
||||
background: color-mix(in srgb, var(--bg-primary) 70%, var(--bg-secondary));
|
||||
cursor: pointer;
|
||||
@@ -4016,7 +4020,7 @@
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
|
||||
.format-label {
|
||||
font-size: 14px;
|
||||
@@ -4034,7 +4038,6 @@
|
||||
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--primary) 36%, var(--border-color));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&.active {
|
||||
@@ -4102,20 +4105,19 @@
|
||||
.media-option-card {
|
||||
position: relative;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent);
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--bg-primary) 72%, var(--bg-secondary));
|
||||
min-height: 74px;
|
||||
padding: 10px 11px;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--bg-primary) 76%, var(--bg-secondary));
|
||||
min-height: 68px;
|
||||
padding: 9px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 9px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
|
||||
transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--primary) 46%, var(--border-color));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:has(.media-option-input:focus-visible) {
|
||||
@@ -4126,7 +4128,7 @@
|
||||
&.active {
|
||||
border-color: color-mix(in srgb, var(--primary) 76%, var(--border-color));
|
||||
background: color-mix(in srgb, var(--primary) 12%, var(--bg-secondary));
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 28%, transparent);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary) 26%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4147,7 +4149,7 @@
|
||||
.media-option-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 9px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-primary) 86%, var(--bg-secondary));
|
||||
color: var(--text-secondary);
|
||||
@@ -4428,7 +4430,7 @@
|
||||
font: inherit;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid rgba(var(--primary-rgb), 0.35);
|
||||
@@ -4437,7 +4439,6 @@
|
||||
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--primary) 44%, var(--border-color));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
span {
|
||||
@@ -4467,14 +4468,12 @@
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(180deg,
|
||||
transparent,
|
||||
var(--card-bg) 38%);
|
||||
background: var(--card-bg);
|
||||
}
|
||||
|
||||
.primary-btn,
|
||||
.secondary-btn {
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
min-height: 38px;
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
@@ -4484,7 +4483,7 @@
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease, transform 0.15s ease;
|
||||
transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
@@ -4495,7 +4494,6 @@
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@@ -4513,7 +4511,6 @@
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
background: color-mix(in srgb, var(--primary) 7%, var(--bg-secondary));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5028,10 +5025,10 @@
|
||||
.table-wrap {
|
||||
--contacts-inline-padding: 10px;
|
||||
--contacts-name-text-width: 10em;
|
||||
--contacts-main-col-width: calc(44px + 10px + var(--contacts-name-text-width));
|
||||
--contacts-message-col-width: 104px;
|
||||
--contacts-media-col-width: 62px;
|
||||
--contacts-action-col-width: 140px;
|
||||
--contacts-main-col-width: calc(var(--contacts-avatar-col-width) + var(--contacts-column-gap) + var(--contacts-name-text-width));
|
||||
--contacts-message-col-width: 94px;
|
||||
--contacts-media-col-width: 56px;
|
||||
--contacts-action-col-width: 126px;
|
||||
}
|
||||
|
||||
.table-wrap .contacts-list-header {
|
||||
@@ -5094,7 +5091,7 @@
|
||||
width: calc(100vw - 20px);
|
||||
max-height: calc(100vh - 20px);
|
||||
padding: 12px 10px 10px;
|
||||
border-radius: 14px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
@@ -5266,10 +5263,10 @@
|
||||
.automation-modal {
|
||||
width: min(680px, 100%);
|
||||
max-height: min(80vh, 820px);
|
||||
border-radius: 20px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid color-mix(in srgb, var(--text-tertiary) 8%, transparent);
|
||||
background: var(--bg-primary);
|
||||
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.16);
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.16);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -5320,9 +5317,9 @@
|
||||
}
|
||||
|
||||
.automation-task-card {
|
||||
border-radius: 14px;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--text-tertiary) 5%, transparent);
|
||||
padding: 14px 16px;
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
@@ -5411,10 +5408,10 @@
|
||||
.automation-editor-modal {
|
||||
width: min(560px, 100%);
|
||||
max-height: min(88vh, 900px);
|
||||
border-radius: 20px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid color-mix(in srgb, var(--text-tertiary) 8%, transparent);
|
||||
background: var(--bg-primary);
|
||||
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.18);
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.16);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -5683,7 +5680,7 @@
|
||||
border-radius: 4px;
|
||||
border: 1px solid color-mix(in srgb, var(--text-tertiary) 30%, transparent);
|
||||
background: transparent;
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
@@ -5724,7 +5721,7 @@
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.18s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--text-tertiary) 14%, transparent);
|
||||
@@ -5749,7 +5746,7 @@
|
||||
|
||||
.automation-draft-summary {
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--text-tertiary) 5%, transparent);
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
@@ -5768,7 +5765,7 @@
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.18s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--text-tertiary) 16%, transparent);
|
||||
@@ -5778,20 +5775,19 @@
|
||||
|
||||
.primary-btn {
|
||||
border: none;
|
||||
border-radius: 9px;
|
||||
border-radius: 8px;
|
||||
padding: 8px 18px;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 6px color-mix(in srgb, var(--primary) 20%, transparent);
|
||||
transition: background 0.15s ease;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--primary) 85%, #fff);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 14px color-mix(in srgb, var(--primary) 30%, transparent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@@ -5819,7 +5815,7 @@
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: background 0.15s ease, box-shadow 0.15s ease;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -9552,7 +9552,7 @@ function ExportPage() {
|
||||
customScrollParent={contactsListScrollParent ?? undefined}
|
||||
data={filteredContacts}
|
||||
computeItemKey={(_, contact) => contact.username}
|
||||
fixedItemHeight={76}
|
||||
fixedItemHeight={64}
|
||||
itemContent={renderContactRow}
|
||||
rangeChanged={handleContactsRangeChanged}
|
||||
atTopStateChange={setIsContactsListAtTop}
|
||||
|
||||
@@ -1955,3 +1955,276 @@
|
||||
opacity: 0.92;
|
||||
}
|
||||
}
|
||||
|
||||
// UI rebuild polish: align group analysis with the shared ChatGPT-style tokens.
|
||||
.group-analytics-shell {
|
||||
gap: 14px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.group-analytics-page {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.group-sidebar,
|
||||
.detail-area {
|
||||
border-radius: 16px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.group-sidebar {
|
||||
.sidebar-header {
|
||||
min-height: 58px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
border-radius: 12px;
|
||||
border: 1px solid transparent;
|
||||
background: var(--bg-primary);
|
||||
|
||||
&:focus-within {
|
||||
border-color: color-mix(in srgb, var(--primary) 40%, var(--border-color));
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 12%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
border-radius: 10px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.group-item {
|
||||
margin: 0 8px 4px;
|
||||
border-bottom: none;
|
||||
border-radius: 12px;
|
||||
|
||||
&.active {
|
||||
background: color-mix(in srgb, var(--primary) 12%, var(--bg-secondary));
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.group-avatar {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-drag-region {
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.function-menu {
|
||||
padding: 18px 22px 24px;
|
||||
|
||||
.selected-group-info {
|
||||
border-radius: 16px;
|
||||
border-color: color-mix(in srgb, var(--border-color) 70%, transparent);
|
||||
background: var(--bg-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.function-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.function-card {
|
||||
min-height: 136px;
|
||||
border-radius: 16px;
|
||||
border-color: color-mix(in srgb, var(--border-color) 76%, transparent);
|
||||
background: var(--bg-primary);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
border-color: color-mix(in srgb, var(--primary) 35%, var(--border-color));
|
||||
background: color-mix(in srgb, var(--primary) 5%, var(--bg-primary));
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.function-content {
|
||||
.content-header {
|
||||
padding: 16px 22px 12px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom-color: color-mix(in srgb, var(--border-color) 70%, transparent);
|
||||
|
||||
.back-btn,
|
||||
.refresh-btn,
|
||||
.export-btn {
|
||||
border-radius: 10px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.content-body {
|
||||
padding: 18px 22px 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.members-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(108px, 1fr));
|
||||
gap: 10px;
|
||||
|
||||
.member-card {
|
||||
border-radius: 12px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--primary) 5%, var(--bg-primary));
|
||||
border-color: color-mix(in srgb, var(--primary) 28%, var(--border-color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.member-export-panel,
|
||||
.member-messages-panel,
|
||||
.member-analytics-panel {
|
||||
.select-trigger {
|
||||
border-radius: 12px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.member-export-options,
|
||||
.member-message-item,
|
||||
.member-message-summary-card {
|
||||
border-radius: 12px;
|
||||
background: var(--bg-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.rankings-list {
|
||||
.ranking-item {
|
||||
border-radius: 12px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--border-color);
|
||||
background: color-mix(in srgb, var(--primary) 4%, var(--bg-primary));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-stats {
|
||||
.media-layout .media-legend,
|
||||
.stat-card,
|
||||
.charts-grid .chart-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 76%, transparent);
|
||||
background: var(--bg-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.member-modal-overlay {
|
||||
background: rgba(0, 0, 0, 0.42);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.member-modal,
|
||||
.member-export-modal,
|
||||
.member-result-modal {
|
||||
border-radius: 16px;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-primary) 96%, transparent);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.member-modal {
|
||||
width: min(380px, calc(100vw - 32px));
|
||||
padding: 28px 32px 32px;
|
||||
|
||||
.member-display-name {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
border-radius: 12px;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.member-modal-actions {
|
||||
margin-top: 18px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.member-modal-primary-btn,
|
||||
.member-modal-secondary-btn {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||
|
||||
span {
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.member-modal-primary-btn {
|
||||
border: 1px solid var(--primary);
|
||||
background: var(--primary);
|
||||
color: var(--on-primary, #fff);
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-hover);
|
||||
border-color: var(--primary-hover);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.member-modal-secondary-btn {
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--primary) 40%, var(--border-color));
|
||||
background: color-mix(in srgb, var(--primary) 8%, var(--bg-secondary));
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-mode="dark"] {
|
||||
.member-modal,
|
||||
.member-export-modal,
|
||||
.member-result-modal {
|
||||
background: color-mix(in srgb, var(--bg-secondary) 94%, transparent);
|
||||
border-color: color-mix(in srgb, var(--border-color) 85%, transparent);
|
||||
}
|
||||
|
||||
.members-grid .member-card,
|
||||
.rankings-list .ranking-item,
|
||||
.member-export-panel .member-message-item,
|
||||
.media-stats .media-layout .media-legend,
|
||||
.media-stats .stat-card,
|
||||
.media-stats .charts-grid .chart-card {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
.home-page {
|
||||
height: 100%;
|
||||
background: var(--bg-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -8,105 +7,29 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.home-bg-blobs {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: blur(80px);
|
||||
z-index: 0;
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
animation: moveBlob 20s infinite alternate ease-in-out;
|
||||
}
|
||||
|
||||
.blob-1 {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: rgba(var(--primary-rgb), 0.25);
|
||||
top: -100px;
|
||||
left: -50px;
|
||||
animation-duration: 25s;
|
||||
}
|
||||
|
||||
.blob-2 {
|
||||
width: 350px;
|
||||
height: 350px;
|
||||
background: rgba(var(--primary-rgb), 0.15);
|
||||
bottom: -50px;
|
||||
right: -50px;
|
||||
animation-duration: 30s;
|
||||
animation-delay: -5s;
|
||||
}
|
||||
|
||||
.blob-3 {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
top: 40%;
|
||||
left: 30%;
|
||||
animation-duration: 22s;
|
||||
animation-delay: -10s;
|
||||
}
|
||||
|
||||
[data-mode="dark"] .blob-3 {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.home-content {
|
||||
z-index: 1;
|
||||
animation: fadeScaleUp 1s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
max-width: 640px;
|
||||
width: 100%;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 64px;
|
||||
font-weight: 800;
|
||||
margin: 0 0 16px;
|
||||
.home-title {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 12px;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -2px;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, rgba(var(--primary-rgb), 0.6) 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 18px;
|
||||
.home-subtitle {
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@keyframes moveBlob {
|
||||
from {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(100px, 50px) scale(1.1);
|
||||
@media (max-width: 480px) {
|
||||
.home-title {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeScaleUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,11 @@
|
||||
import { FolderOpen, ShieldCheck, Sparkles, Waves } from 'lucide-react'
|
||||
import { useAppStore } from '../stores/appStore'
|
||||
import './HomePage.scss'
|
||||
|
||||
function HomePage() {
|
||||
return (
|
||||
<div className="home-page">
|
||||
<div className="home-bg-blobs">
|
||||
<div className="blob blob-1"></div>
|
||||
<div className="blob blob-2"></div>
|
||||
<div className="blob blob-3"></div>
|
||||
</div>
|
||||
|
||||
<div className="home-content">
|
||||
<div className="hero">
|
||||
<h1 className="hero-title">WeFlow</h1>
|
||||
<p className="hero-subtitle">每一条消息的背后,都藏着一段温暖的时光</p>
|
||||
</div>
|
||||
<h1 className="home-title">WeFlow</h1>
|
||||
<p className="home-subtitle">每一条消息的背后,都藏着一段温暖的时光</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.insight-inbox-page {
|
||||
--insight-panel-width: 360px;
|
||||
--insight-panel-width: 328px;
|
||||
--insight-card-bg: var(--bg-secondary);
|
||||
display: flex;
|
||||
height: calc(100% + 48px);
|
||||
@@ -14,15 +14,15 @@
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 18px 24px 14px;
|
||||
padding: 16px 20px 12px;
|
||||
}
|
||||
|
||||
.insight-inbox-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 0 4px 12px;
|
||||
gap: 14px;
|
||||
padding: 0 2px 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.insight-inbox-title-line {
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-size: 21px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
@@ -82,7 +82,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease, background 0.2s ease, border-color 0.2s ease;
|
||||
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
@@ -92,15 +92,15 @@
|
||||
}
|
||||
|
||||
.insight-icon-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.insight-action-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 8px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
|
||||
&.code {
|
||||
color: var(--primary);
|
||||
@@ -118,10 +118,10 @@
|
||||
}
|
||||
|
||||
.insight-focus-bar {
|
||||
margin: 12px 4px 0;
|
||||
padding: 9px 12px;
|
||||
margin: 10px 2px 0;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid rgba(91, 147, 144, 0.22);
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
background: rgba(91, 147, 144, 0.08);
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
@@ -143,14 +143,14 @@
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 16px 4px 22px;
|
||||
padding: 12px 2px 18px;
|
||||
}
|
||||
|
||||
.insight-date-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.insight-date-label {
|
||||
@@ -158,27 +158,26 @@
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
width: fit-content;
|
||||
padding: 5px 10px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--bg-primary) 86%, transparent);
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-tertiary);
|
||||
font-size: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.insight-card {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
padding: 18px;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 14px;
|
||||
border-radius: 8px;
|
||||
background: var(--insight-card-bg);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
box-shadow: none;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(91, 147, 144, 0.28);
|
||||
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.07);
|
||||
background: color-mix(in srgb, var(--bg-secondary) 86%, var(--bg-primary));
|
||||
}
|
||||
|
||||
&.unread {
|
||||
@@ -187,7 +186,7 @@
|
||||
|
||||
&.focused {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(91, 147, 144, 0.14), 0 12px 32px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: inset 0 0 0 1px rgba(91, 147, 144, 0.28);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +199,7 @@
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.insight-card-header {
|
||||
@@ -277,8 +276,8 @@
|
||||
.insight-body {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: 15px;
|
||||
line-height: 1.72;
|
||||
font-size: 14px;
|
||||
line-height: 1.66;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -286,14 +285,14 @@
|
||||
.insight-filter-panel {
|
||||
width: var(--insight-panel-width);
|
||||
flex-shrink: 0;
|
||||
padding: 24px 24px 18px;
|
||||
padding: 18px 18px 14px;
|
||||
border-left: 1px solid var(--border-color);
|
||||
background: color-mix(in srgb, var(--bg-secondary) 70%, var(--bg-primary));
|
||||
background: var(--bg-secondary);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.insight-filter-header {
|
||||
margin-bottom: 18px;
|
||||
margin-bottom: 14px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
@@ -304,10 +303,10 @@
|
||||
|
||||
.insight-filter-widget {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
background: var(--bg-secondary);
|
||||
padding: 14px;
|
||||
margin-bottom: 14px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-primary);
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.insight-widget-title {
|
||||
@@ -330,14 +329,14 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 9px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-tertiary);
|
||||
padding: 0 9px;
|
||||
|
||||
input {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
height: 38px;
|
||||
height: 34px;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
@@ -410,12 +409,12 @@
|
||||
|
||||
.insight-contact-row {
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
min-height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
border: none;
|
||||
border-radius: 9px;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
padding: 7px 8px;
|
||||
@@ -478,7 +477,6 @@
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -490,9 +488,9 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 14px;
|
||||
border-radius: 10px;
|
||||
background: var(--bg-secondary);
|
||||
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.18);
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.14);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -501,7 +499,7 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 18px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-tertiary);
|
||||
|
||||
@@ -549,7 +547,7 @@
|
||||
.insight-log-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 18px;
|
||||
padding: 16px;
|
||||
background: var(--bg-primary);
|
||||
|
||||
section {
|
||||
@@ -567,7 +565,7 @@
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-family: Consolas, Monaco, 'Courier New', monospace;
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
gap: 24px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
animation: footprintPageEnter 0.4s ease-out;
|
||||
|
||||
.card-surface {
|
||||
/* Removing border and strong shadows, just subtle background if any */
|
||||
@@ -31,7 +30,7 @@
|
||||
gap: 20px;
|
||||
padding: 10px 0 20px 0;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--border-color) 40%, transparent);
|
||||
animation: footprintFadeSlideUp 0.3s ease both;
|
||||
animation: footprintFadeSlideUp 0.22s ease both;
|
||||
}
|
||||
|
||||
.footprint-title-wrap {
|
||||
@@ -41,10 +40,10 @@
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 26px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
letter-spacing: -0.3px;
|
||||
letter-spacing: 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -84,7 +83,7 @@
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
@@ -93,7 +92,7 @@
|
||||
&.active {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-primary);
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,9 +116,9 @@
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.02) inset;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--text-tertiary) 12%, transparent);
|
||||
@@ -135,7 +134,7 @@
|
||||
&::-webkit-calendar-picker-indicator {
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
transition: all 0.2s ease;
|
||||
transition: opacity 0.15s ease, background 0.15s ease;
|
||||
padding: 4px;
|
||||
margin-left: 4px;
|
||||
margin-right: -4px;
|
||||
@@ -165,7 +164,7 @@
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
color: var(--text-tertiary);
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:focus-within {
|
||||
background: color-mix(in srgb, var(--primary) 8%, transparent);
|
||||
@@ -201,7 +200,7 @@
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--text-tertiary) 15%, transparent);
|
||||
@@ -217,8 +216,8 @@
|
||||
.kpi-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
padding: 20px 0;
|
||||
gap: 16px;
|
||||
padding: 18px 0;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--border-color) 40%, transparent);
|
||||
}
|
||||
|
||||
@@ -241,11 +240,11 @@
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: 32px;
|
||||
font-size: 30px;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.5px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
small {
|
||||
@@ -259,7 +258,7 @@
|
||||
}
|
||||
|
||||
.footprint-ai-result {
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
background: color-mix(in srgb, var(--text-tertiary) 8%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 40%, transparent);
|
||||
@@ -360,7 +359,7 @@
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
@@ -688,11 +687,11 @@
|
||||
|
||||
.footprint-export-modal {
|
||||
width: min(520px, 100%);
|
||||
border-radius: 16px;
|
||||
border-radius: 10px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 60%, transparent);
|
||||
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.2);
|
||||
padding: 22px 22px 18px;
|
||||
box-shadow: 0 14px 38px rgba(0, 0, 0, 0.16);
|
||||
padding: 18px 18px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
@@ -700,7 +699,7 @@
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
@@ -716,7 +715,7 @@
|
||||
.export-modal-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -740,8 +739,8 @@
|
||||
.export-modal-path {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
padding: 9px 11px;
|
||||
border-radius: 8px;
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
.resources-page.stream-rebuild {
|
||||
--stream-columns: 4;
|
||||
--stream-grid-gap: 12px;
|
||||
--stream-card-width: 272px;
|
||||
--stream-card-height: 356px;
|
||||
--stream-visual-height: 236px;
|
||||
--stream-grid-gap: 10px;
|
||||
--stream-card-width: 248px;
|
||||
--stream-card-height: 318px;
|
||||
--stream-visual-height: 210px;
|
||||
--stream-slot-width: calc(var(--stream-card-width) + var(--stream-grid-gap));
|
||||
--stream-slot-height: calc(var(--stream-card-height) + var(--stream-grid-gap));
|
||||
--stream-grid-width: calc(var(--stream-slot-width) * var(--stream-columns));
|
||||
height: calc(100% + 48px);
|
||||
margin: -24px;
|
||||
padding: 16px 18px;
|
||||
padding: 14px 16px;
|
||||
position: relative;
|
||||
background: var(--bg-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
.stream-toolbar {
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 78%, transparent);
|
||||
background: var(--card-bg, #f8f9fb);
|
||||
border-radius: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
@@ -41,7 +41,7 @@
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
padding: 4px;
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--bg-secondary) 85%, transparent);
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
@@ -49,16 +49,16 @@
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary, #5f6674);
|
||||
border-radius: 9px;
|
||||
padding: 7px 14px;
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&.active {
|
||||
background: color-mix(in srgb, var(--primary) 18%, var(--card-bg));
|
||||
color: var(--text-primary, #1c2230);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary) 45%, transparent);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,9 +76,9 @@
|
||||
border: 1px solid color-mix(in srgb, var(--border-color, #d2d7df) 95%, transparent);
|
||||
background: var(--bg-secondary, #f3f5f8);
|
||||
color: var(--text-secondary, #566074);
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
padding: 0 10px;
|
||||
min-height: 36px;
|
||||
min-height: 34px;
|
||||
box-sizing: border-box;
|
||||
|
||||
svg {
|
||||
@@ -103,8 +103,8 @@
|
||||
color: var(--text-primary, #1c2230);
|
||||
font-size: 13px;
|
||||
min-width: 0;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
font-family: "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif;
|
||||
appearance: none;
|
||||
}
|
||||
@@ -129,8 +129,8 @@
|
||||
border: 1px solid var(--border-color);
|
||||
background: transparent;
|
||||
color: var(--text-secondary, #5f6674);
|
||||
border-radius: 10px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -152,8 +152,8 @@
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary, #f3f5f8);
|
||||
color: var(--text-secondary, #5f6674);
|
||||
border-radius: 10px;
|
||||
height: 34px;
|
||||
border-radius: 8px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
display: inline-flex;
|
||||
@@ -184,9 +184,9 @@
|
||||
}
|
||||
|
||||
.stream-state {
|
||||
height: 120px;
|
||||
height: 104px;
|
||||
border: 1px dashed var(--border-color);
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--card-bg);
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
@@ -204,7 +204,7 @@
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent);
|
||||
border-radius: 16px;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--card-bg) 94%, transparent);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
@@ -226,7 +226,7 @@
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
align-content: flex-start;
|
||||
padding: 10px 0 2px;
|
||||
padding: 8px 0 2px;
|
||||
width: var(--stream-grid-width);
|
||||
min-width: var(--stream-grid-width);
|
||||
max-width: var(--stream-grid-width);
|
||||
@@ -257,7 +257,7 @@
|
||||
height: 100%;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 14px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.16s ease;
|
||||
position: relative;
|
||||
@@ -288,7 +288,7 @@
|
||||
z-index: 4;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 9px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid color-mix(in srgb, var(--danger) 48%, var(--border-color));
|
||||
color: var(--danger);
|
||||
background: color-mix(in srgb, var(--bg-secondary) 90%, transparent);
|
||||
@@ -297,9 +297,9 @@
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transform: translateY(-2px) scale(0.96);
|
||||
transform: none;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.16s ease, transform 0.16s ease;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.floating-info {
|
||||
@@ -317,7 +317,7 @@
|
||||
.media-card:hover .floating-delete,
|
||||
.media-card:focus-within .floating-delete {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
transform: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@@ -329,7 +329,7 @@
|
||||
border: 1px solid color-mix(in srgb, var(--primary) 45%, var(--border-color));
|
||||
background: color-mix(in srgb, var(--bg-secondary) 90%, transparent);
|
||||
color: var(--text-primary);
|
||||
border-radius: 9px;
|
||||
border-radius: 6px;
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
font-size: 11px;
|
||||
@@ -408,15 +408,15 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(140deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.04));
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -40%;
|
||||
background: linear-gradient(105deg, transparent 35%, rgba(255, 255, 255, 0.35) 50%, transparent 65%);
|
||||
animation: decrypt-sheen 1.6s linear infinite;
|
||||
background: none;
|
||||
animation: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@@ -429,16 +429,14 @@
|
||||
border-radius: 999px;
|
||||
border: 2px solid rgba(15, 23, 42, 0.2);
|
||||
border-top-color: color-mix(in srgb, var(--primary) 78%, #ffffff);
|
||||
animation: decrypt-spin 0.85s linear infinite, decrypt-pulse 1.2s ease-in-out infinite;
|
||||
box-shadow:
|
||||
0 0 0 8px rgba(255, 255, 255, 0.26),
|
||||
0 10px 24px rgba(15, 23, 42, 0.12);
|
||||
animation: decrypt-spin 0.85s linear infinite;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
padding: 9px 10px 8px;
|
||||
min-height: 66px;
|
||||
padding: 8px 10px;
|
||||
min-height: 58px;
|
||||
margin-top: auto;
|
||||
cursor: pointer;
|
||||
border-top: 1px solid color-mix(in srgb, var(--border-color) 70%, transparent);
|
||||
@@ -517,8 +515,8 @@
|
||||
width: min(420px, calc(100% - 32px));
|
||||
background: var(--dialog-surface);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 90%, transparent);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.28);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1698,6 +1698,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
if (selected) return
|
||||
setQuoteLayout(option.value)
|
||||
await configService.setQuoteLayout(option.value)
|
||||
window.dispatchEvent(new CustomEvent('quote-layout-changed', { detail: option.value }))
|
||||
showMessage(option.successMessage, true)
|
||||
}}
|
||||
role="radio"
|
||||
@@ -1711,16 +1712,30 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
{isQuoteBottom ? (
|
||||
<>
|
||||
<div className="message-text">拍得真不错!</div>
|
||||
<div className="quoted-message">
|
||||
<span className="quoted-sender">张三</span>
|
||||
<span className="quoted-text">那天去爬山的照片...</span>
|
||||
<div className="ambient-reply-wrapper">
|
||||
<div className="reply-anchor">
|
||||
<svg className="reply-anchor-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="9 14 4 9 9 4" />
|
||||
<path d="M20 20v-7a4 4 0 0 0-4-4H4" />
|
||||
</svg>
|
||||
<span className="reply-anchor-name">张三</span>
|
||||
<span className="reply-anchor-sep">·</span>
|
||||
<span className="reply-anchor-excerpt">那天去爬山的照片...</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="quoted-message">
|
||||
<span className="quoted-sender">张三</span>
|
||||
<span className="quoted-text">那天去爬山的照片...</span>
|
||||
<div className="ambient-reply-wrapper">
|
||||
<div className="reply-anchor">
|
||||
<svg className="reply-anchor-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="9 14 4 9 9 4" />
|
||||
<path d="M20 20v-7a4 4 0 0 0-4-4H4" />
|
||||
</svg>
|
||||
<span className="reply-anchor-name">张三</span>
|
||||
<span className="reply-anchor-sep">·</span>
|
||||
<span className="reply-anchor-excerpt">那天去爬山的照片...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="message-text">拍得真不错!</div>
|
||||
</>
|
||||
@@ -2212,23 +2227,32 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
/>
|
||||
</div>
|
||||
<div className="anti-revoke-toolbar-actions">
|
||||
<div className="anti-revoke-btn-group">
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => void handleRefreshAntiRevokeStatus()} disabled={busy}>
|
||||
<RefreshCw size={14} /> {isAntiRevokeRefreshing ? '刷新中...' : '刷新状态'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="anti-revoke-btn-group">
|
||||
<button className="btn btn-secondary btn-sm" onClick={selectAllFiltered} disabled={busy || filteredSessionIds.length === 0 || allFilteredSelected}>
|
||||
全选
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={clearSelection} disabled={busy || selectedCount === 0}>
|
||||
清空选择
|
||||
</button>
|
||||
</div>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => void handleRefreshAntiRevokeStatus()} disabled={busy}>
|
||||
<RefreshCw size={14} /> {isAntiRevokeRefreshing ? '刷新中...' : '刷新状态'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="anti-revoke-selection-strip">
|
||||
<div className="anti-revoke-selected-count">
|
||||
<span>已选 <strong>{selectedCount}</strong> 个会话</span>
|
||||
<span>筛选命中 <strong>{selectedInFilteredCount}</strong> / {filteredSessionIds.length}</span>
|
||||
</div>
|
||||
<div className="anti-revoke-selection-actions">
|
||||
<button className="btn btn-secondary btn-sm" onClick={selectAllFiltered} disabled={busy || filteredSessionIds.length === 0 || allFilteredSelected}>
|
||||
全选
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={clearSelection} disabled={busy || selectedCount === 0}>
|
||||
清空选择
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="anti-revoke-batch-actions">
|
||||
<div className="anti-revoke-batch-copy">
|
||||
<span className="anti-revoke-section-label">批量部署</span>
|
||||
<span>对已选会话执行防撤回安装或卸载</span>
|
||||
</div>
|
||||
<div className="anti-revoke-btn-group anti-revoke-batch-btns">
|
||||
<button className="btn btn-primary btn-sm" onClick={() => void handleInstallAntiRevokeTriggers()} disabled={busy || selectedCount === 0}>
|
||||
{isAntiRevokeInstalling ? '安装中...' : '批量安装'}
|
||||
@@ -2237,10 +2261,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
{isAntiRevokeUninstalling ? '卸载中...' : '批量卸载'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="anti-revoke-selected-count">
|
||||
<span>已选 <strong>{selectedCount}</strong> 个会话</span>
|
||||
<span>筛选命中 <strong>{selectedInFilteredCount}</strong> / {filteredSessionIds.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2539,11 +2559,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
|
||||
const renderModelsTab = () => (
|
||||
<div className="tab-content">
|
||||
<div className="form-group">
|
||||
<label>模型管理</label>
|
||||
<span className="form-hint">管理语音识别模型</span>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>语音识别模型 (Whisper)</label>
|
||||
<span className="form-hint">用于语音消息转文字功能</span>
|
||||
@@ -2561,12 +2576,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
) : (
|
||||
<span className="status-indicator warning">未安装</span>
|
||||
)}
|
||||
{resolvedWhisperModelPath && (
|
||||
<div className="model-path-block">
|
||||
<span className="path-label">模型目录</span>
|
||||
<div className="path-text" title={resolvedWhisperModelPath}>{resolvedWhisperModelPath}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{(!whisperModelStatus?.exists || isWhisperDownloading) && (
|
||||
@@ -2597,25 +2606,21 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="sub-setting">
|
||||
<div className="sub-label">自定义模型目录</div>
|
||||
<div className="path-selector">
|
||||
<div className="model-directory-control">
|
||||
<input
|
||||
type="text"
|
||||
value={whisperModelDir}
|
||||
value={resolvedWhisperModelPath}
|
||||
readOnly
|
||||
placeholder="默认目录"
|
||||
title={resolvedWhisperModelPath || '默认目录'}
|
||||
/>
|
||||
<button className="btn-icon" onClick={handleSelectWhisperModelDir} title="选择目录">
|
||||
<FolderOpen size={18} />
|
||||
<button className="btn btn-secondary btn-sm" onClick={handleSelectWhisperModelDir} title="选择自定义目录">
|
||||
<FolderOpen size={14} /> 选择自定义目录
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={handleResetWhisperModelDir} disabled={!whisperModelDir} title="恢复默认">
|
||||
<RotateCcw size={14} /> 恢复默认
|
||||
</button>
|
||||
{whisperModelDir && (
|
||||
<button className="btn-icon danger" onClick={handleResetWhisperModelDir} title="重置为默认">
|
||||
<RotateCcw size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/* Global Variables */
|
||||
:root {
|
||||
--sns-max-width: 800px;
|
||||
--sns-panel-width: 380px;
|
||||
--sns-max-width: 920px;
|
||||
--sns-panel-width: 320px;
|
||||
--sns-bg-color: var(--bg-primary);
|
||||
--sns-card-bg: var(--bg-secondary);
|
||||
--sns-border-radius-lg: 16px;
|
||||
--sns-border-radius-md: 12px;
|
||||
--sns-border-radius-sm: 8px;
|
||||
--sns-card-bg: transparent;
|
||||
--sns-border-radius-lg: 10px;
|
||||
--sns-border-radius-md: 8px;
|
||||
--sns-border-radius-sm: 6px;
|
||||
--sns-media-cell: 88px;
|
||||
}
|
||||
|
||||
.sns-page-layout {
|
||||
@@ -33,7 +34,7 @@
|
||||
.sns-feed-container {
|
||||
width: 100%;
|
||||
max-width: var(--sns-max-width);
|
||||
padding: 10px 24px 12px 24px;
|
||||
padding: 14px 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
@@ -45,13 +46,12 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
padding: 0 4px;
|
||||
gap: 16px;
|
||||
margin-bottom: 0;
|
||||
padding: 2px 2px 10px;
|
||||
z-index: 2;
|
||||
background: var(--sns-bg-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-top: 4px;
|
||||
padding-bottom: 6px;
|
||||
|
||||
.feed-header-main {
|
||||
display: flex;
|
||||
@@ -70,9 +70,9 @@
|
||||
.feed-stats-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.jump-calendar-anchor {
|
||||
@@ -186,24 +186,28 @@
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: var(--bg-tertiary);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--sns-border-radius-sm);
|
||||
padding: 8px;
|
||||
padding: 0;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--hover-bg);
|
||||
color: var(--primary);
|
||||
transform: scale(1.05);
|
||||
background: var(--bg-hover);
|
||||
border-color: color-mix(in srgb, var(--text-tertiary) 34%, var(--border-color));
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.spinning {
|
||||
@@ -217,20 +221,21 @@
|
||||
gap: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--sns-border-radius-sm);
|
||||
background: var(--bg-tertiary);
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
padding: 8px 10px;
|
||||
min-height: 32px;
|
||||
padding: 0 9px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--hover-bg);
|
||||
color: var(--primary);
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--primary);
|
||||
background: rgba(var(--primary-rgb), 0.08);
|
||||
background: rgba(var(--primary-rgb), 0.07);
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
@@ -256,19 +261,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sns-posts-scroll {
|
||||
.sns-posts-stage {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sns-posts-scroll {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding-top: 16px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.sns-post-row {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.feed-contact-filter-bar {
|
||||
margin: 10px 4px 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--primary) 28%, var(--border-color));
|
||||
border-radius: 12px;
|
||||
background: rgba(var(--primary-rgb), 0.08);
|
||||
margin: 8px 0 0;
|
||||
padding: 7px 9px;
|
||||
border: 1px solid color-mix(in srgb, var(--primary) 20%, var(--border-color));
|
||||
border-radius: var(--sns-border-radius-md);
|
||||
background: rgba(var(--primary-rgb), 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -281,7 +295,7 @@
|
||||
}
|
||||
|
||||
.feed-contact-filter-summary {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
min-width: 0;
|
||||
@@ -293,7 +307,7 @@
|
||||
background: transparent;
|
||||
color: var(--primary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
@@ -308,7 +322,7 @@
|
||||
.posts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* =========================================
|
||||
@@ -316,19 +330,21 @@
|
||||
========================================= */
|
||||
.sns-post-item {
|
||||
background: var(--sns-card-bg);
|
||||
border-radius: var(--sns-border-radius-lg);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 20px;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent);
|
||||
padding: 10px 6px 12px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02);
|
||||
gap: 12px;
|
||||
transition: background 0.15s ease;
|
||||
box-shadow: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.06);
|
||||
background: color-mix(in srgb, var(--bg-secondary) 72%, transparent);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
&.post-deleted {
|
||||
@@ -341,9 +357,9 @@
|
||||
gap: 4px;
|
||||
background: rgba(255, 77, 79, 0.1);
|
||||
color: #ff4d4f;
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 3px 8px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
@@ -362,12 +378,12 @@
|
||||
}
|
||||
|
||||
.avatar-trigger {
|
||||
border-radius: 12px;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
border-radius: var(--sns-border-radius-md);
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.1);
|
||||
opacity: 0.86;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@@ -386,7 +402,8 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
gap: 10px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.post-author-info {
|
||||
display: flex;
|
||||
@@ -395,12 +412,10 @@
|
||||
overflow: hidden;
|
||||
|
||||
.author-name {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
/* Changed to primary from accent for cleaner look, or keep accent */
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
margin-bottom: 2px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.author-name-trigger {
|
||||
@@ -430,13 +445,14 @@
|
||||
.post-time {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
line-height: 1.25;
|
||||
}
|
||||
}
|
||||
|
||||
.post-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -451,9 +467,11 @@
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
background: none;
|
||||
border: 1px solid var(--border-color);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -462,8 +480,7 @@
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--text-secondary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
&.delete-btn:hover {
|
||||
@@ -751,20 +768,20 @@
|
||||
}
|
||||
|
||||
.post-text {
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
line-height: 1.48;
|
||||
color: var(--text-primary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
margin-bottom: 12px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.post-location {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
margin: -4px 0 12px;
|
||||
font-size: 13px;
|
||||
margin: -2px 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: var(--text-secondary);
|
||||
|
||||
@@ -780,7 +797,7 @@
|
||||
}
|
||||
|
||||
.post-media-container {
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.post-link-card {
|
||||
@@ -788,14 +805,14 @@
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--sns-border-radius-md);
|
||||
padding: 10px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: all 0.2s;
|
||||
margin-bottom: 12px;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--primary-rgb), 0.08);
|
||||
@@ -803,8 +820,8 @@
|
||||
}
|
||||
|
||||
.link-thumb {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--sns-border-radius-sm);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
@@ -830,7 +847,7 @@
|
||||
min-width: 0;
|
||||
|
||||
.link-title {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
@@ -839,7 +856,7 @@
|
||||
}
|
||||
|
||||
.link-url {
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
@@ -851,15 +868,15 @@
|
||||
}
|
||||
|
||||
.post-interactions {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed var(--border-color);
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
font-size: 12px;
|
||||
|
||||
.likes-block {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
|
||||
@@ -871,11 +888,11 @@
|
||||
.comments-block {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--sns-border-radius-sm);
|
||||
padding: 8px 12px;
|
||||
padding: 6px 8px;
|
||||
|
||||
.comment-row {
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 3px;
|
||||
line-height: 1.45;
|
||||
color: var(--text-secondary);
|
||||
|
||||
&:last-child {
|
||||
@@ -912,13 +929,13 @@
|
||||
========================================= */
|
||||
.sns-media-grid {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
|
||||
&.grid-1 .sns-media-item {
|
||||
width: fit-content;
|
||||
height: auto;
|
||||
max-width: 300px;
|
||||
max-height: 480px;
|
||||
max-width: 270px;
|
||||
max-height: 340px;
|
||||
aspect-ratio: auto;
|
||||
border-radius: var(--sns-border-radius-md);
|
||||
|
||||
@@ -930,7 +947,7 @@
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
max-height: 480px;
|
||||
max-height: 340px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
@@ -941,26 +958,25 @@
|
||||
|
||||
&.grid-2,
|
||||
&.grid-4 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
max-width: 320px;
|
||||
grid-template-columns: repeat(2, var(--sns-media-cell));
|
||||
max-width: calc((var(--sns-media-cell) * 2) + 4px);
|
||||
}
|
||||
|
||||
&.grid-3,
|
||||
&.grid-6,
|
||||
&.grid-9 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
max-width: 320px;
|
||||
grid-template-columns: repeat(3, var(--sns-media-cell));
|
||||
max-width: calc((var(--sns-media-cell) * 3) + 8px);
|
||||
}
|
||||
|
||||
.sns-media-item {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
border-radius: var(--sns-border-radius-md);
|
||||
/* Consistent radius for grid items */
|
||||
border-radius: var(--sns-border-radius-sm);
|
||||
overflow: hidden;
|
||||
cursor: zoom-in;
|
||||
background: var(--bg-tertiary);
|
||||
transition: opacity 0.2s;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
@@ -972,7 +988,7 @@
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
animation: fade-in 0.3s ease;
|
||||
animation: fade-in 0.2s ease;
|
||||
}
|
||||
|
||||
.media-badge {
|
||||
@@ -984,7 +1000,6 @@
|
||||
height: 32px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -993,8 +1008,8 @@
|
||||
/* Let clicks pass through badge to item */
|
||||
|
||||
&.live {
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
left: auto;
|
||||
transform: none;
|
||||
width: 24px;
|
||||
@@ -1015,30 +1030,27 @@
|
||||
gap: 8px;
|
||||
z-index: 10;
|
||||
font-size: 12px;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.media-download-btn {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
right: 6px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
bottom: 5px;
|
||||
right: 5px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.2s;
|
||||
transition: opacity 0.15s ease, background 0.15s ease;
|
||||
z-index: 5;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Increase click area */
|
||||
@@ -1092,8 +1104,8 @@
|
||||
background: var(--bg-secondary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
gap: 24px;
|
||||
padding: 16px 14px;
|
||||
gap: 14px;
|
||||
z-index: 10;
|
||||
|
||||
.filter-header {
|
||||
@@ -1102,8 +1114,8 @@
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -1112,10 +1124,16 @@
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--text-tertiary);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: var(--sns-border-radius-sm);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
animation: spin 0.5s ease;
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1123,30 +1141,30 @@
|
||||
.filter-widgets {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
gap: 14px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.filter-widget {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--sns-border-radius-md);
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
|
||||
.widget-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
|
||||
svg {
|
||||
color: var(--primary);
|
||||
opacity: 0.8;
|
||||
color: var(--text-tertiary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.badge {
|
||||
@@ -1161,7 +1179,7 @@
|
||||
.widget-header-summary {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
color: var(--text-tertiary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1183,17 +1201,16 @@
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--sns-border-radius-sm);
|
||||
padding: 10px 30px 10px 12px;
|
||||
height: 34px;
|
||||
padding: 0 30px 0 10px;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
|
||||
&:focus {
|
||||
background: var(--bg-primary);
|
||||
background: var(--bg-secondary);
|
||||
border-color: transparent;
|
||||
/* Explicitly transparent */
|
||||
outline: none;
|
||||
/* Ensure no outline */
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
@@ -1218,14 +1235,16 @@
|
||||
min-height: 300px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 12px;
|
||||
|
||||
.widget-header {
|
||||
padding: 16px 16px 12px 16px;
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.contact-search-bar {
|
||||
padding: 0 16px 12px 16px;
|
||||
padding: 0 0 10px;
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
@@ -1233,22 +1252,23 @@
|
||||
width: 100%;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 8px 32px 8px 12px;
|
||||
border-radius: var(--sns-border-radius-sm);
|
||||
height: 32px;
|
||||
padding: 0 32px 0 10px;
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background: var(--bg-primary);
|
||||
border-color: var(--primary);
|
||||
background: var(--bg-secondary);
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 28px;
|
||||
top: 8px;
|
||||
right: 10px;
|
||||
top: 9px;
|
||||
color: var(--text-tertiary);
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
@@ -1256,8 +1276,8 @@
|
||||
|
||||
.clear-icon {
|
||||
position: absolute;
|
||||
right: 28px;
|
||||
top: 8px;
|
||||
right: 10px;
|
||||
top: 9px;
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -1270,7 +1290,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 10px 16px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px dashed color-mix(in srgb, var(--border-color) 72%, transparent);
|
||||
}
|
||||
|
||||
@@ -1287,9 +1307,9 @@
|
||||
background: color-mix(in srgb, var(--bg-primary) 84%, rgba(var(--primary-rgb), 0.08));
|
||||
color: var(--text-secondary);
|
||||
border-radius: 999px;
|
||||
padding: 5px 10px;
|
||||
padding: 4px 9px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
|
||||
@@ -1312,7 +1332,7 @@
|
||||
}
|
||||
|
||||
.contact-count-progress {
|
||||
padding: 8px 16px 10px;
|
||||
padding: 7px 0 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
border-bottom: 1px dashed color-mix(in srgb, var(--border-color) 70%, transparent);
|
||||
@@ -1320,7 +1340,7 @@
|
||||
}
|
||||
|
||||
.contact-interaction-hint {
|
||||
padding: 10px 16px 0;
|
||||
padding: 8px 0 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-tertiary);
|
||||
@@ -1328,23 +1348,20 @@
|
||||
|
||||
.contact-list-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
min-height: 0;
|
||||
padding: 8px 0 0;
|
||||
|
||||
.contact-list-virtuoso {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.contact-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: var(--sns-border-radius-md);
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
gap: 4px;
|
||||
min-height: 40px;
|
||||
border-radius: var(--sns-border-radius-sm);
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&.is-selected .contact-main-btn {
|
||||
background: rgba(var(--primary-rgb), 0.06);
|
||||
@@ -1362,7 +1379,7 @@
|
||||
}
|
||||
|
||||
.contact-select-btn {
|
||||
width: 32px;
|
||||
width: 28px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
@@ -1373,7 +1390,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--primary-rgb), 0.1);
|
||||
@@ -1390,17 +1407,17 @@
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--sns-border-radius-md);
|
||||
gap: 8px;
|
||||
padding: 5px 6px;
|
||||
border-radius: var(--sns-border-radius-sm);
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, border-color 0.2s ease;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background: var(--hover-bg);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1412,7 +1429,7 @@
|
||||
gap: 2px;
|
||||
|
||||
.contact-name {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -1422,7 +1439,7 @@
|
||||
|
||||
.contact-post-count-wrap {
|
||||
margin-left: 8px;
|
||||
min-width: 46px;
|
||||
min-width: 42px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
@@ -1430,7 +1447,7 @@
|
||||
}
|
||||
|
||||
.contact-post-count {
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
@@ -1453,9 +1470,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px 14px;
|
||||
padding: 10px 0 0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
background: color-mix(in srgb, var(--bg-primary) 86%, var(--bg-secondary));
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.contact-batch-summary {
|
||||
@@ -1470,8 +1487,8 @@
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
border-radius: var(--sns-border-radius-md);
|
||||
height: 32px;
|
||||
border-radius: var(--sns-border-radius-sm);
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1483,7 +1500,7 @@
|
||||
|
||||
&:hover {
|
||||
border-color: var(--text-tertiary);
|
||||
background: var(--hover-bg);
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -1508,9 +1525,9 @@
|
||||
========================================= */
|
||||
.status-indicator {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
padding: 10px 8px;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -1520,8 +1537,10 @@
|
||||
background: rgba(var(--primary-rgb), 0.1);
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
border: 1px solid color-mix(in srgb, var(--primary) 18%, var(--border-color));
|
||||
border-radius: var(--sns-border-radius-sm);
|
||||
margin: 0 24px;
|
||||
margin: 0 0 8px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1530,45 +1549,44 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120px 0;
|
||||
padding: 72px 0;
|
||||
|
||||
.loading-pulse {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
gap: 12px;
|
||||
|
||||
.pulse-circle {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary, #576b95);
|
||||
opacity: 0.25;
|
||||
animation: pulse-ring 1.4s ease-in-out infinite;
|
||||
opacity: 0.4;
|
||||
animation: pulse-ring 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0.15;
|
||||
transform: scale(0.85);
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.0);
|
||||
opacity: 0.35;
|
||||
transform: scale(1);
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0.15;
|
||||
transform: scale(0.85);
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1576,20 +1594,25 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 60px 0;
|
||||
padding: 56px 0;
|
||||
color: var(--text-tertiary);
|
||||
|
||||
.no-results-icon {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 10px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.reset-inline {
|
||||
margin-top: 16px;
|
||||
margin-top: 12px;
|
||||
background: none;
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
|
||||
@@ -2760,7 +2783,7 @@
|
||||
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--hover-bg);
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -2902,7 +2925,7 @@
|
||||
color: var(--text-secondary);
|
||||
|
||||
&:hover {
|
||||
background: var(--hover-bg);
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useLayoutEffect, useState, useRef, useCallback, useMemo } from 'react'
|
||||
import { RefreshCw, Search, X, Download, FolderOpen, FileJson, FileText, Image, CheckCircle, AlertCircle, Calendar, Info, Shield, ShieldOff, Loader2, Pause, Play, Square } from 'lucide-react'
|
||||
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
|
||||
import './SnsPage.scss'
|
||||
import { SnsPost } from '../types/sns'
|
||||
import { SnsPostItem } from '../components/Sns/SnsPostItem'
|
||||
@@ -229,7 +230,8 @@ export default function SnsPage() {
|
||||
const [cacheMigrationDone, setCacheMigrationDone] = useState(false)
|
||||
const [cacheMigrationError, setCacheMigrationError] = useState<string | null>(null)
|
||||
|
||||
const postsContainerRef = useRef<HTMLDivElement>(null)
|
||||
const postsContainerRef = useRef<HTMLElement | null>(null)
|
||||
const postsVirtuosoRef = useRef<VirtuosoHandle | null>(null)
|
||||
const jumpCalendarWrapRef = useRef<HTMLDivElement | null>(null)
|
||||
const [hasNewer, setHasNewer] = useState(false)
|
||||
const [loadingNewer, setLoadingNewer] = useState(false)
|
||||
@@ -1110,6 +1112,7 @@ export default function SnsPage() {
|
||||
setHasNewer(false);
|
||||
}
|
||||
|
||||
postsVirtuosoRef.current?.scrollToIndex({ index: 0, align: 'start', behavior: 'auto' })
|
||||
if (postsContainerRef.current) {
|
||||
postsContainerRef.current.scrollTop = 0
|
||||
}
|
||||
@@ -1764,23 +1767,61 @@ export default function SnsPage() {
|
||||
loadPosts({ reset: true })
|
||||
}, [loadPosts, selectedContactUsernamesKey])
|
||||
|
||||
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.currentTarget
|
||||
if (scrollHeight - scrollTop - clientHeight < 400 && hasMore && !loading && !loadingNewer) {
|
||||
loadPosts({ direction: 'older' })
|
||||
}
|
||||
if (scrollTop < 10 && hasNewer && !loading && !loadingNewer) {
|
||||
loadPosts({ direction: 'newer' })
|
||||
}
|
||||
}
|
||||
const handlePostsEndReached = useCallback(() => {
|
||||
if (!hasMore || loading || loadingNewer) return
|
||||
void loadPosts({ direction: 'older' })
|
||||
}, [hasMore, loadPosts, loading, loadingNewer])
|
||||
|
||||
const handleWheel = (e: React.WheelEvent<HTMLDivElement>) => {
|
||||
const container = postsContainerRef.current
|
||||
if (!container) return
|
||||
if (e.deltaY < -20 && container.scrollTop <= 0 && hasNewer && !loading && !loadingNewer) {
|
||||
loadPosts({ direction: 'newer' })
|
||||
}
|
||||
}
|
||||
const renderPostItem = useCallback((_: number, post: SnsPost) => (
|
||||
<div className="sns-post-row">
|
||||
<SnsPostItem
|
||||
post={{ ...post, isProtected: triggerInstalled === true }}
|
||||
onPreview={(src, isVideo, liveVideoPath) => {
|
||||
if (isVideo) {
|
||||
void window.electronAPI.window.openVideoPlayerWindow(src)
|
||||
} else {
|
||||
void window.electronAPI.window.openImageViewerWindow(src, liveVideoPath || undefined)
|
||||
}
|
||||
}}
|
||||
onDebug={(p) => setDebugPost(p)}
|
||||
onDelete={handlePostDelete}
|
||||
onOpenAuthorPosts={openAuthorTimeline}
|
||||
/>
|
||||
</div>
|
||||
), [handlePostDelete, openAuthorTimeline, triggerInstalled])
|
||||
|
||||
const snsVirtuosoComponents = useMemo(() => ({
|
||||
Header: () => (
|
||||
<>
|
||||
{loadingNewer && (
|
||||
<div className="status-indicator loading-newer">
|
||||
<RefreshCw size={14} className="spinning" />
|
||||
<span>正在检查更新动态</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loadingNewer && hasNewer && (
|
||||
<button type="button" className="status-indicator newer-hint" onClick={() => void loadPosts({ direction: 'newer' })}>
|
||||
有新动态,点击查看
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
Footer: () => (
|
||||
<>
|
||||
{loading && visiblePosts.length > 0 && (
|
||||
<div className="status-indicator loading-more">
|
||||
<RefreshCw size={14} className="spinning" />
|
||||
<span>正在加载更多</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasMore && visiblePosts.length > 0 && (
|
||||
<div className="status-indicator no-more">已加载全部动态</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}), [hasMore, hasNewer, loadPosts, loading, loadingNewer, visiblePosts.length])
|
||||
|
||||
return (
|
||||
<div className="sns-page-layout">
|
||||
@@ -1940,62 +1981,19 @@ export default function SnsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="sns-posts-scroll" onScroll={handleScroll} onWheel={handleWheel} ref={postsContainerRef}>
|
||||
{loadingNewer && (
|
||||
<div className="status-indicator loading-newer">
|
||||
<RefreshCw size={16} className="spinning" />
|
||||
<span>正在检查更新的动态...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loadingNewer && hasNewer && (
|
||||
<div className="status-indicator newer-hint" onClick={() => loadPosts({ direction: 'newer' })}>
|
||||
有新动态,点击查看
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="posts-list">
|
||||
{visiblePosts.map(post => (
|
||||
<SnsPostItem
|
||||
key={post.id}
|
||||
post={{ ...post, isProtected: triggerInstalled === true }}
|
||||
onPreview={(src, isVideo, liveVideoPath) => {
|
||||
if (isVideo) {
|
||||
void window.electronAPI.window.openVideoPlayerWindow(src)
|
||||
} else {
|
||||
void window.electronAPI.window.openImageViewerWindow(src, liveVideoPath || undefined)
|
||||
}
|
||||
}}
|
||||
onDebug={(p) => setDebugPost(p)}
|
||||
onDelete={handlePostDelete}
|
||||
onOpenAuthorPosts={openAuthorTimeline}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="sns-posts-stage">
|
||||
{loading && visiblePosts.length === 0 && (
|
||||
<div className="initial-loading">
|
||||
<div className="loading-pulse">
|
||||
<div className="pulse-circle"></div>
|
||||
<span>正在加载朋友圈...</span>
|
||||
<span>正在加载动态</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && visiblePosts.length > 0 && (
|
||||
<div className="status-indicator loading-more">
|
||||
<RefreshCw size={16} className="spinning" />
|
||||
<span>正在加载更多...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasMore && visiblePosts.length > 0 && (
|
||||
<div className="status-indicator no-more">或许过往已无可溯洄,但好在还有可以与你相遇的明天</div>
|
||||
)}
|
||||
|
||||
{!loading && visiblePosts.length === 0 && (
|
||||
<div className="no-results">
|
||||
<div className="no-results-icon"><Search size={48} /></div>
|
||||
<div className="no-results-icon"><Search size={28} /></div>
|
||||
<p>未找到相关动态</p>
|
||||
{(searchKeyword || jumpTargetDate || selectedContactUsernames.length > 0) && (
|
||||
<button onClick={() => {
|
||||
@@ -2008,6 +2006,24 @@ export default function SnsPage() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{visiblePosts.length > 0 && (
|
||||
<Virtuoso
|
||||
ref={postsVirtuosoRef}
|
||||
className="sns-posts-scroll"
|
||||
data={visiblePosts}
|
||||
computeItemKey={(_, post) => post.id}
|
||||
itemContent={renderPostItem}
|
||||
components={snsVirtuosoComponents}
|
||||
endReached={handlePostsEndReached}
|
||||
scrollerRef={(ref) => {
|
||||
postsContainerRef.current = ref instanceof HTMLElement ? ref : null
|
||||
}}
|
||||
defaultItemHeight={220}
|
||||
increaseViewportBy={{ top: 260, bottom: 520 }}
|
||||
overscan={{ main: 900, reverse: 480 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,400 +1,213 @@
|
||||
// CSS 变量 - 主题
|
||||
// WeFlow — ChatGPT-inspired design token system
|
||||
// Accent colors per theme, backgrounds unified across themes.
|
||||
@use './chat-patterns.scss';
|
||||
|
||||
:root {
|
||||
// 颜色
|
||||
--primary: #8B7355;
|
||||
--primary-hover: #7A6548;
|
||||
--primary-light: rgba(139, 115, 85, 0.1);
|
||||
--danger: #dc3545;
|
||||
--warning: #ffc107;
|
||||
// =============================================
|
||||
// Light mode (default)
|
||||
// =============================================
|
||||
:root,
|
||||
[data-mode="light"] {
|
||||
// ---- Accent / Brand (default = ChatGPT green) ----
|
||||
--primary: #10a37f;
|
||||
--primary-rgb: 16, 163, 127;
|
||||
--primary-hover: #0d8a6c;
|
||||
--primary-light: rgba(16, 163, 127, 0.1);
|
||||
--primary-gradient: linear-gradient(135deg, #10a37f 0%, #1ab893 100%);
|
||||
|
||||
// 背景
|
||||
--bg-primary: #F0EEE9;
|
||||
--bg-secondary: rgba(255, 255, 255, 0.7);
|
||||
--bg-tertiary: rgba(0, 0, 0, 0.03);
|
||||
--bg-hover: rgba(0, 0, 0, 0.05);
|
||||
--danger: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
|
||||
// 文字
|
||||
--text-primary: #3d3d3d;
|
||||
--text-secondary: #666666;
|
||||
--text-tertiary: #999999;
|
||||
// ---- Backgrounds (same for all themes) ----
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f9f9f9;
|
||||
--bg-secondary-solid: #f9f9f9;
|
||||
--bg-tertiary: #f0f0f0;
|
||||
--bg-hover: #ececec;
|
||||
--bg-sidebar: #f9f9f9;
|
||||
--bg-gradient: linear-gradient(180deg, #ffffff 0%, #f7f7f8 100%);
|
||||
|
||||
// 边框
|
||||
// ---- Text ----
|
||||
--text-primary: #0d0d0d;
|
||||
--text-secondary: #6e6e80;
|
||||
--text-tertiary: #8e8ea0;
|
||||
|
||||
// ---- Borders ----
|
||||
--border-color: rgba(0, 0, 0, 0.08);
|
||||
--border-radius: 9999px;
|
||||
--border-radius: 12px;
|
||||
|
||||
// 阴影
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
// ---- Shadows ----
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||
|
||||
// 侧边栏
|
||||
--sidebar-width: 220px;
|
||||
|
||||
// 主题渐变
|
||||
--bg-gradient: linear-gradient(135deg, #F0EEE9 0%, #E8E6E1 100%);
|
||||
--primary-gradient: linear-gradient(135deg, #8B7355 0%, #A68B5B 100%);
|
||||
|
||||
// 卡片背景
|
||||
--card-bg: rgba(255, 255, 255, 0.7);
|
||||
--card-inner-bg: #FAFAF7;
|
||||
// ---- Cards ----
|
||||
--card-bg: #f7f7f8;
|
||||
--card-inner-bg: #ffffff;
|
||||
--sent-card-bg: var(--primary);
|
||||
|
||||
// primary 色上方的前景文字色(大多数主题为白色)
|
||||
--on-primary: white;
|
||||
// ---- On-primary foreground ----
|
||||
--on-primary: #ffffff;
|
||||
|
||||
// ---- Layout ----
|
||||
--sidebar-width: 200px;
|
||||
}
|
||||
|
||||
// ==================== 浅色主题 ====================
|
||||
// =============================================
|
||||
// Dark mode
|
||||
// =============================================
|
||||
[data-mode="dark"] {
|
||||
--primary: #10a37f;
|
||||
--primary-rgb: 16, 163, 127;
|
||||
--primary-hover: #1ab893;
|
||||
--primary-light: rgba(16, 163, 127, 0.15);
|
||||
--primary-gradient: linear-gradient(135deg, #10a37f 0%, #1ab893 100%);
|
||||
|
||||
// 云上舞白主题 (默认)
|
||||
[data-theme="cloud-dancer"][data-mode="light"],
|
||||
[data-theme="cloud-dancer"]:not([data-mode]) {
|
||||
--danger: #f87171;
|
||||
--warning: #fbbf24;
|
||||
|
||||
--bg-primary: #212121;
|
||||
--bg-secondary: #2f2f2f;
|
||||
--bg-secondary-solid: #2f2f2f;
|
||||
--bg-tertiary: #383838;
|
||||
--bg-hover: #383838;
|
||||
--bg-sidebar: #171717;
|
||||
--bg-gradient: linear-gradient(180deg, #212121 0%, #1a1a1a 100%);
|
||||
|
||||
--text-primary: #ececec;
|
||||
--text-secondary: #b4b4b4;
|
||||
--text-tertiary: #676767;
|
||||
|
||||
--border-color: rgba(255, 255, 255, 0.08);
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
|
||||
--card-bg: #2f2f2f;
|
||||
--card-inner-bg: #353535;
|
||||
--sent-card-bg: var(--primary);
|
||||
--on-primary: #ffffff;
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// Accent color themes (override --primary only)
|
||||
// Backgrounds/text stay the same — like ChatGPT's accent system.
|
||||
// =============================================
|
||||
|
||||
// 云上舞白 — warm brown
|
||||
[data-theme="cloud-dancer"] {
|
||||
--primary: #8B7355;
|
||||
--primary-rgb: 139, 115, 85;
|
||||
--primary-hover: #7A6548;
|
||||
--primary-light: rgba(139, 115, 85, 0.1);
|
||||
--bg-primary: #F0EEE9;
|
||||
--bg-secondary: rgba(255, 255, 255, 0.7);
|
||||
--bg-tertiary: rgba(0, 0, 0, 0.03);
|
||||
--bg-hover: rgba(0, 0, 0, 0.05);
|
||||
--text-primary: #3d3d3d;
|
||||
--text-secondary: #666666;
|
||||
--text-tertiary: #999999;
|
||||
--border-color: rgba(0, 0, 0, 0.08);
|
||||
--bg-gradient: linear-gradient(135deg, #F0EEE9 0%, #E8E6E1 100%);
|
||||
--primary-gradient: linear-gradient(135deg, #8B7355 0%, #A68B5B 100%);
|
||||
--card-bg: rgba(255, 255, 255, 0.7);
|
||||
--card-inner-bg: #FAFAF7;
|
||||
--sent-card-bg: var(--primary);
|
||||
}
|
||||
|
||||
// 刚玉蓝主题
|
||||
[data-theme="corundum-blue"][data-mode="light"],
|
||||
[data-theme="corundum-blue"]:not([data-mode]) {
|
||||
--primary: #4A6670;
|
||||
--primary-rgb: 74, 102, 112;
|
||||
--primary-hover: #3D565E;
|
||||
--primary-light: rgba(74, 102, 112, 0.1);
|
||||
--bg-primary: #E8EEF0;
|
||||
--bg-secondary: rgba(255, 255, 255, 0.7);
|
||||
--bg-tertiary: rgba(0, 0, 0, 0.03);
|
||||
--bg-hover: rgba(0, 0, 0, 0.05);
|
||||
--text-primary: #3d3d3d;
|
||||
--text-secondary: #666666;
|
||||
--text-tertiary: #999999;
|
||||
--border-color: rgba(0, 0, 0, 0.08);
|
||||
--bg-gradient: linear-gradient(135deg, #E8EEF0 0%, #D8E4E8 100%);
|
||||
--primary-gradient: linear-gradient(135deg, #4A6670 0%, #5A7A86 100%);
|
||||
--card-bg: rgba(255, 255, 255, 0.7);
|
||||
--card-inner-bg: #F8FAFB;
|
||||
--sent-card-bg: var(--primary);
|
||||
}
|
||||
|
||||
// 冰猕猴桃汁绿主题
|
||||
[data-theme="kiwi-green"][data-mode="light"],
|
||||
[data-theme="kiwi-green"]:not([data-mode]) {
|
||||
--primary: #7A9A5C;
|
||||
--primary-rgb: 122, 154, 92;
|
||||
--primary-hover: #6A8A4C;
|
||||
--primary-light: rgba(122, 154, 92, 0.1);
|
||||
--bg-primary: #E8F0E4;
|
||||
--bg-secondary: rgba(255, 255, 255, 0.7);
|
||||
--bg-tertiary: rgba(0, 0, 0, 0.03);
|
||||
--bg-hover: rgba(0, 0, 0, 0.05);
|
||||
--text-primary: #3d3d3d;
|
||||
--text-secondary: #666666;
|
||||
--text-tertiary: #999999;
|
||||
--border-color: rgba(0, 0, 0, 0.08);
|
||||
--bg-gradient: linear-gradient(135deg, #E8F0E4 0%, #D8E8D0 100%);
|
||||
--primary-gradient: linear-gradient(135deg, #7A9A5C 0%, #8AAA6C 100%);
|
||||
--card-bg: rgba(255, 255, 255, 0.7);
|
||||
--card-inner-bg: #F8FBF6;
|
||||
--sent-card-bg: var(--primary);
|
||||
}
|
||||
|
||||
// 辛辣红主题
|
||||
[data-theme="spicy-red"][data-mode="light"],
|
||||
[data-theme="spicy-red"]:not([data-mode]) {
|
||||
--primary: #8B4049;
|
||||
--primary-rgb: 139, 64, 73;
|
||||
--primary-hover: #7A3540;
|
||||
--primary-light: rgba(139, 64, 73, 0.1);
|
||||
--bg-primary: #F0E8E8;
|
||||
--bg-secondary: rgba(255, 255, 255, 0.7);
|
||||
--bg-tertiary: rgba(0, 0, 0, 0.03);
|
||||
--bg-hover: rgba(0, 0, 0, 0.05);
|
||||
--text-primary: #3d3d3d;
|
||||
--text-secondary: #666666;
|
||||
--text-tertiary: #999999;
|
||||
--border-color: rgba(0, 0, 0, 0.08);
|
||||
--bg-gradient: linear-gradient(135deg, #F0E8E8 0%, #E8D8D8 100%);
|
||||
--primary-gradient: linear-gradient(135deg, #8B4049 0%, #A05058 100%);
|
||||
--card-bg: rgba(255, 255, 255, 0.7);
|
||||
--card-inner-bg: #FAF8F8;
|
||||
--sent-card-bg: var(--primary);
|
||||
}
|
||||
|
||||
// 明水鸭色主题
|
||||
[data-theme="teal-water"][data-mode="light"],
|
||||
[data-theme="teal-water"]:not([data-mode]) {
|
||||
--primary: #5A8A8A;
|
||||
--primary-rgb: 90, 138, 138;
|
||||
--primary-hover: #4A7A7A;
|
||||
--primary-light: rgba(90, 138, 138, 0.1);
|
||||
--bg-primary: #E4F0F0;
|
||||
--bg-secondary: rgba(255, 255, 255, 0.7);
|
||||
--bg-tertiary: rgba(0, 0, 0, 0.03);
|
||||
--bg-hover: rgba(0, 0, 0, 0.05);
|
||||
--text-primary: #3d3d3d;
|
||||
--text-secondary: #666666;
|
||||
--text-tertiary: #999999;
|
||||
--border-color: rgba(0, 0, 0, 0.08);
|
||||
--bg-gradient: linear-gradient(135deg, #E4F0F0 0%, #D4E8E8 100%);
|
||||
--primary-gradient: linear-gradient(135deg, #5A8A8A 0%, #6A9A9A 100%);
|
||||
--card-bg: rgba(255, 255, 255, 0.7);
|
||||
--card-inner-bg: #F6FBFB;
|
||||
--sent-card-bg: var(--primary);
|
||||
}
|
||||
|
||||
// 繁花如梦 - 浅色(晨曦花境)
|
||||
[data-theme="blossom-dream"][data-mode="light"],
|
||||
[data-theme="blossom-dream"]:not([data-mode]) {
|
||||
// 三色定义(供伪元素光晕使用,饱和度提高以便在底色上可见)
|
||||
--blossom-pink: #F0A0B8;
|
||||
--blossom-peach: #FFB07A;
|
||||
--blossom-blue: #90B8E0;
|
||||
|
||||
// 主品牌色:Pantone 粉晶 Rose Quartz
|
||||
--primary: #D4849A;
|
||||
--primary-rgb: 212, 132, 154;
|
||||
--primary-hover: #C4748A;
|
||||
--primary-light: rgba(212, 132, 154, 0.12);
|
||||
|
||||
// 背景三层:主背景最深(相对),面板次之,卡片最白
|
||||
--bg-primary: #F5EDF2;
|
||||
--bg-secondary: rgba(255, 255, 255, 0.82);
|
||||
--bg-tertiary: rgba(212, 132, 154, 0.06);
|
||||
--bg-hover: rgba(212, 132, 154, 0.09);
|
||||
|
||||
// 文字:提高对比度,主色接近纯黑只带微弱紫调
|
||||
--text-primary: #1E1A22;
|
||||
--text-secondary: #6B5F70;
|
||||
--text-tertiary: #9A8A9E;
|
||||
// 边框:粉色半透明,有存在感但不强硬
|
||||
--border-color: rgba(212, 132, 154, 0.18);
|
||||
|
||||
--bg-gradient: linear-gradient(150deg, #F5EDF2 0%, #F0EAF6 50%, #EAF0F8 100%);
|
||||
--primary-gradient: linear-gradient(135deg, #D4849A 0%, #E8A8B8 100%);
|
||||
|
||||
// 卡片:高不透明度白,与背景形成明显层次
|
||||
--card-bg: rgba(255, 255, 255, 0.88);
|
||||
--card-inner-bg: rgba(255, 255, 255, 0.95);
|
||||
|
||||
--sent-card-bg: var(--primary);
|
||||
}
|
||||
|
||||
// Geist · 极简黑白 - 浅色
|
||||
[data-theme="geist"][data-mode="light"],
|
||||
[data-theme="geist"]:not([data-mode]) {
|
||||
--primary: #444444;
|
||||
--primary-rgb: 68, 68, 68;
|
||||
--primary-hover: #333333;
|
||||
--primary-light: rgba(68, 68, 68, 0.08);
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: rgba(250, 250, 250, 0.95);
|
||||
--bg-tertiary: rgba(0, 0, 0, 0.03);
|
||||
--bg-hover: rgba(0, 0, 0, 0.05);
|
||||
--text-primary: #111111;
|
||||
--text-secondary: #666666;
|
||||
--text-tertiary: #999999;
|
||||
--border-color: #eaeaea;
|
||||
--border-radius: 6px;
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.08);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
--bg-gradient: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
|
||||
--primary-gradient: linear-gradient(135deg, #444444 0%, #666666 100%);
|
||||
--card-bg: rgba(250, 250, 250, 0.95);
|
||||
--card-inner-bg: #f5f5f5;
|
||||
--sent-card-bg: #444444;
|
||||
}
|
||||
|
||||
// ==================== 深色主题 ====================
|
||||
|
||||
// 云上舞白 - 深色
|
||||
[data-theme="cloud-dancer"][data-mode="dark"] {
|
||||
--primary: #C9A86C;
|
||||
--primary-rgb: 201, 168, 108;
|
||||
--primary-hover: #D9B87C;
|
||||
--primary-light: rgba(201, 168, 108, 0.15);
|
||||
--bg-primary: #1a1816;
|
||||
--bg-secondary: rgba(40, 36, 32, 0.9);
|
||||
--bg-secondary-solid: #282420;
|
||||
--bg-tertiary: rgba(255, 255, 255, 0.05);
|
||||
--bg-hover: rgba(255, 255, 255, 0.08);
|
||||
--text-primary: #F0EEE9;
|
||||
--text-secondary: #b3b0aa;
|
||||
--text-tertiary: #807d78;
|
||||
--border-color: rgba(255, 255, 255, 0.1);
|
||||
--bg-gradient: linear-gradient(135deg, #1a1816 0%, #252220 100%);
|
||||
--primary-gradient: linear-gradient(135deg, #8B7355 0%, #C9A86C 100%);
|
||||
--card-bg: rgba(40, 36, 32, 0.9);
|
||||
--card-inner-bg: #27231F;
|
||||
--sent-card-bg: var(--primary);
|
||||
}
|
||||
|
||||
// 刚玉蓝 - 深色
|
||||
// 繁花如梦 — rose pink
|
||||
[data-theme="blossom-dream"] {
|
||||
--primary: #D4849A;
|
||||
--primary-rgb: 212, 132, 154;
|
||||
--primary-hover: #C4748A;
|
||||
--primary-light: rgba(212, 132, 154, 0.12);
|
||||
--primary-gradient: linear-gradient(135deg, #D4849A 0%, #E8A8B8 100%);
|
||||
}
|
||||
[data-theme="blossom-dream"][data-mode="dark"] {
|
||||
--primary: #D19EBB;
|
||||
--primary-rgb: 209, 158, 187;
|
||||
--primary-hover: #DDB0C8;
|
||||
--primary-light: rgba(209, 158, 187, 0.15);
|
||||
--primary-gradient: linear-gradient(135deg, #D19EBB 0%, #A878A8 100%);
|
||||
}
|
||||
|
||||
// 刚玉蓝 — steel blue
|
||||
[data-theme="corundum-blue"] {
|
||||
--primary: #4A6670;
|
||||
--primary-rgb: 74, 102, 112;
|
||||
--primary-hover: #3D565E;
|
||||
--primary-light: rgba(74, 102, 112, 0.1);
|
||||
--primary-gradient: linear-gradient(135deg, #4A6670 0%, #5A7A86 100%);
|
||||
}
|
||||
[data-theme="corundum-blue"][data-mode="dark"] {
|
||||
--primary: #6A9AAA;
|
||||
--primary-rgb: 106, 154, 170;
|
||||
--primary-hover: #7AAABA;
|
||||
--primary-light: rgba(106, 154, 170, 0.15);
|
||||
--bg-primary: #141a1c;
|
||||
--bg-secondary: rgba(30, 40, 44, 0.9);
|
||||
--bg-secondary-solid: #1e282c;
|
||||
--bg-tertiary: rgba(255, 255, 255, 0.05);
|
||||
--bg-hover: rgba(255, 255, 255, 0.08);
|
||||
--text-primary: #E8EEF0;
|
||||
--text-secondary: #a8b4b8;
|
||||
--text-tertiary: #6a7a80;
|
||||
--border-color: rgba(255, 255, 255, 0.1);
|
||||
--bg-gradient: linear-gradient(135deg, #141a1c 0%, #1e282c 100%);
|
||||
--primary-gradient: linear-gradient(135deg, #4A6670 0%, #6A9AAA 100%);
|
||||
--card-bg: rgba(30, 40, 44, 0.9);
|
||||
--card-inner-bg: #1D272A;
|
||||
--sent-card-bg: var(--primary);
|
||||
}
|
||||
|
||||
// 冰猕猴桃汁绿 - 深色
|
||||
// 冰猕猴桃汁绿 — kiwi green
|
||||
[data-theme="kiwi-green"] {
|
||||
--primary: #7A9A5C;
|
||||
--primary-rgb: 122, 154, 92;
|
||||
--primary-hover: #6A8A4C;
|
||||
--primary-light: rgba(122, 154, 92, 0.1);
|
||||
--primary-gradient: linear-gradient(135deg, #7A9A5C 0%, #8AAA6C 100%);
|
||||
}
|
||||
[data-theme="kiwi-green"][data-mode="dark"] {
|
||||
--primary: #9ABA7C;
|
||||
--primary-rgb: 154, 186, 124;
|
||||
--primary-hover: #AACA8C;
|
||||
--primary-light: rgba(154, 186, 124, 0.15);
|
||||
--bg-primary: #161a14;
|
||||
--bg-secondary: rgba(34, 42, 30, 0.9);
|
||||
--bg-secondary-solid: #222a1e;
|
||||
--bg-tertiary: rgba(255, 255, 255, 0.05);
|
||||
--bg-hover: rgba(255, 255, 255, 0.08);
|
||||
--text-primary: #E8F0E4;
|
||||
--text-secondary: #a8b4a0;
|
||||
--text-tertiary: #6a7a60;
|
||||
--border-color: rgba(255, 255, 255, 0.1);
|
||||
--bg-gradient: linear-gradient(135deg, #161a14 0%, #222a1e 100%);
|
||||
--primary-gradient: linear-gradient(135deg, #7A9A5C 0%, #9ABA7C 100%);
|
||||
--card-bg: rgba(34, 42, 30, 0.9);
|
||||
--card-inner-bg: #21281D;
|
||||
--sent-card-bg: var(--primary);
|
||||
}
|
||||
|
||||
// 辛辣红 - 深色
|
||||
// 辛辣红 — spicy red
|
||||
[data-theme="spicy-red"] {
|
||||
--primary: #8B4049;
|
||||
--primary-rgb: 139, 64, 73;
|
||||
--primary-hover: #7A3540;
|
||||
--primary-light: rgba(139, 64, 73, 0.1);
|
||||
--primary-gradient: linear-gradient(135deg, #8B4049 0%, #A05058 100%);
|
||||
}
|
||||
[data-theme="spicy-red"][data-mode="dark"] {
|
||||
--primary: #C06068;
|
||||
--primary-rgb: 192, 96, 104;
|
||||
--primary-hover: #D07078;
|
||||
--primary-light: rgba(192, 96, 104, 0.15);
|
||||
--bg-primary: #1a1416;
|
||||
--bg-secondary: rgba(42, 32, 34, 0.9);
|
||||
--bg-secondary-solid: #2a2022;
|
||||
--bg-tertiary: rgba(255, 255, 255, 0.05);
|
||||
--bg-hover: rgba(255, 255, 255, 0.08);
|
||||
--text-primary: #F0E8E8;
|
||||
--text-secondary: #b4a8aa;
|
||||
--text-tertiary: #7a6a6c;
|
||||
--border-color: rgba(255, 255, 255, 0.1);
|
||||
--bg-gradient: linear-gradient(135deg, #1a1416 0%, #2a2022 100%);
|
||||
--primary-gradient: linear-gradient(135deg, #8B4049 0%, #C06068 100%);
|
||||
--card-bg: rgba(42, 32, 34, 0.9);
|
||||
--card-inner-bg: #281F21;
|
||||
--sent-card-bg: var(--primary);
|
||||
}
|
||||
|
||||
// 明水鸭色 - 深色
|
||||
// 明水鸭色 — teal
|
||||
[data-theme="teal-water"] {
|
||||
--primary: #5A8A8A;
|
||||
--primary-rgb: 90, 138, 138;
|
||||
--primary-hover: #4A7A7A;
|
||||
--primary-light: rgba(90, 138, 138, 0.1);
|
||||
--primary-gradient: linear-gradient(135deg, #5A8A8A 0%, #6A9A9A 100%);
|
||||
}
|
||||
[data-theme="teal-water"][data-mode="dark"] {
|
||||
--primary: #7ABAAA;
|
||||
--primary-rgb: 122, 186, 170;
|
||||
--primary-hover: #8ACABA;
|
||||
--primary-light: rgba(122, 186, 170, 0.15);
|
||||
--bg-primary: #121a1a;
|
||||
--bg-secondary: rgba(28, 42, 42, 0.9);
|
||||
--bg-secondary-solid: #1c2a2a;
|
||||
--bg-tertiary: rgba(255, 255, 255, 0.05);
|
||||
--bg-hover: rgba(255, 255, 255, 0.08);
|
||||
--text-primary: #E4F0F0;
|
||||
--text-secondary: #a0b4b4;
|
||||
--text-tertiary: #607a7a;
|
||||
--border-color: rgba(255, 255, 255, 0.1);
|
||||
--bg-gradient: linear-gradient(135deg, #121a1a 0%, #1c2a2a 100%);
|
||||
--primary-gradient: linear-gradient(135deg, #5A8A8A 0%, #7ABAAA 100%);
|
||||
--card-bg: rgba(28, 42, 42, 0.9);
|
||||
--card-inner-bg: #1B2828;
|
||||
--sent-card-bg: var(--primary);
|
||||
}
|
||||
|
||||
// 繁花如梦 - 深色(夜阑幽梦)
|
||||
[data-theme="blossom-dream"][data-mode="dark"] {
|
||||
// 光晕色(供伪元素使用,降低饱和度避免刺眼)
|
||||
--blossom-pink: #C670C3;
|
||||
--blossom-purple: #5F4B8B;
|
||||
--blossom-blue: #3A2A50;
|
||||
|
||||
// 主品牌色:藕粉/烟紫粉,降饱和度不刺眼
|
||||
--primary: #D19EBB;
|
||||
--primary-rgb: 209, 158, 187;
|
||||
--primary-hover: #DDB0C8;
|
||||
--primary-light: rgba(209, 158, 187, 0.15);
|
||||
|
||||
// 背景三层:极深黑灰底(去掉紫薯色),面板略浅,卡片再浅一级
|
||||
--bg-primary: #151316;
|
||||
--bg-secondary: rgba(34, 30, 36, 0.92);
|
||||
--bg-secondary-solid: #221E24;
|
||||
--bg-tertiary: rgba(255, 255, 255, 0.04);
|
||||
--bg-hover: rgba(209, 158, 187, 0.1);
|
||||
|
||||
// 文字
|
||||
--text-primary: #F0EAF4;
|
||||
--text-secondary: #A898AE;
|
||||
--text-tertiary: #6A5870;
|
||||
// 边框:极细白色内发光,剥离层级
|
||||
--border-color: rgba(255, 255, 255, 0.07);
|
||||
|
||||
--bg-gradient: linear-gradient(150deg, #151316 0%, #1A1620 50%, #131018 100%);
|
||||
--primary-gradient: linear-gradient(135deg, #D19EBB 0%, #A878A8 100%);
|
||||
|
||||
// 卡片:比面板更亮一档,用深灰而非紫色
|
||||
--card-bg: rgba(42, 38, 46, 0.92);
|
||||
--card-inner-bg: rgba(52, 48, 56, 0.96);
|
||||
|
||||
--sent-card-bg: var(--primary);
|
||||
// Geist — monochrome
|
||||
[data-theme="geist"] {
|
||||
--primary: #444444;
|
||||
--primary-rgb: 68, 68, 68;
|
||||
--primary-hover: #333333;
|
||||
--primary-light: rgba(68, 68, 68, 0.08);
|
||||
--primary-gradient: linear-gradient(135deg, #444444 0%, #666666 100%);
|
||||
}
|
||||
|
||||
// Geist · 极简黑白 - 深色
|
||||
[data-theme="geist"][data-mode="dark"] {
|
||||
--primary: #ededed;
|
||||
--primary-rgb: 237, 237, 237;
|
||||
--primary-hover: #d5d5d5;
|
||||
--primary-light: rgba(237, 237, 237, 0.1);
|
||||
--bg-primary: #1a1a1a;
|
||||
--bg-secondary: rgba(34, 34, 34, 0.95);
|
||||
--bg-secondary-solid: #222222;
|
||||
--bg-tertiary: rgba(255, 255, 255, 0.04);
|
||||
--bg-hover: rgba(255, 255, 255, 0.07);
|
||||
--text-primary: #ededed;
|
||||
--text-secondary: #999999;
|
||||
--text-tertiary: #666666;
|
||||
--border-color: #2e2e2e;
|
||||
--border-radius: 6px;
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.5);
|
||||
--bg-gradient: linear-gradient(135deg, #1a1a1a 0%, #222222 100%);
|
||||
--primary-gradient: linear-gradient(135deg, #ededed 0%, #cccccc 100%);
|
||||
--card-bg: rgba(34, 34, 34, 0.95);
|
||||
--card-inner-bg: #2a2a2a;
|
||||
--sent-card-bg: #3a3a3a;
|
||||
// primary 是浅灰色,上方文字需要用深色
|
||||
--on-primary: #111111;
|
||||
}
|
||||
|
||||
// 重置样式
|
||||
// =============================================
|
||||
// Global reset
|
||||
// =============================================
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -412,12 +225,13 @@ body {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// 滚动条样式
|
||||
// =============================================
|
||||
// Scrollbar
|
||||
// =============================================
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
@@ -436,17 +250,21 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮基础样式
|
||||
// =============================================
|
||||
// Button base
|
||||
// =============================================
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: background 0.15s ease, opacity 0.15s ease;
|
||||
|
||||
&-primary {
|
||||
background: var(--primary);
|
||||
@@ -455,6 +273,11 @@ body {
|
||||
&:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&-secondary {
|
||||
@@ -462,20 +285,28 @@ body {
|
||||
color: var(--text-primary);
|
||||
|
||||
&:hover {
|
||||
background: var(--border-color);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片样式
|
||||
// =============================================
|
||||
// Card base
|
||||
// =============================================
|
||||
.card {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
// 全局 Switch 开关样式
|
||||
// =============================================
|
||||
// Switch toggle
|
||||
// =============================================
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
@@ -525,7 +356,6 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
// 禁用状态
|
||||
input:disabled+.switch-slider {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
|
||||
Reference in New Issue
Block a user