feat: 统一界面视觉规范并优化导出交互

This commit is contained in:
Wxw-Gu
2026-08-20 20:13:23 +08:00
parent 65830d70fe
commit 740bec6d04
28 changed files with 777 additions and 1468 deletions
@@ -177,7 +177,7 @@ export function ExportConfigurationPanel({
aria-pressed={active}
className={`${
active ? 'active border-2 border-primary bg-primary/10 hover:bg-primary/15' : ''
} h-[78px] min-w-0 flex-col gap-1.5 whitespace-normal px-2 text-foreground`}
} !h-[78px] min-w-0 flex-col gap-1.5 whitespace-normal px-2 text-foreground`}
disabled={!exportAll && exportContactCount > 1 && value !== 'html'}
onClick={() => onFormatChange(value)}
>
@@ -1,6 +1,8 @@
import React from 'react'
import React, { useRef } from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'
import type { ExportContactType } from '../../../../shared/export'
import { Button, Checkbox, Input, Tabs, TabsList, TabsTrigger } from '../ui'
import { SearchIcon } from '../chat/icons'
import type { Contact, SelfInfo } from './exportTypes'
import { displayName } from './exportUtils'
@@ -27,6 +29,71 @@ interface ExportContactPanelProps {
onOpenSettings: () => void
}
interface ExportContactRowProps {
contact: Contact
active: boolean
pressed: boolean
selected: boolean
showSelection: boolean
disabled: boolean
style?: React.CSSProperties
onSelect: (contact: Contact) => void
}
function ExportContactRow({
contact,
active,
pressed,
selected,
showSelection,
disabled,
style,
onSelect
}: ExportContactRowProps): React.ReactElement {
const name = displayName(contact)
return (
<button
type="button"
className={`${style ? 'absolute left-0 top-0 ' : ''}flex w-full items-center gap-2.5 border-0 border-l-[3px] px-4 py-[11px] text-left text-foreground transition-colors hover:bg-surface/60 disabled:cursor-not-allowed disabled:opacity-50 ${
active ? 'border-l-primary bg-primary/10' : 'border-l-transparent bg-transparent'
}`}
style={style}
onClick={() => onSelect(contact)}
disabled={disabled}
aria-pressed={pressed}
>
<span className="grid h-[38px] w-[38px] shrink-0 place-items-center overflow-hidden rounded-lg bg-primary/10 font-bold text-primary">
{contact.avatar ? (
<img className="h-full w-full object-cover" src={contact.avatar} alt="" />
) : (
name.slice(0, 1)
)}
</span>
<span className="grid min-w-0 flex-1 gap-0.5">
<strong className="overflow-hidden text-ellipsis whitespace-nowrap text-[13px]">
{name}
</strong>
<small className="text-[11px] text-muted-foreground">
{contact.type === 'group' ? '群聊' : '联系人'}
</small>
</span>
{showSelection && (
<span
className={`grid h-[18px] w-[18px] shrink-0 place-items-center rounded border text-xs text-primary-foreground ${
selected ? 'border-primary bg-primary' : 'border-border-strong'
}`}
aria-hidden
>
{selected && (
<span className="h-2 w-1 -translate-y-px rotate-45 border-b-2 border-r-2 border-current" />
)}
</span>
)}
</button>
)
}
export function ExportContactPanel({
contacts,
filteredContacts,
@@ -49,6 +116,17 @@ export function ExportContactPanel({
onToggleAllContactType,
onOpenSettings
}: ExportContactPanelProps): React.ReactElement {
const virtualizeContacts = filteredContacts.length >= 100
const contactListRef = useRef<HTMLDivElement>(null)
// TanStack Virtual owns mutable measurements, so React Compiler must skip this hook.
// eslint-disable-next-line react-hooks/incompatible-library
const contactVirtualizer = useVirtualizer({
count: virtualizeContacts ? filteredContacts.length : 0,
getScrollElement: () => contactListRef.current,
estimateSize: () => 60,
getItemKey: (index) => filteredContacts[index]?.md5 || index,
overscan: 10
})
const groupCount = contacts.filter((contact) => contact.type === 'group').length
const userCount = contacts.length - groupCount
const selectedAllCount =
@@ -69,7 +147,7 @@ export function ExportContactPanel({
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
aria-hidden
>
<SearchIcon className="h-3.5 w-3.5 fill-none stroke-current stroke-2" />
</span>
<Input
className="h-[38px] pl-9 text-[13px]"
@@ -104,7 +182,7 @@ export function ExportContactPanel({
</Tabs>
<Button
variant="outline"
className={`mt-2.5 h-auto w-full justify-between gap-3 px-2.5 py-2 text-left ${
className={`mt-2.5 !h-auto w-full justify-between gap-3 px-2.5 py-2 text-left ${
exportAll ? 'border-primary bg-primary/10 text-primary hover:bg-primary/15' : ''
}`}
aria-pressed={exportAll}
@@ -170,61 +248,71 @@ export function ExportContactPanel({
</div>
) : null}
<div className="export-contact-list min-h-0 flex-1 overflow-auto py-2">
{filteredContacts.map((contact) => {
const name = displayName(contact)
<div ref={contactListRef} className="export-contact-list min-h-0 flex-1 overflow-auto py-2">
{virtualizeContacts ? (
<div
className="relative w-full"
style={{ height: `${contactVirtualizer.getTotalSize()}px` }}
>
{contactVirtualizer.getVirtualItems().map((virtualItem) => {
const contact = filteredContacts[virtualItem.index]
if (!contact) return null
const selected = selectedContactIds.includes(contact.md5)
const selectedByAll = exportAll && allContactTypes.includes(contact.type)
const visuallySelected = exportAll ? selectedByAll : selected
const atLimit =
!exportAll && selectionMode && !selected && selectedContactIds.length >= selectionLimit
!exportAll &&
selectionMode &&
!selected &&
selectedContactIds.length >= selectionLimit
return (
<button
key={contact.md5}
type="button"
className={`flex w-full items-center gap-2.5 border-0 border-l-[3px] px-4 py-[11px] text-left text-foreground transition-colors hover:bg-surface/60 disabled:cursor-not-allowed disabled:opacity-50 ${
!exportAll && activeContact?.md5 === contact.md5
? 'border-l-primary bg-primary/10'
: 'border-l-transparent bg-transparent'
}`}
onClick={() => onSelectContact(contact)}
<ExportContactRow
key={virtualItem.key}
contact={contact}
active={!exportAll && activeContact?.md5 === contact.md5}
pressed={visuallySelected}
selected={selected}
showSelection={!exportAll && selectionMode}
disabled={atLimit}
aria-pressed={visuallySelected}
>
<span className="grid h-[38px] w-[38px] shrink-0 place-items-center overflow-hidden rounded-lg bg-primary/10 font-bold text-primary">
{contact.avatar ? (
<img className="h-full w-full object-cover" src={contact.avatar} alt="" />
) : (
name.slice(0, 1)
)}
</span>
<span className="grid min-w-0 flex-1 gap-0.5">
<strong className="overflow-hidden text-ellipsis whitespace-nowrap text-[13px]">
{name}
</strong>
<small className="text-[11px] text-muted-foreground">
{contact.type === 'group' ? '群聊' : '联系人'}
</small>
</span>
{!exportAll && selectionMode && (
<span
className={`grid h-[18px] w-[18px] shrink-0 place-items-center rounded border text-xs text-primary-foreground ${
selected ? 'border-primary bg-primary' : 'border-border-strong'
}`}
aria-hidden
>
{selected ? '✓' : ''}
</span>
)}
</button>
style={{
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`
}}
onSelect={onSelectContact}
/>
)
})}
</div>
) : (
filteredContacts.map((contact) => {
const selected = selectedContactIds.includes(contact.md5)
const selectedByAll = exportAll && allContactTypes.includes(contact.type)
const visuallySelected = exportAll ? selectedByAll : selected
const atLimit =
!exportAll &&
selectionMode &&
!selected &&
selectedContactIds.length >= selectionLimit
return (
<ExportContactRow
key={contact.md5}
contact={contact}
active={!exportAll && activeContact?.md5 === contact.md5}
pressed={visuallySelected}
selected={selected}
showSelection={!exportAll && selectionMode}
disabled={atLimit}
onSelect={onSelectContact}
/>
)
})
)}
</div>
<Button
type="button"
variant="ghost"
className="h-auto justify-start gap-2 border-t border-border px-4 py-3.5 text-left"
className="!h-auto justify-start gap-2 border-t border-border px-4 py-3.5 text-left"
onClick={onOpenSettings}
>
<span className="grid h-[34px] w-[34px] shrink-0 place-items-center overflow-hidden rounded-full bg-primary/10 font-bold text-primary">
@@ -141,7 +141,7 @@ export function ExportPreviewPanel({
systemMessage
? 'max-w-[92%] bg-muted px-2.5 py-1 text-center text-[11px] text-muted-foreground shadow-none'
: message.isSender
? 'bg-[#95ec69]'
? 'bg-accent'
: 'bg-surface'
}`}
>
+15 -11
View File
@@ -3,23 +3,27 @@ import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '../../lib/cn'
const buttonVariants = cva(
'inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md border-0 bg-transparent text-sm font-medium transition-colors duration-fast ease-tm-standard focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
'inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border-0 bg-transparent text-sm font-medium transition-[color,background-color,border-color,transform] duration-fast ease-tm-standard active:translate-y-px disabled:pointer-events-none disabled:translate-y-0 disabled:!text-disabled-foreground disabled:opacity-100',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow-surface hover:bg-primary/90',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/75',
ghost: 'text-foreground hover:bg-accent hover:text-accent-foreground',
outline: 'border border-border bg-surface text-foreground shadow-surface hover:bg-accent',
default:
'bg-primary text-primary-foreground hover:bg-primary-hover disabled:bg-disabled-surface disabled:text-disabled-foreground',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/75 active:bg-secondary/60 disabled:bg-disabled-surface disabled:text-disabled-foreground',
ghost:
'text-foreground hover:bg-accent hover:text-accent-foreground active:bg-accent/75 disabled:text-disabled-foreground',
outline:
'border border-border bg-surface text-foreground hover:border-primary/25 hover:bg-accent active:bg-accent/75 disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground',
destructive:
'bg-destructive text-destructive-foreground shadow-surface hover:bg-destructive/90',
link: 'text-primary underline-offset-4 hover:underline'
'bg-destructive text-destructive-foreground hover:bg-destructive/90 active:bg-destructive/80 disabled:bg-disabled-surface disabled:text-disabled-foreground',
link: '!h-auto !p-0 text-primary underline-offset-4 hover:text-primary-hover hover:underline disabled:text-disabled-foreground'
},
size: {
sm: 'h-8 rounded-md px-3 text-xs',
default: 'h-9 px-4 py-2',
lg: 'h-10 rounded-lg px-6',
icon: 'h-9 w-9'
sm: 'h-control-compact rounded-md px-2.5 text-xs',
default: 'h-control-standard px-3.5',
lg: 'h-control-form rounded-lg px-5',
icon: 'h-control-standard w-8 p-0'
}
},
defaultVariants: {
+3 -3
View File
@@ -9,13 +9,13 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-border bg-surface shadow-surface transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
'peer h-4 w-4 shrink-0 rounded-sm border border-muted-foreground/60 bg-surface transition-colors duration-fast ease-tm-standard hover:border-primary/70 disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground disabled:data-[state=checked]:border-disabled-border disabled:data-[state=checked]:bg-primary/40',
className
)}
{...props}
>
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-[11px]">
<CheckboxPrimitive.Indicator className="flex items-center justify-center">
<span className="h-2 w-1 -translate-y-px rotate-45 border-b-2 border-r-2 border-current" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
+1 -1
View File
@@ -10,7 +10,7 @@ export const Input = React.forwardRef<
ref={ref}
type={type}
className={cn(
'flex h-9 w-full rounded-md border border-border bg-surface px-3 py-1 text-sm text-foreground shadow-surface transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
'flex h-control-form w-full rounded-md border border-border bg-surface px-3 py-1 text-sm text-foreground transition-colors duration-fast ease-tm-standard placeholder:text-muted-foreground hover:border-primary/25 disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 disabled:placeholder:text-disabled-foreground',
className
)}
{...props}
@@ -17,7 +17,7 @@ const RadioGroupItem = React.forwardRef<
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
'aspect-square h-4 w-4 rounded-full border border-border bg-surface text-primary shadow-surface focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
'aspect-square h-4 w-4 rounded-full border border-muted-foreground/60 bg-surface text-primary transition-colors duration-fast ease-tm-standard hover:border-primary/70 disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 data-[state=checked]:border-primary disabled:data-[state=checked]:border-disabled-border',
className
)}
{...props}
+9 -6
View File
@@ -13,14 +13,17 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-9 w-full items-center justify-between rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground shadow-surface focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
'flex h-control-form w-full items-center justify-between rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground transition-colors duration-fast ease-tm-standard hover:border-primary/25 disabled:cursor-not-allowed disabled:border-disabled-border disabled:bg-disabled-surface disabled:text-disabled-foreground disabled:opacity-100 [&>span]:line-clamp-1',
className
)}
{...props}
>
{children}
<span aria-hidden="true" className="ml-2 text-muted-foreground">
<span
aria-hidden="true"
className="ml-2 flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground"
>
<span className="h-1.5 w-1.5 -translate-y-px rotate-45 border-b border-r border-current" />
</span>
</SelectPrimitive.Trigger>
))
@@ -66,13 +69,13 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex min-h-8 w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors duration-fast ease-tm-standard focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:text-disabled-foreground data-[disabled]:opacity-100',
className
)}
{...props}
>
<SelectPrimitive.ItemIndicator className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center text-primary">
<span className="h-2 w-1 rotate-45 border-b-2 border-r-2 border-current" />
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
+2 -2
View File
@@ -9,12 +9,12 @@ const Switch = React.forwardRef<
<SwitchPrimitive.Root
ref={ref}
className={cn(
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary',
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-muted transition-colors duration-fast ease-tm-standard hover:bg-muted/80 disabled:cursor-not-allowed disabled:bg-disabled-surface disabled:opacity-100 data-[state=checked]:bg-primary disabled:data-[state=checked]:bg-primary/40',
className
)}
{...props}
>
<SwitchPrimitive.Thumb className="pointer-events-none block h-4 w-4 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0" />
<SwitchPrimitive.Thumb className="pointer-events-none block h-4 w-4 rounded-full bg-white shadow-surface ring-0 transition-transform duration-fast ease-tm-standard data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0" />
</SwitchPrimitive.Root>
))
Switch.displayName = SwitchPrimitive.Root.displayName
+17 -16
View File
@@ -1,20 +1,21 @@
:root {
--wxex-bg-app: #f4f6f5;
--wxex-bg-main: #fafbfa;
--wxex-bg-sidebar: #eef2f0;
--wxex-bg-elevated: #ffffff;
--wxex-text-primary: #202724;
--wxex-text-secondary: #66706b;
--wxex-text-muted: #929a96;
--wxex-border: #dde3e0;
--wxex-brand: #247a63;
--wxex-brand-hover: #1d6754;
--wxex-brand-soft: #ddede7;
--wxex-ai: #686edc;
--wxex-ai-soft: #eeeffd;
--wxex-success: #2e8b68;
--wxex-warning: #c68635;
--wxex-danger: #c85a5a;
// Original TraceMemo brand tokens remain available while the UI Layer consumes --tm-*.
--wxex-bg-app: hsl(var(--tm-background));
--wxex-bg-main: hsl(var(--tm-canvas));
--wxex-bg-sidebar: hsl(var(--tm-surface-muted));
--wxex-bg-elevated: hsl(var(--tm-surface-elevated));
--wxex-text-primary: hsl(var(--tm-foreground));
--wxex-text-secondary: hsl(var(--tm-muted-foreground));
--wxex-text-muted: hsl(var(--tm-subtle-foreground));
--wxex-border: hsl(var(--tm-border));
--wxex-brand: hsl(var(--tm-primary));
--wxex-brand-hover: hsl(var(--tm-primary-hover));
--wxex-brand-soft: hsl(var(--tm-accent));
--wxex-ai: hsl(var(--tm-ai));
--wxex-ai-soft: hsl(var(--tm-ai-soft));
--wxex-success: hsl(var(--tm-success));
--wxex-warning: hsl(var(--tm-warning));
--wxex-danger: hsl(var(--tm-destructive));
--wxex-font:
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
--wxex-nav-width: 76px;
+10 -26
View File
@@ -86,7 +86,6 @@
}
.api-section-heading h2,
.api-introduction h2,
.api-integrations h2,
.api-runtime-title h2 {
margin: 0;
color: var(--wxex-text-primary);
@@ -150,7 +149,7 @@
}
.api-skill-status.error {
color: var(--wxex-danger);
background: #fff1f1;
background: color-mix(in srgb, var(--wxex-danger) 10%, var(--wxex-bg-elevated));
}
.api-version {
color: var(--wxex-text-muted);
@@ -166,21 +165,6 @@
.api-header-actions {
flex: 0 0 auto;
}
.api-primary-button {
border-color: var(--wxex-brand) !important;
background: var(--wxex-brand) !important;
color: #fff !important;
}
.api-primary-button:hover:not(:disabled),
.api-primary-button:focus-visible,
.api-primary-button:active {
background: var(--wxex-brand-hover) !important;
color: #fff !important;
}
.api-primary-button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.api-trust-bar {
display: flex;
flex-wrap: wrap;
@@ -190,7 +174,7 @@
padding: 10px 12px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: #f5f8f6;
background: var(--wxex-bg-sidebar);
color: var(--wxex-text-secondary);
font: 12px/18px var(--wxex-font);
}
@@ -228,7 +212,7 @@
padding: 7px 10px;
border: 1px solid var(--wxex-border);
border-radius: 5px;
background: #fff;
background: var(--wxex-bg-elevated);
white-space: nowrap;
}
.api-flow strong {
@@ -360,7 +344,7 @@
padding: 11px 14px;
}
.api-endpoint-head {
background: #f1f4f2;
background: var(--wxex-bg-sidebar);
color: var(--wxex-text-secondary);
font: 700 12px/18px var(--wxex-font);
}
@@ -371,7 +355,7 @@
font: 12px/18px var(--wxex-font);
}
.api-endpoint-row.active {
background: #f7faf8;
background: var(--wxex-brand-soft);
}
.api-endpoint-row code {
color: var(--wxex-text-primary);
@@ -405,7 +389,7 @@
padding: 10px 12px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: #f5f8f6;
background: var(--wxex-bg-sidebar);
overflow: hidden;
}
.api-request-meta code {
@@ -446,7 +430,7 @@
padding: 8px 10px;
border: 1px solid rgba(200, 90, 90, 0.35);
border-radius: var(--wxex-radius-sm);
background: #fff4f4;
background: color-mix(in srgb, var(--wxex-danger) 10%, var(--wxex-bg-elevated));
color: var(--wxex-danger);
font: 12px/18px var(--wxex-font);
}
@@ -512,8 +496,8 @@
margin: 12px 16px;
padding: 9px 10px;
border-left: 3px solid var(--wxex-warning);
background: #fff8ec;
color: #915d1e;
background: color-mix(in srgb, var(--wxex-warning) 10%, var(--wxex-bg-elevated));
color: var(--wxex-warning);
font: 12px/18px var(--wxex-font);
}
.api-token-value {
@@ -595,7 +579,7 @@
font: 11px/16px var(--wxex-font);
}
.api-privacy {
background: #f5f8f6;
background: var(--wxex-bg-sidebar);
}
.api-privacy p {
margin: 0;
+11 -241
View File
@@ -162,7 +162,7 @@
grid-template-columns: auto 1fr;
gap: 3px 10px;
border-radius: var(--wxex-radius-md);
background: #f3f6f4;
background: var(--wxex-bg-sidebar);
padding: 12px 14px;
span {
@@ -201,8 +201,8 @@
}
&.blocked {
border-color: rgba(190, 96, 60, 0.24);
background: rgba(190, 96, 60, 0.06);
border-color: color-mix(in srgb, var(--wxex-danger) 28%, transparent);
background: color-mix(in srgb, var(--wxex-danger) 7%, var(--wxex-bg-elevated));
}
strong,
@@ -230,7 +230,7 @@
}
small.error {
color: #b15338;
color: var(--wxex-danger);
word-break: break-all;
}
}
@@ -287,11 +287,11 @@
.ready & {
background: var(--wxex-success);
box-shadow: 0 0 0 3px rgba(36, 122, 99, 0.12);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wxex-success) 14%, transparent);
}
.blocked & {
background: #be603c;
background: var(--wxex-danger);
}
}
@@ -406,11 +406,11 @@
display: grid;
gap: 3px;
border-radius: var(--wxex-radius-md);
background: rgba(190, 96, 60, 0.08);
background: color-mix(in srgb, var(--wxex-warning) 10%, var(--wxex-bg-elevated));
padding: 10px 12px;
&.ready {
background: rgba(36, 122, 99, 0.08);
background: color-mix(in srgb, var(--wxex-success) 9%, var(--wxex-bg-elevated));
}
strong {
@@ -559,8 +559,8 @@
}
&.error {
background: rgba(190, 96, 60, 0.1);
color: #a84e32;
background: color-mix(in srgb, var(--wxex-danger) 10%, var(--wxex-bg-elevated));
color: var(--wxex-danger);
}
}
@@ -621,7 +621,7 @@
justify-content: space-between;
gap: 16px;
border-bottom: 1px solid var(--wxex-border);
background: #f3f6f4;
background: var(--wxex-bg-sidebar);
color: var(--wxex-text-secondary);
padding: 0 18px;
font-size: 12px;
@@ -769,11 +769,6 @@
word-break: break-word;
}
.wechat-system-message-meta {
color: #a0a7ab;
font-size: 11px;
}
.message-avatar {
width: 38px;
height: 38px;
@@ -845,10 +840,6 @@
}
}
.message-meta {
display: none;
}
.message-hover-time,
.message-accessible-sender {
color: var(--wxex-text-muted);
@@ -1202,90 +1193,6 @@
word-break: break-word;
}
.chat-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
th {
text-align: left;
padding: 8px;
border-bottom: 1px solid var(--border-color);
background-color: #f9f9f9;
color: #666;
font-weight: normal;
position: sticky;
top: 0;
z-index: 1;
position: relative;
}
td {
padding: 8px;
border-bottom: 1px solid #eee;
vertical-align: top;
color: #333;
}
tr:hover {
background-color: #f0f0f0;
}
}
.column-resizer {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 5px;
cursor: col-resize;
z-index: 2;
}
.column-resizer:hover {
background-color: #ccc;
}
.col-sender {
width: 80px;
}
.col-type {
width: 80px;
}
.col-time {
width: 150px;
}
.col-content {
}
.chat-toolbar {
padding: 10px;
border-top: 1px solid var(--border-color);
display: flex;
gap: 10px;
background-color: #fff;
flex-wrap: wrap;
.toolbar-btn {
padding: 4px 12px;
border: 1px solid #dcdcdc;
border-radius: 4px;
background-color: #fff;
font-size: 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
&:hover {
background-color: #f5f5f5;
}
}
}
.empty-state {
display: flex;
align-items: center;
@@ -1970,143 +1877,6 @@
}
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
min-width: 400px;
max-width: 90vw;
max-height: 90vh;
overflow: auto;
}
.ai-settings-modal {
width: min(460px, 90vw);
}
.ai-settings-modal h3 {
margin: 0 0 18px;
}
.ai-filter-section {
margin-bottom: 16px;
}
.ai-filter-label {
margin-bottom: 7px;
color: #333;
font-size: 14px;
font-weight: 600;
}
.ai-date-options {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
}
.ai-date-options label {
display: flex;
min-width: 0;
align-items: center;
justify-content: center;
gap: 5px;
padding: 8px 6px;
border: 1px solid #d8dcdf;
border-radius: 6px;
background: #fff;
color: #4a5257;
cursor: pointer;
font-size: 13px;
}
.ai-date-options label.selected {
border-color: #07c160;
background: #eefaf3;
color: #078f49;
}
.ai-date-options input,
.ai-type-options input {
margin: 0;
accent-color: #07c160;
}
.ai-type-options {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px 10px;
padding: 11px;
border: 1px solid #e1e4e6;
border-radius: 6px;
background: #f8f9fa;
}
.ai-type-options label {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
color: #3f474c;
cursor: pointer;
font-size: 13px;
white-space: nowrap;
}
.image-preview-modal {
width: min(520px, 92vw);
max-height: 88vh;
background: #f7f8fa;
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.18);
padding: 16px;
overflow: hidden;
}
.report-preview-frame {
border: 1px solid #d8dde3;
border-radius: 10px;
background: #eef1f4;
padding: 12px;
}
.report-preview-scroller {
max-height: calc(88vh - 120px);
overflow-y: auto;
overflow-x: hidden;
padding-right: 4px;
}
.report-preview-image {
display: block;
width: min(430px, 100%);
height: auto;
margin: 0 auto;
border-radius: 8px;
border: 1px solid #ccd3d9;
background: #fff;
}
.report-preview-actions {
margin-top: 12px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.database-account-list {
display: grid;
gap: 10px;
-201
View File
@@ -109,39 +109,6 @@
stroke-linejoin: round;
}
.conversation-date-range {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 6px;
margin-top: 10px;
-webkit-app-region: no-drag;
}
.conversation-date-range-button {
box-sizing: border-box;
height: 30px;
min-width: 0;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: var(--wxex-bg-elevated);
color: var(--wxex-text-secondary);
cursor: pointer;
font: 12px/16px var(--wxex-font);
white-space: nowrap;
&:hover {
border-color: rgba(36, 122, 99, 0.28);
color: var(--wxex-text-primary);
}
&.active {
border-color: var(--wxex-brand);
background: var(--wxex-brand);
color: #ffffff;
font-weight: 600;
}
}
.conversation-list {
flex: 1;
min-height: 0;
@@ -320,15 +287,6 @@
white-space: nowrap;
}
.conversation-item-meta {
overflow: hidden;
color: var(--wxex-text-muted);
font-size: 12px;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.conversation-sidebar-account {
box-sizing: border-box;
flex: 0 0 auto;
@@ -345,153 +303,6 @@
flex: 1;
}
.sidebar {
background-color: var(--sidebar-bg);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
flex-shrink: 0;
/* Prevent shrinking */
}
.sidebar-header {
flex: 0 0 auto;
padding: 10px;
border-bottom: 1px solid var(--border-color);
-webkit-app-region: drag;
}
.sidebar-footer {
padding: 10px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
}
.sidebar-btn {
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
color: #666;
&:hover {
color: #333;
}
}
.sidebar-status {
color: #07c160;
font-size: 12px;
}
.search-input {
width: 100%;
padding: 5px;
border-radius: 4px;
border: 1px solid #ccc;
-webkit-app-region: no-drag;
}
.date-range-selector {
display: flex;
gap: 5px;
margin-top: 8px;
justify-content: space-between;
}
.range-btn {
padding: 2px 6px;
font-size: 10px;
border: 1px solid #ccc;
border-radius: 10px;
background: #fff;
cursor: pointer;
color: #666;
flex: 1;
text-align: center;
&.active {
background: #07c160;
color: #fff;
border-color: #07c160;
}
}
.contact-list {
flex: 1;
overflow-y: auto;
}
.section-header {
padding: 8px 10px;
background-color: #f0f0f0;
font-size: 12px;
color: #666;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
user-select: none;
&:hover {
background-color: #e0e0e0;
}
.arrow {
margin-right: 5px;
font-size: 10px;
width: 12px;
}
}
.section-empty {
padding: 10px 14px 12px 27px;
color: #888;
font-size: 12px;
}
.contact-item {
padding: 10px;
display: flex;
align-items: center;
cursor: pointer;
&:hover {
background-color: var(--sidebar-hover);
}
&.active {
background-color: var(--sidebar-active);
}
}
.contact-avatar {
width: 40px;
height: 40px;
border-radius: 4px;
background-color: #ccc;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #fff;
overflow: hidden;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.message-avatar {
img {
width: 100%;
@@ -501,18 +312,6 @@
}
}
.contact-info {
flex: 1;
overflow: hidden;
}
.contact-name {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.resizer {
width: 4px;
cursor: col-resize;
-34
View File
@@ -361,40 +361,6 @@ body {
background: var(--wxex-bg-main);
}
.app-page-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
color: var(--wxex-text-secondary);
text-align: center;
h2 {
margin: 0 0 8px;
color: var(--wxex-text-primary);
font-size: 20px;
font-weight: 600;
line-height: 28px;
}
p {
margin: 0;
font-size: 13px;
line-height: 20px;
}
}
.app-page-placeholder-eyebrow {
margin-bottom: 8px;
color: var(--wxex-brand);
font-size: 11px;
font-weight: 700;
letter-spacing: 0;
}
.app-container {
display: flex;
width: 100%;
+116 -89
View File
@@ -226,7 +226,7 @@
padding: 8px 10px;
border: 1px solid rgba(200, 90, 90, 0.35);
border-radius: var(--wxex-radius-md);
background: #fff4f4;
background: color-mix(in srgb, var(--wxex-danger) 10%, var(--wxex-bg-elevated));
color: var(--wxex-danger);
font: 12px/17px var(--wxex-font);
}
@@ -248,7 +248,6 @@
}
.report-empty-state,
.report-config-section,
.report-privacy-note,
.report-result-panel {
margin-bottom: 16px;
@@ -258,6 +257,11 @@
background: var(--wxex-bg-elevated);
}
.report-config-section {
margin-bottom: 22px;
padding: 0;
}
.report-empty-state h2,
.report-privacy-note h3,
.report-config-section h3,
@@ -317,7 +321,11 @@
.report-density-options {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
gap: 2px;
padding: 3px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: var(--wxex-bg-sidebar);
}
.report-density-options {
@@ -334,7 +342,11 @@
.report-type-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
gap: 1px;
overflow: hidden;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: var(--wxex-border);
}
.report-check-row {
@@ -342,11 +354,17 @@
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: start;
gap: 8px;
padding: 10px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: #fbfcfb;
min-width: 0;
padding: 11px 12px;
border: 0;
border-radius: 0;
background: var(--wxex-bg-elevated);
color: var(--wxex-text-primary);
transition: background-color var(--tm-motion-fast) var(--tm-ease-standard);
}
.report-check-row:hover {
background: var(--wxex-bg-sidebar);
}
.report-check-row span {
@@ -372,7 +390,11 @@
.report-readonly-modules {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
gap: 1px;
overflow: hidden;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: var(--wxex-border);
cursor: default;
}
@@ -381,10 +403,10 @@
min-width: 0;
align-items: center;
gap: 8px;
padding: 8px 10px;
border: 1px solid #e9eeeb;
border-radius: var(--wxex-radius-md);
background: #f7f9f8;
padding: 9px 10px;
border: 0;
border-radius: 0;
background: var(--wxex-bg-elevated);
color: var(--wxex-text-secondary);
font: 12px/17px var(--wxex-font);
cursor: default;
@@ -477,11 +499,11 @@
.ai-report-footer {
display: flex;
flex: 0 0 auto;
min-height: 66px;
min-height: 64px;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 28px;
padding: 10px 28px;
border-top: 1px solid var(--wxex-border);
background: var(--wxex-bg-main);
}
@@ -494,7 +516,10 @@
}
.report-footer-note {
flex: 1 1 auto;
overflow: hidden;
color: var(--wxex-text-muted);
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -504,12 +529,14 @@
min-width: 0;
flex: 0 0 auto;
align-items: center;
gap: 12px;
gap: 10px;
}
.report-footer-actions span {
max-width: 280px;
max-width: 240px;
overflow: hidden;
color: var(--wxex-text-muted);
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -522,7 +549,7 @@
flex-direction: column;
overflow: hidden;
border-left: 1px solid var(--wxex-border);
background: #f7f9f8;
background: var(--wxex-bg-sidebar);
}
.report-task-header {
@@ -657,7 +684,7 @@
}
.report-image-insight-list article.failed {
background: #fff7f3;
background: color-mix(in srgb, var(--wxex-danger) 8%, var(--wxex-bg-main));
}
.report-image-insight-list article > div {
@@ -687,7 +714,7 @@
.report-image-decision {
border-color: rgba(197, 137, 48, 0.4);
background: #fffaf0;
background: color-mix(in srgb, var(--wxex-warning) 10%, var(--wxex-bg-main));
}
.report-image-decision p {
@@ -796,7 +823,7 @@
.report-task-error {
border: 1px solid rgba(200, 90, 90, 0.35);
background: #fff4f4;
background: color-mix(in srgb, var(--wxex-danger) 10%, var(--wxex-bg-sidebar));
color: var(--wxex-danger);
}
@@ -846,16 +873,17 @@
height: 100%;
min-width: 0;
overflow: auto;
padding: 34px 40px 48px;
background: #fafbfa;
padding: 26px 32px 36px;
background: var(--wxex-bg-main);
color: var(--wxex-text-primary);
}
.agent-hub-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
gap: 20px;
max-width: 1080px;
margin: 0 auto 28px;
margin: 0 auto 20px;
}
.agent-hub-eyebrow,
.agent-hub-card-kicker {
@@ -867,7 +895,7 @@
.agent-hub-header h1 {
margin: 4px 0 6px;
color: var(--wxex-text-primary);
font: 750 28px/36px var(--wxex-font);
font: 750 24px/32px var(--wxex-font);
}
.agent-hub-header p,
.agent-hub-card p {
@@ -880,22 +908,22 @@
align-items: center;
padding: 6px 10px;
border-radius: 999px;
background: #f0f2f0;
background: var(--wxex-bg-sidebar);
color: var(--wxex-text-muted);
font: 700 12px/18px var(--wxex-font);
}
.agent-hub-runtime.online {
background: #e8f6ed;
color: #24864b;
background: color-mix(in srgb, var(--wxex-success) 12%, var(--wxex-bg-elevated));
color: var(--wxex-success);
}
.agent-hub-runtime.error {
background: #fff0ef;
background: color-mix(in srgb, var(--wxex-danger) 10%, var(--wxex-bg-elevated));
color: var(--wxex-danger);
}
.agent-hub-grid {
display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(280px, 0.75fr);
gap: 20px;
gap: 16px;
max-width: 1080px;
margin: 0 auto;
}
@@ -903,18 +931,17 @@
box-sizing: border-box;
min-width: 0;
border: 1px solid var(--wxex-border);
border-radius: 18px;
border-radius: var(--wxex-radius-lg);
background: var(--wxex-bg-elevated);
box-shadow: 0 8px 28px rgba(28, 38, 31, 0.045);
}
.agent-hub-login-card {
min-height: 520px;
padding: 26px;
min-height: 420px;
padding: 22px;
display: flex;
flex-direction: column;
}
.agent-hub-capability-card {
padding: 26px;
padding: 22px;
align-self: start;
}
.agent-hub-card-heading {
@@ -922,7 +949,7 @@
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--wxex-border);
}
.agent-hub-card h2 {
@@ -936,7 +963,7 @@
gap: 7px;
padding: 5px 9px;
border-radius: 999px;
background: #f2f3f2;
background: var(--wxex-bg-sidebar);
color: var(--wxex-text-secondary);
font: 650 12px/18px var(--wxex-font);
white-space: nowrap;
@@ -945,28 +972,28 @@
width: 7px;
height: 7px;
border-radius: 50%;
background: #9aa39d;
background: var(--wxex-text-muted);
}
.agent-hub-status.online {
background: #e8f6ed;
color: #24864b;
background: color-mix(in srgb, var(--wxex-success) 12%, var(--wxex-bg-elevated));
color: var(--wxex-success);
}
.agent-hub-status.online i {
background: #35a663;
background: var(--wxex-success);
}
.agent-hub-status.waiting_scan,
.agent-hub-status.scanned,
.agent-hub-status.starting {
background: #fff5de;
color: #9b6a12;
background: color-mix(in srgb, var(--wxex-warning) 12%, var(--wxex-bg-elevated));
color: var(--wxex-warning);
}
.agent-hub-status.waiting_scan i,
.agent-hub-status.scanned i,
.agent-hub-status.starting i {
background: #d89b2b;
background: var(--wxex-warning);
}
.agent-hub-status.error {
background: #fff0ef;
background: color-mix(in srgb, var(--wxex-danger) 10%, var(--wxex-bg-elevated));
color: var(--wxex-danger);
}
.agent-hub-status.error i {
@@ -978,13 +1005,13 @@
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 280px;
min-height: 210px;
text-align: center;
}
.agent-hub-empty-login h3,
.agent-hub-connected h3,
.agent-hub-qr-copy h3 {
margin: 18px 0 6px;
margin: 14px 0 5px;
color: var(--wxex-text-primary);
font: 700 16px/24px var(--wxex-font);
}
@@ -992,26 +1019,26 @@
max-width: 360px;
}
.agent-hub-phone {
width: 64px;
height: 88px;
width: 56px;
height: 76px;
position: relative;
border: 2px solid #9eb3a5;
border-radius: 13px;
background: linear-gradient(145deg, #f6fbf7, #eaf4ed);
border: 2px solid color-mix(in srgb, var(--wxex-brand) 45%, var(--wxex-border));
border-radius: 11px;
background: var(--wxex-brand-soft);
}
.agent-hub-phone::before {
content: '';
position: absolute;
top: 8px;
left: 22px;
width: 18px;
left: 19px;
width: 16px;
height: 3px;
border-radius: 3px;
background: #b8c8bd;
background: color-mix(in srgb, var(--wxex-brand) 30%, var(--wxex-bg-elevated));
}
.agent-hub-phone span {
position: absolute;
left: 23px;
left: 19px;
bottom: 8px;
width: 16px;
height: 16px;
@@ -1023,7 +1050,7 @@
justify-content: center;
gap: 10px;
min-height: 38px;
padding-top: 18px;
padding-top: 14px;
}
.agent-hub-qr-panel {
flex: 1;
@@ -1076,17 +1103,17 @@
display: grid;
place-items: center;
border-radius: 50%;
background: #e4f5ea;
color: #23834a;
background: color-mix(in srgb, var(--wxex-success) 14%, var(--wxex-bg-elevated));
color: var(--wxex-success);
font: 800 22px/1 var(--wxex-font);
}
.agent-hub-capability-card > p {
margin-top: 8px;
}
.agent-hub-example {
margin: 24px 0 20px;
padding: 16px;
border-radius: 12px;
margin: 18px 0 14px;
padding: 14px;
border-radius: var(--wxex-radius-md);
background: var(--wxex-brand-soft);
}
.agent-hub-example span {
@@ -1109,7 +1136,7 @@
display: flex;
align-items: center;
gap: 9px;
padding: 9px 0;
padding: 7px 0;
border-bottom: 1px solid var(--wxex-border);
color: var(--wxex-text-secondary);
font: 13px/19px var(--wxex-font);
@@ -1133,8 +1160,8 @@
}
.agent-hub-log-card {
max-width: 1080px;
margin: 20px auto 0;
padding: 22px;
margin: 16px auto 0;
padding: 20px 22px;
}
.agent-hub-log-heading {
display: flex;
@@ -1245,13 +1272,13 @@
}
.ai-vision-test h2 {
color: #202724;
color: var(--wxex-text-primary);
font-size: 15px;
}
.ai-vision-test header h3 {
margin-top: 12px;
color: #35403b;
color: var(--wxex-text-primary);
font-size: 13px;
}
@@ -1259,22 +1286,22 @@
.ai-vision-model,
.ai-vision-upload small {
margin-top: 4px;
color: #66706b;
color: var(--wxex-text-secondary);
font-size: 12px;
}
.ai-vision-capability {
border-radius: 999px;
padding: 5px 9px;
background: #f1f3f2;
color: #66706b;
background: var(--wxex-bg-sidebar);
color: var(--wxex-text-secondary);
white-space: nowrap;
font-size: 12px;
}
.ai-vision-capability.supported {
background: #eaf5f1;
color: #2e8b68;
background: var(--wxex-brand-soft);
color: var(--wxex-brand);
}
.ai-vision-model {
@@ -1288,18 +1315,18 @@
align-items: center;
justify-content: center;
gap: 14px;
border: 1px dashed #bfc9c4;
border-radius: 10px;
border: 1px dashed var(--wxex-border);
border-radius: var(--wxex-radius-md);
padding: 14px;
background: #f7f9f8;
color: #35403b;
background: var(--wxex-bg-sidebar);
color: var(--wxex-text-primary);
text-align: center;
cursor: pointer;
}
.ai-vision-upload:hover {
border-color: #247a63;
background: #f2f8f5;
border-color: var(--wxex-brand);
background: var(--wxex-brand-soft);
}
.ai-vision-upload input {
@@ -1333,26 +1360,26 @@
}
.ai-vision-privacy {
color: #66706b;
color: var(--wxex-text-secondary);
font-size: 12px;
line-height: 1.6;
}
.ai-vision-error {
border-left: 3px solid #c85a5a;
border-left: 3px solid var(--wxex-danger);
padding: 9px 11px;
background: #fff2f2;
color: #a84444;
background: color-mix(in srgb, var(--wxex-danger) 12%, transparent);
color: var(--wxex-danger);
font-size: 12px;
}
.ai-vision-result {
display: grid;
gap: 12px;
border: 1px solid #dce7e2;
border-radius: 9px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
padding: 14px;
background: #f7faf9;
background: var(--wxex-bg-sidebar);
}
.ai-vision-result dl {
@@ -1363,20 +1390,20 @@
}
.ai-vision-result dt {
color: #929a96;
color: var(--wxex-text-muted);
font-size: 11px;
}
.ai-vision-result dd {
margin: 4px 0 0;
color: #35403b;
color: var(--wxex-text-primary);
font-size: 12px;
font-weight: 600;
}
.ai-vision-result > p {
white-space: pre-wrap;
color: #3d4742;
color: var(--wxex-text-secondary);
font-size: 13px;
line-height: 1.7;
}
@@ -555,7 +555,7 @@
padding: 12px 14px;
border: 1.5px solid var(--wxex-border);
border-radius: 12px;
background: #fff;
background: var(--wxex-bg-elevated);
transition:
border-color 0.2s,
background 0.2s;
@@ -563,7 +563,7 @@
.report-template-item.active {
border-color: var(--wxex-primary, #07c160);
background: #f0fbf3;
background: var(--wxex-brand-soft);
}
.report-template-item.disabled {
+5 -441
View File
@@ -20,7 +20,7 @@
min-height: var(--ai-search-header-height);
padding: 14px 22px 12px;
border-bottom: 1px solid var(--wxex-border);
background: rgba(250, 251, 250, 0.94);
background: color-mix(in srgb, var(--wxex-bg-main) 94%, transparent);
h1 {
margin: 0;
@@ -334,50 +334,6 @@
font-weight: 700;
}
.ai-search-local-badge {
border-radius: var(--wxex-radius-sm);
background: var(--wxex-ai-soft);
color: var(--wxex-ai);
font-size: 10px;
font-weight: 700;
line-height: 16px;
padding: 2px 6px;
white-space: nowrap;
}
.ai-search-scope-toggle,
.ai-search-range-grid {
display: grid;
gap: 4px;
}
.ai-search-scope-toggle {
grid-template-columns: 1fr 1fr;
margin-bottom: 18px;
padding: 3px;
border-radius: var(--wxex-radius-sm);
background: var(--wxex-bg-elevated);
button {
padding: 7px 4px;
font-size: 11px;
&.active {
background: var(--wxex-brand);
color: #fff;
font-weight: 700;
}
}
}
.ai-search-scope-help {
min-height: 30px;
margin: -10px 2px 12px;
color: var(--wxex-text-muted);
font-size: 10px;
line-height: 15px;
}
.ai-search-field-label {
display: block;
margin: 0 0 7px;
@@ -387,178 +343,6 @@
line-height: 16px;
}
.ai-search-filter-input {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
margin-bottom: 10px;
padding: 7px 9px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-sm);
background: var(--wxex-bg-elevated);
svg {
width: 15px;
height: 15px;
flex: 0 0 auto;
color: var(--wxex-text-muted);
}
input {
width: 100%;
min-width: 0;
border: 0;
outline: 0;
background: transparent;
color: var(--wxex-text-primary);
font: inherit;
font-size: 11px;
}
}
.ai-search-contact-list {
display: flex;
max-height: 168px;
flex-direction: column;
gap: 3px;
margin-bottom: 18px;
overflow-y: auto;
}
.ai-search-contact {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
padding: 7px;
border: 0;
border-radius: var(--wxex-radius-sm);
background: transparent;
color: var(--wxex-text-primary);
cursor: pointer;
font: inherit;
text-align: left;
&:hover,
&.active {
background: var(--wxex-brand-soft);
}
}
.ai-search-contact-avatar {
width: 28px;
height: 28px;
display: grid;
place-items: center;
flex: 0 0 auto;
overflow: hidden;
border-radius: var(--wxex-radius-sm);
background: var(--wxex-bg-elevated);
color: var(--wxex-brand);
font-size: 12px;
font-weight: 700;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.ai-search-contact > span:last-child {
display: flex;
min-width: 0;
flex-direction: column;
gap: 1px;
}
.ai-search-contact strong,
.ai-search-contact small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ai-search-contact strong {
font-size: 11px;
font-weight: 700;
line-height: 16px;
}
.ai-search-contact small {
color: var(--wxex-text-muted);
font-size: 10px;
line-height: 14px;
}
.ai-search-range-grid {
padding: 4px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: var(--wxex-bg-elevated);
grid-template-columns: 1fr 1fr;
margin-bottom: 16px;
button {
border: 0;
border-radius: var(--wxex-radius-sm);
background: transparent;
color: var(--wxex-text-secondary);
cursor: pointer;
font: inherit;
min-height: 34px;
padding: 7px 4px;
border: 1px solid transparent;
font-size: 11px;
&:hover {
border-color: var(--wxex-brand);
color: var(--wxex-brand);
}
&.active {
border-color: var(--wxex-brand);
background: var(--wxex-brand);
color: #fff;
box-shadow: 0 1px 3px rgba(34, 89, 74, 0.2);
}
}
}
.ai-search-field-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.ai-search-field-heading .ai-search-field-label {
margin-bottom: 7px;
}
.ai-search-field-heading > span {
color: var(--wxex-brand);
font-size: 10px;
font-weight: 700;
}
.ai-search-filter-note {
display: flex;
gap: 6px;
padding: 9px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-sm);
background: rgba(255, 255, 255, 0.48);
color: var(--wxex-text-secondary);
font-size: 10px;
line-height: 16px;
span {
color: var(--wxex-success);
}
}
.ai-search-main {
display: flex;
min-width: 0;
@@ -640,41 +424,6 @@
}
}
.ai-search-spinner {
width: 42px;
height: 42px;
margin-bottom: 20px;
border: 3px solid var(--wxex-brand-soft);
border-top-color: var(--wxex-brand);
border-radius: 50%;
animation: ai-search-spin 0.9s linear infinite;
}
@keyframes ai-search-spin {
to {
transform: rotate(360deg);
}
}
.ai-search-loading-steps {
display: flex;
flex-direction: column;
gap: 9px;
margin-top: 24px;
color: var(--wxex-text-muted);
font-size: 11px;
text-align: left;
.done {
color: var(--wxex-success);
}
.active {
color: var(--wxex-brand);
font-weight: 700;
}
}
.ai-search-loading {
align-items: flex-start;
justify-content: flex-start;
@@ -745,7 +494,7 @@
}
&.error {
background: #fff8ef;
background: color-mix(in srgb, var(--wxex-warning) 10%, var(--wxex-bg-elevated));
strong,
p {
@@ -782,7 +531,7 @@
.ai-search-pipeline-step.error .ai-search-pipeline-mark {
border-color: var(--wxex-warning);
background: #fff1df;
background: color-mix(in srgb, var(--wxex-warning) 12%, var(--wxex-bg-elevated));
color: var(--wxex-warning);
}
@@ -1062,55 +811,6 @@
}
}
.ai-search-knowledge-status {
display: grid;
gap: 4px;
margin: 0 0 16px;
padding: 10px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: var(--wxex-bg-elevated);
}
.ai-search-knowledge-status > div {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.ai-search-knowledge-status > div span,
.ai-search-knowledge-status p,
.ai-search-knowledge-status small {
margin: 0;
color: var(--wxex-text-muted);
font-size: 10px;
line-height: 15px;
}
.ai-search-knowledge-status > div span {
font-weight: 700;
letter-spacing: 0.04em;
}
.ai-search-knowledge-status strong {
color: var(--wxex-text-primary);
font-size: 11px;
}
.ai-search-knowledge-status.building,
.ai-search-knowledge-status.syncing {
border-color: var(--wxex-ai);
}
.ai-search-knowledge-status.ready strong {
color: var(--wxex-success);
}
.ai-search-knowledge-status.error {
border-color: var(--wxex-warning);
}
.ai-search-insufficient-icon {
width: 44px;
height: 44px;
@@ -1141,7 +841,6 @@
}
@media (prefers-reduced-motion: reduce) {
.ai-search-spinner,
.ai-search-result,
.ai-search-summary-block,
.ai-search-answer > *,
@@ -1158,12 +857,6 @@
font-size: 11px;
font-weight: 600;
}
.ai-search-muted {
color: var(--wxex-text-muted);
font-size: 10px;
line-height: 16px;
}
@media (max-width: 1120px) {
.ai-search-workspace {
--ai-search-sidebar-width: 210px;
@@ -1190,10 +883,6 @@
padding-top: 12px;
}
.ai-search-contact-list {
max-height: 148px;
}
.ai-search-empty,
.ai-search-loading,
.ai-search-insufficient {
@@ -1217,7 +906,7 @@
flex: 0 0 auto;
padding: 10px 22px 12px;
border-bottom: 1px solid var(--wxex-border);
background: #f7faf8;
background: var(--wxex-bg-sidebar);
}
.ai-search-debug-header {
@@ -1263,7 +952,7 @@
overflow: auto;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-sm);
background: #fff;
background: var(--wxex-bg-elevated);
color: var(--wxex-text-secondary);
font:
10px/16px ui-monospace,
@@ -1274,127 +963,6 @@
word-break: break-word;
}
.ai-search-task-button {
padding: 7px 10px;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-sm);
background: var(--wxex-bg-elevated);
color: var(--wxex-text-secondary);
cursor: pointer;
font: inherit;
font-size: 11px;
white-space: nowrap;
}
.ai-search-task-button.active,
.ai-search-task-button:hover {
border-color: var(--wxex-brand);
color: var(--wxex-brand);
}
.ai-search-task-center {
flex: 0 0 auto;
max-height: 218px;
overflow-y: auto;
padding: 10px 22px 8px;
border-bottom: 1px solid var(--wxex-border);
background: var(--wxex-bg-elevated);
}
.ai-search-task-center-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding-bottom: 10px;
border-bottom: 1px solid var(--wxex-border);
}
.ai-search-task-center-header div {
display: grid;
gap: 2px;
}
.ai-search-task-center-header strong {
color: var(--wxex-text-primary);
font-size: 12px;
}
.ai-search-task-center-header span,
.ai-search-task-center-header > span {
color: var(--wxex-text-muted);
font-size: 10px;
}
.ai-search-task-row {
display: grid;
grid-template-columns: 8px minmax(0, 1fr) auto;
align-items: center;
gap: 9px;
padding: 10px 0;
border-bottom: 1px solid var(--wxex-border);
}
.ai-search-task-row:last-child {
border-bottom: 0;
}
.ai-search-task-status {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--wxex-text-muted);
}
.ai-search-task-status.running {
background: var(--wxex-brand);
box-shadow: 0 0 0 3px var(--wxex-brand-soft);
}
.ai-search-task-status.completed {
background: var(--wxex-success);
}
.ai-search-task-status.failed {
background: var(--wxex-warning);
}
.ai-search-task-row > div {
display: grid;
min-width: 0;
gap: 2px;
}
.ai-search-task-row strong,
.ai-search-task-row small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ai-search-task-row strong {
color: var(--wxex-text-primary);
font-size: 11px;
}
.ai-search-task-row small {
color: var(--wxex-text-muted);
font-size: 10px;
}
.ai-search-task-result {
color: var(--wxex-text-secondary);
font-size: 10px;
white-space: nowrap;
}
.ai-search-task-empty {
margin: 12px 0 2px;
color: var(--wxex-text-muted);
font-size: 11px;
text-align: center;
}
.ai-search-markdown-spacer {
height: 8px;
}
@@ -1442,8 +1010,4 @@
flex-direction: column;
gap: 6px;
}
.ai-search-task-row {
grid-template-columns: 8px minmax(0, 1fr) auto;
}
}
@@ -1,45 +1,3 @@
.settings-primary-button,
.settings-danger-button {
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-sm);
background: var(--wxex-bg-elevated);
color: var(--wxex-text-primary);
cursor: pointer;
font: 12px/18px var(--wxex-font);
padding: 8px 12px;
&:hover:not(:disabled) {
border-color: var(--wxex-brand);
color: var(--wxex-brand);
}
&:disabled {
cursor: not-allowed;
opacity: 0.55;
}
}
.settings-primary-button {
border-color: var(--wxex-brand);
background: var(--wxex-brand);
color: #fff;
font-weight: 600;
&:hover:not(:disabled) {
background: var(--wxex-brand-hover);
color: #fff;
}
}
.settings-danger-button {
color: var(--wxex-danger);
&:hover:not(:disabled) {
border-color: var(--wxex-danger);
color: var(--wxex-danger);
}
}
.settings-card-kicker {
display: block;
margin-bottom: 6px;
@@ -306,7 +264,7 @@
cursor: pointer;
&:hover {
background: var(--wxex-surface-muted, #f7f9f8);
background: var(--wxex-bg-sidebar);
}
&.selected {
@@ -322,7 +280,7 @@
overflow: hidden;
border: 1px solid var(--wxex-border);
border-radius: 50%;
background: var(--wxex-surface-muted, #f7f9f8);
background: var(--wxex-bg-sidebar);
color: var(--wxex-text-secondary);
font-size: 12px;
}
@@ -383,7 +341,7 @@
align-content: start;
gap: 16px;
padding: 14px;
background: var(--wxex-surface-muted, #f7f9f8);
background: var(--wxex-bg-sidebar);
}
.voice-batch-field {
+4 -101
View File
@@ -18,7 +18,7 @@
min-width: 0;
min-height: 0;
overflow: hidden;
background: #fafbfa;
background: var(--wxex-bg-main);
}
.settings-page-panel {
@@ -244,9 +244,9 @@
font-size: 16px;
}
.settings-card {
border: 1px solid #dde3e0;
border-radius: 10px;
background: #fff;
border: 1px solid var(--wxex-border);
border-radius: var(--wxex-radius-md);
background: var(--wxex-bg-elevated);
padding: 24px 26px;
}
.settings-account-overview {
@@ -505,34 +505,6 @@
.database-key-muted {
color: #66706b !important;
}
.database-key-primary,
.database-key-secondary {
min-height: 36px;
padding: 0 15px;
border: 1px solid #247a63;
border-radius: 8px;
cursor: pointer;
font: 600 13px/1 inherit;
}
.database-key-primary {
background: #247a63;
color: #fff;
}
.database-key-secondary {
background: #fff;
color: #247a63;
}
.database-key-primary:hover:not(:disabled) {
background: #1d6754;
}
.database-key-secondary:hover:not(:disabled) {
background: #f0f7f4;
}
.database-key-primary:disabled,
.database-key-secondary:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.database-key-editor label {
display: block;
margin-bottom: 9px;
@@ -841,52 +813,6 @@
color: #786868;
font-size: 12px;
}
.database-key-confirm-backdrop {
position: fixed;
z-index: 30;
inset: 0;
display: grid;
place-items: center;
padding: 24px;
background: rgba(25, 32, 29, 0.34);
}
.database-key-confirm {
width: min(100%, 430px);
padding: 22px;
border-radius: 12px;
background: #fff;
box-shadow: 0 18px 50px rgba(24, 35, 30, 0.2);
}
.database-key-confirm h2 {
margin: 0;
color: #202724;
font-size: 17px;
}
.database-key-confirm p {
margin: 12px 0 22px;
color: #66706b;
font-size: 13px;
line-height: 1.7;
}
.database-key-confirm > div {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.database-key-confirm button {
min-height: 34px;
border: 1px solid #dde3e0;
border-radius: 8px;
padding: 0 14px;
background: #fff;
color: #46514c;
cursor: pointer;
}
.database-key-confirm button.danger {
border-color: #c85a5a;
background: #c85a5a;
color: #fff;
}
/* SETTINGS-03: image decryption */
.image-decryption-content {
padding-bottom: 48px;
@@ -1102,29 +1028,6 @@
font-size: 12px;
line-height: 1.6;
}
.image-test-result {
display: flex;
flex-wrap: wrap;
gap: 8px 18px;
margin-top: 14px;
padding: 12px 14px;
border-radius: 8px;
color: #2e765d;
font-size: 12px;
}
.image-test-result.success {
border: 1px solid #bfded3;
background: #eaf5f1;
}
.image-test-result.error {
border: 1px solid #efcaca;
background: #fff2f2;
color: #a84444;
}
.image-test-result p {
width: 100%;
margin: 2px 0 0;
}
.image-step-list {
list-style: none;
margin: 14px 0 0;
@@ -1,71 +1,3 @@
/* Sidebar 自助卡片 + 入口 */
.sidebar-footer {
padding: 10px 12px;
border-top: 1px solid var(--border-color);
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
background-color: #f3f4f5;
transition: background-color 0.15s ease;
user-select: none;
&:hover {
background-color: #e6e9eb;
}
}
.sidebar-self-avatar {
width: 36px;
height: 36px;
border-radius: 6px;
background-color: #07c160;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 500;
overflow: hidden;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.sidebar-self-info {
flex: 1;
min-width: 0;
overflow: hidden;
}
.sidebar-self-nickname {
font-size: 13px;
font-weight: 500;
color: #1f2429;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-self-wxid {
font-size: 11px;
color: #8a9298;
margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-self-arrow {
color: #8a9298;
font-size: 18px;
flex-shrink: 0;
}
/* 启动自动连接 Splash */
.boot-splash {
position: fixed;
+81 -44
View File
@@ -4,26 +4,34 @@
@layer base {
:root {
--tm-background: 150 18% 96%;
--tm-foreground: 150 12% 15%;
--tm-background: 150 10% 96.0784%;
--tm-canvas: 120 11.1111% 98.2353%;
--tm-foreground: 154.2857 9.8592% 13.9216%;
--tm-surface: 0 0% 100%;
--tm-surface-muted: 150 14% 94%;
--tm-surface-muted: 150 13.3333% 94.1176%;
--tm-surface-elevated: 0 0% 100%;
--tm-border: 150 13% 87%;
--tm-border: 150 9.6774% 87.8431%;
--tm-border-subtle: 150 10% 92%;
--tm-primary: 161 54% 31%;
--tm-primary: 163.9535 54.4304% 30.9804%;
--tm-primary-hover: 164.5946 56.0606% 25.8824%;
--tm-primary-foreground: 0 0% 100%;
--tm-secondary: 150 13% 91%;
--tm-secondary-foreground: 150 12% 20%;
--tm-muted: 150 11% 92%;
--tm-muted-foreground: 150 8% 44%;
--tm-accent: 157 39% 89%;
--tm-accent-foreground: 161 54% 25%;
--tm-success: 155 51% 37%;
--tm-warning: 34 57% 49%;
--tm-destructive: 0 51% 54%;
--tm-secondary: 150 13.3333% 94.1176%;
--tm-secondary-foreground: 154.2857 9.8592% 13.9216%;
--tm-muted: 150 10% 92%;
--tm-muted-foreground: 150 4.6729% 41.9608%;
--tm-subtle-foreground: 150 3.8095% 58.8235%;
--tm-accent: 157.5 30.7692% 89.8039%;
--tm-accent-foreground: 163.9535 54.4304% 30.9804%;
--tm-ai: 236.8966 62.3656% 63.5294%;
--tm-ai-soft: 236 78.9474% 96.2745%;
--tm-success: 157.4194 50.2703% 36.2745%;
--tm-warning: 33.5172 57.7689% 49.2157%;
--tm-destructive: 0 50% 56.8627%;
--tm-destructive-foreground: 0 0% 100%;
--tm-focus-ring: 161 54% 38%;
--tm-focus-ring: 164 54.4% 31%;
--tm-disabled-surface: 150 13.3333% 94.1176%;
--tm-disabled-foreground: 150 3.8095% 58.8235%;
--tm-disabled-border: 150 10% 92%;
--tm-radius-sm: 0.375rem;
--tm-radius-md: 0.5rem;
--tm-radius-lg: 0.75rem;
@@ -37,6 +45,9 @@
--tm-motion-slow: 260ms;
--tm-ease-standard: cubic-bezier(0.2, 0, 0, 1);
--tm-ease-emphasized: cubic-bezier(0.16, 1, 0.3, 1);
--tm-control-height-compact: 1.75rem;
--tm-control-height-standard: 2rem;
--tm-control-height-form: 2.25rem;
--tm-z-base: 0;
--tm-z-dropdown: 40;
--tm-z-popover: 50;
@@ -45,26 +56,34 @@
}
[data-theme='dark'] {
--tm-background: 150 15% 10%;
--tm-foreground: 145 25% 94%;
--tm-background: 165 8% 9.8039%;
--tm-canvas: 160 9.0909% 12.9412%;
--tm-foreground: 145.7143 24.1379% 94.3137%;
--tm-surface: 150 13% 15%;
--tm-surface-muted: 150 12% 18%;
--tm-surface-elevated: 150 12% 19%;
--tm-border: 150 12% 28%;
--tm-surface-muted: 153.3333 12.3288% 14.3137%;
--tm-surface-elevated: 160 10.3448% 17.0588%;
--tm-border: 152.3077 10.2362% 24.902%;
--tm-border-subtle: 150 10% 22%;
--tm-primary: 157 48% 52%;
--tm-primary-foreground: 150 15% 10%;
--tm-secondary: 150 12% 24%;
--tm-secondary-foreground: 145 25% 94%;
--tm-muted: 150 12% 24%;
--tm-muted-foreground: 150 10% 66%;
--tm-accent: 157 31% 25%;
--tm-accent-foreground: 157 48% 82%;
--tm-primary: 163.9535 54.4304% 30.9804%;
--tm-primary-hover: 157.4194 50.2703% 36.2745%;
--tm-primary-foreground: 0 0% 100%;
--tm-secondary: 150 10% 22%;
--tm-secondary-foreground: 145.7143 24.1379% 94.3137%;
--tm-muted: 150 10% 22%;
--tm-muted-foreground: 150 10% 72.549%;
--tm-subtle-foreground: 153.75 6.7797% 53.7255%;
--tm-accent: 158.8235 30.9091% 21.5686%;
--tm-accent-foreground: 145.7143 24.1379% 94.3137%;
--tm-ai: 236.8966 62.3656% 63.5294%;
--tm-ai-soft: 236 24% 20%;
--tm-success: 155 51% 55%;
--tm-warning: 34 75% 63%;
--tm-destructive: 0 65% 66%;
--tm-destructive-foreground: 150 15% 10%;
--tm-destructive-foreground: 165 8% 9.8039%;
--tm-focus-ring: 157 48% 62%;
--tm-disabled-surface: 150 10% 18%;
--tm-disabled-foreground: 153.75 6.7797% 53.7255%;
--tm-disabled-border: 150 10% 22%;
--tm-shadow-surface: 0 1px 2px rgb(0 0 0 / 0.2);
--tm-shadow-floating: 0 8px 24px rgb(0 0 0 / 0.28);
--tm-shadow-popover: 0 12px 32px rgb(0 0 0 / 0.38);
@@ -73,30 +92,48 @@
@media (prefers-color-scheme: dark) {
[data-theme='system'] {
--tm-background: 150 15% 10%;
--tm-foreground: 145 25% 94%;
--tm-background: 165 8% 9.8039%;
--tm-canvas: 160 9.0909% 12.9412%;
--tm-foreground: 145.7143 24.1379% 94.3137%;
--tm-surface: 150 13% 15%;
--tm-surface-muted: 150 12% 18%;
--tm-surface-elevated: 150 12% 19%;
--tm-border: 150 12% 28%;
--tm-surface-muted: 153.3333 12.3288% 14.3137%;
--tm-surface-elevated: 160 10.3448% 17.0588%;
--tm-border: 152.3077 10.2362% 24.902%;
--tm-border-subtle: 150 10% 22%;
--tm-primary: 157 48% 52%;
--tm-primary-foreground: 150 15% 10%;
--tm-secondary: 150 12% 24%;
--tm-secondary-foreground: 145 25% 94%;
--tm-muted: 150 12% 24%;
--tm-muted-foreground: 150 10% 66%;
--tm-accent: 157 31% 25%;
--tm-accent-foreground: 157 48% 82%;
--tm-primary: 163.9535 54.4304% 30.9804%;
--tm-primary-hover: 157.4194 50.2703% 36.2745%;
--tm-primary-foreground: 0 0% 100%;
--tm-secondary: 150 10% 22%;
--tm-secondary-foreground: 145.7143 24.1379% 94.3137%;
--tm-muted: 150 10% 22%;
--tm-muted-foreground: 150 10% 72.549%;
--tm-subtle-foreground: 153.75 6.7797% 53.7255%;
--tm-accent: 158.8235 30.9091% 21.5686%;
--tm-accent-foreground: 145.7143 24.1379% 94.3137%;
--tm-ai: 236.8966 62.3656% 63.5294%;
--tm-ai-soft: 236 24% 20%;
--tm-success: 155 51% 55%;
--tm-warning: 34 75% 63%;
--tm-destructive: 0 65% 66%;
--tm-destructive-foreground: 150 15% 10%;
--tm-destructive-foreground: 165 8% 9.8039%;
--tm-focus-ring: 157 48% 62%;
--tm-disabled-surface: 150 10% 18%;
--tm-disabled-foreground: 153.75 6.7797% 53.7255%;
--tm-disabled-border: 150 10% 22%;
}
}
:where(button, input, textarea, select):focus-visible {
:where(
button,
input,
textarea,
select,
[role='button'],
[role='checkbox'],
[role='radio'],
[role='switch'],
[role='tab']
):focus-visible {
outline: 2px solid hsl(var(--tm-focus-ring) / 0.75);
outline-offset: 2px;
}
+16 -10
View File
@@ -1,13 +1,20 @@
@mixin dark-theme {
--wxex-bg-app: #171b1a;
--wxex-bg-main: #1e2422;
--wxex-bg-sidebar: #202925;
--wxex-bg-elevated: #27302d;
--wxex-text-primary: #edf4f0;
--wxex-text-secondary: #b2c0b9;
--wxex-text-muted: #81918a;
--wxex-border: #394640;
--wxex-brand-soft: #26483c;
--wxex-bg-app: hsl(var(--tm-background));
--wxex-bg-main: hsl(var(--tm-canvas));
--wxex-bg-sidebar: hsl(var(--tm-surface-muted));
--wxex-bg-elevated: hsl(var(--tm-surface-elevated));
--wxex-text-primary: hsl(var(--tm-foreground));
--wxex-text-secondary: hsl(var(--tm-muted-foreground));
--wxex-text-muted: hsl(var(--tm-subtle-foreground));
--wxex-border: hsl(var(--tm-border));
--wxex-brand: hsl(var(--tm-primary));
--wxex-brand-hover: hsl(var(--tm-primary-hover));
--wxex-brand-soft: hsl(var(--tm-accent));
--wxex-ai: hsl(var(--tm-ai));
--wxex-ai-soft: hsl(var(--tm-ai-soft));
--wxex-success: hsl(var(--tm-success));
--wxex-warning: hsl(var(--tm-warning));
--wxex-danger: hsl(var(--tm-destructive));
.conversation-section-header:hover,
.conversation-item:hover,
@@ -52,7 +59,6 @@
color: var(--wxex-text-secondary);
}
.wechat-system-message-meta,
.message-sender-name,
.message-hover-time,
.message-accessible-sender {
+11
View File
@@ -11,6 +11,7 @@ module.exports = {
extend: {
colors: {
background: 'hsl(var(--tm-background) / <alpha-value>)',
canvas: 'hsl(var(--tm-canvas) / <alpha-value>)',
foreground: 'hsl(var(--tm-foreground) / <alpha-value>)',
surface: 'hsl(var(--tm-surface) / <alpha-value>)',
'surface-muted': 'hsl(var(--tm-surface-muted) / <alpha-value>)',
@@ -18,19 +19,29 @@ module.exports = {
border: 'hsl(var(--tm-border) / <alpha-value>)',
'border-subtle': 'hsl(var(--tm-border-subtle) / <alpha-value>)',
primary: 'hsl(var(--tm-primary) / <alpha-value>)',
'primary-hover': 'hsl(var(--tm-primary-hover) / <alpha-value>)',
'primary-foreground': 'hsl(var(--tm-primary-foreground) / <alpha-value>)',
secondary: 'hsl(var(--tm-secondary) / <alpha-value>)',
'secondary-foreground': 'hsl(var(--tm-secondary-foreground) / <alpha-value>)',
muted: 'hsl(var(--tm-muted) / <alpha-value>)',
'muted-foreground': 'hsl(var(--tm-muted-foreground) / <alpha-value>)',
'subtle-foreground': 'hsl(var(--tm-subtle-foreground) / <alpha-value>)',
accent: 'hsl(var(--tm-accent) / <alpha-value>)',
'accent-foreground': 'hsl(var(--tm-accent-foreground) / <alpha-value>)',
success: 'hsl(var(--tm-success) / <alpha-value>)',
warning: 'hsl(var(--tm-warning) / <alpha-value>)',
destructive: 'hsl(var(--tm-destructive) / <alpha-value>)',
'destructive-foreground': 'hsl(var(--tm-destructive-foreground) / <alpha-value>)',
'disabled-surface': 'hsl(var(--tm-disabled-surface) / <alpha-value>)',
'disabled-foreground': 'hsl(var(--tm-disabled-foreground) / <alpha-value>)',
'disabled-border': 'hsl(var(--tm-disabled-border) / <alpha-value>)',
ring: 'hsl(var(--tm-focus-ring) / <alpha-value>)'
},
height: {
'control-compact': 'var(--tm-control-height-compact)',
'control-standard': 'var(--tm-control-height-standard)',
'control-form': 'var(--tm-control-height-form)'
},
borderRadius: {
sm: 'var(--tm-radius-sm)',
md: 'var(--tm-radius-md)',
@@ -16,6 +16,32 @@ const baseProps = {
}
describe('export progress panel', () => {
it('uses the semantic accent surface for outgoing preview messages', () => {
render(
<ExportPreviewPanel
{...baseProps}
status="idle"
previewItems={[
{
id: 'outgoing-preview',
from: 'self',
type: '文字',
datetime: '',
content: '已发送的消息',
isSender: true
}
]}
progress={null}
includeVoiceTranscripts={false}
zip={false}
/>
)
expect(screen.getByText('已发送的消息').closest('.export-preview-bubble')).toHaveClass(
'bg-accent'
)
})
it('shows an indeterminate bar while the first message scan is still at zero', () => {
render(
<ExportPreviewPanel
+112
View File
@@ -0,0 +1,112 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it } from 'vitest'
import {
Button,
Checkbox,
Input,
RadioGroup,
RadioGroupItem,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Switch
} from '../../src/renderer/src/components/ui'
describe('TraceMemo UI control states', () => {
it('uses the compact, standard, and form control height contract', () => {
render(
<>
<Button size="sm"></Button>
<Button></Button>
<Button size="lg"></Button>
<Input aria-label="表单输入" />
</>
)
expect(screen.getByRole('button', { name: '紧凑' })).toHaveClass('h-control-compact')
expect(screen.getByRole('button', { name: '标准' })).toHaveClass('h-control-standard')
expect(screen.getByRole('button', { name: '表单' })).toHaveClass('h-control-form')
expect(screen.getByRole('textbox', { name: '表单输入' })).toHaveClass('h-control-form')
})
it('keeps disabled controls readable without the old opacity and surface shadow', () => {
render(
<>
<Button disabled></Button>
<Input aria-label="不可用输入" value="仍可读取" disabled readOnly />
<Checkbox aria-label="不可用多选" disabled />
<RadioGroup aria-label="不可用单选组">
<RadioGroupItem value="disabled" aria-label="不可用单选" disabled />
</RadioGroup>
<Switch aria-label="不可用开关" disabled />
</>
)
for (const control of [
screen.getByRole('button', { name: '不可用操作' }),
screen.getByRole('textbox', { name: '不可用输入' }),
screen.getByRole('checkbox', { name: '不可用多选' }),
screen.getByRole('radio', { name: '不可用单选' }),
screen.getByRole('switch', { name: '不可用开关' })
]) {
expect(control).toHaveClass('disabled:opacity-100')
expect(control).not.toHaveClass('shadow-surface')
}
expect(screen.getByRole('button', { name: '不可用操作' })).toHaveClass(
'disabled:bg-disabled-surface',
'disabled:text-disabled-foreground',
'disabled:!text-disabled-foreground'
)
expect(screen.getByRole('checkbox', { name: '不可用多选' })).toHaveClass(
'border-muted-foreground/60'
)
expect(screen.getByRole('radio', { name: '不可用单选' })).toHaveClass(
'border-muted-foreground/60'
)
})
it('keeps selection semantics while removing text glyph indicators', async () => {
const user = userEvent.setup()
render(
<>
<Select defaultValue="recent">
<SelectTrigger aria-label="时间范围">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="recent"> 30 </SelectItem>
<SelectItem value="all"></SelectItem>
</SelectContent>
</Select>
<Checkbox aria-label="包含图片" />
<RadioGroup aria-label="密度" defaultValue="compact">
<RadioGroupItem value="compact" aria-label="紧凑密度" />
<RadioGroupItem value="comfortable" aria-label="舒适密度" />
</RadioGroup>
<Switch aria-label="显示头像" />
</>
)
const trigger = screen.getByRole('combobox', { name: '时间范围' })
expect(trigger).not.toHaveTextContent('⌄')
await user.click(trigger)
expect(await screen.findByRole('option', { name: '最近 30 天' })).not.toHaveTextContent('✓')
await user.keyboard('{Escape}')
const checkbox = screen.getByRole('checkbox', { name: '包含图片' })
await user.click(checkbox)
expect(checkbox).toBeChecked()
expect(checkbox).not.toHaveTextContent('✓')
await user.click(screen.getByRole('radio', { name: '舒适密度' }))
expect(screen.getByRole('radio', { name: '舒适密度' })).toBeChecked()
const toggle = screen.getByRole('switch', { name: '显示头像' })
await user.click(toggle)
expect(toggle).toBeChecked()
})
})
+56
View File
@@ -649,6 +649,62 @@ test('EXPORT-01 multi-chat selection stays local to export and forces HTML', asy
}
})
test('EXPORT-02 large contact list stays bounded and searchable', async () => {
const fixture = await launchTestApp({ largeContacts: 1500 })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize({ width: 1400, height: 772 })
await fixture.page.getByRole('button', { name: '导出' }).click()
await expect(fixture.page.getByText('共 1,503 个')).toBeVisible()
const contactList = fixture.page.locator('.export-contact-list')
expect(await contactList.getByRole('button').count()).toBeLessThan(50)
const compositeButtons = [
fixture.page.getByRole('button', { name: /^全部导出/ }),
fixture.page.getByRole('button', { name: /HTML/ }),
fixture.page.locator('.export-workspace > aside:first-child > button:last-child')
]
for (const button of compositeButtons) {
await expect(button).toBeVisible()
expect(
await button.evaluate(
(element) => element.scrollHeight <= element.clientHeight && element.clientHeight > 32
)
).toBe(true)
}
const previewGeometryIsStable = await fixture.page
.locator('.export-workspace > aside:last-child')
.evaluate((previewPanel) => {
const scrollRegion = previewPanel.children.item(1)
const statistics = previewPanel.children.item(2)
if (!(scrollRegion instanceof HTMLElement) || !(statistics instanceof HTMLElement)) {
return false
}
const panelRect = previewPanel.getBoundingClientRect()
const scrollRect = scrollRegion.getBoundingClientRect()
const statisticsRect = statistics.getBoundingClientRect()
return (
scrollRect.bottom <= statisticsRect.top &&
statisticsRect.bottom <= panelRect.bottom &&
statistics.scrollHeight <= statistics.clientHeight
)
})
expect(previewGeometryIsStable).toBe(true)
await fixture.page.getByRole('textbox', { name: '搜索聊天' }).fill('性能样本 1499')
const target = contactList.getByRole('button', { name: /性能样本 1499/ })
await expect(target).toBeVisible()
await target.click()
await expect(target).toHaveAttribute('aria-pressed', 'true')
expect(pageErrors).toEqual([])
} finally {
await fixture.close()
}
})
test('LAYOUT-01 core workspaces fit a narrow desktop viewport without page errors', async () => {
const fixture = await launchTestApp()
const pageErrors: Error[] = []
+15 -3
View File
@@ -23,10 +23,16 @@ export async function launchTestApp(
aiFailure?: string
now?: number
appearanceTheme?: 'light' | 'dark'
stableUserData?: string
} = {}
): Promise<TestApplication> {
const ownsDirectory = !options.userData
const userData = options.userData || mkdtempSync(resolve(tmpdir(), 'wxe-e2e-'))
const ownsDirectory = !options.userData || Boolean(options.stableUserData)
const userData =
options.userData ||
(options.stableUserData
? resolve(options.stableUserData)
: mkdtempSync(resolve(tmpdir(), 'wxe-e2e-')))
if (options.stableUserData) rmSync(userData, { recursive: true, force: true })
const localTestEnv = loadEnv('test', process.cwd(), 'WXE_E2E_')
const configuredCloseDelay = Number(
process.env.WXE_E2E_CLOSE_DELAY_MS ?? localTestEnv.WXE_E2E_CLOSE_DELAY_MS
@@ -50,15 +56,21 @@ export async function launchTestApp(
const page = await app.firstWindow()
await page.waitForLoadState('domcontentloaded')
const setWindowContentSize = async (size: { width: number; height: number }): Promise<void> => {
await app.evaluate(({ BrowserWindow }, nextSize) => {
await app.evaluate(({ BrowserWindow, screen }, nextSize) => {
const [window] = BrowserWindow.getAllWindows()
if (!window) throw new Error('E2E BrowserWindow is unavailable')
const { workArea } = screen.getPrimaryDisplay()
window.setPosition(workArea.x + 40, workArea.y + 40)
window.setContentSize(nextSize.width, nextSize.height)
}, size)
await page.waitForFunction(
(nextSize) => window.innerWidth === nextSize.width && window.innerHeight === nextSize.height,
size
)
await page.evaluate(
() =>
new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
)
}
return {
app,
+62 -12
View File
@@ -4,7 +4,7 @@ import { resolve } from 'path'
import { launchTestApp } from './support/electron'
const baselineDirectory = resolve(`tests/e2e/__screenshots__/${process.platform}/visual.spec.ts`)
const visualViewport = { width: 1000, height: 650 }
const visualViewport = { width: 1400, height: 772 }
const visualNow = Date.parse('2026-08-19T14:46:40+08:00')
async function clearScreenshotFocus(page: import('@playwright/test').Page): Promise<void> {
@@ -200,6 +200,30 @@ test('API-00 Reader Skill page visual @visual', async () => {
}
})
test('API-00 Reader Skill page dark visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow, appearanceTheme: 'dark' })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
await fixture.setWindowContentSize(visualViewport)
await fixture.page.getByRole('button', { name: 'API' }).click()
await expect(fixture.page.getByRole('heading', { name: 'TraceMemo Reader' })).toBeVisible()
await expect(fixture.page.getByText('API Token', { exact: true })).toBeVisible()
await expect(fixture.page.locator('html')).toHaveAttribute('data-theme', 'dark')
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('api-center-page-dark.png', {
animations: 'disabled',
caret: 'hide'
})
} finally {
await fixture.close()
}
})
test('API-01 Reader Skill preview visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
const pageErrors: Error[] = []
@@ -259,8 +283,9 @@ test('API-02 Agent target segmented control visual @visual', async () => {
}
})
test('REPORT-00 report configuration controls visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow })
for (const appearanceTheme of ['light', 'dark'] as const) {
test(`REPORT-00 report configuration controls ${appearanceTheme} visual @visual`, async () => {
const fixture = await launchTestApp({ now: visualNow, appearanceTheme })
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
@@ -275,21 +300,30 @@ test('REPORT-00 report configuration controls visual @visual', async () => {
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('report-config-page.png', {
await expect(fixture.page).toHaveScreenshot(
`${appearanceTheme === 'light' ? 'report-config-page' : 'report-config-page-dark'}.png`,
{
animations: 'disabled',
caret: 'hide'
})
}
)
const modelHeading = fixture.page.getByRole('heading', { name: '模型配置' })
await modelHeading.scrollIntoViewIfNeeded()
await expect(modelHeading).toBeVisible()
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('report-model-controls.png', {
await expect(fixture.page).toHaveScreenshot(
`${appearanceTheme === 'light' ? 'report-model-controls' : 'report-model-controls-dark'}.png`,
{
animations: 'disabled',
caret: 'hide'
})
}
)
const templateSection = fixture.page.getByRole('heading', { name: '日报模板' }).locator('..')
if (appearanceTheme === 'light') {
const templateSection = fixture.page
.getByRole('heading', { name: '日报模板' })
.locator('..')
await templateSection.getByRole('button', { name: '查看版式' }).first().click()
const templateDialog = fixture.page.getByRole('dialog', { name: '经典日报' })
await expect(templateDialog).toBeVisible()
@@ -298,13 +332,21 @@ test('REPORT-00 report configuration controls visual @visual', async () => {
animations: 'disabled',
caret: 'hide'
})
}
} finally {
await fixture.close()
}
})
}
test('REPORT-01 report retry controls visual @visual', async () => {
const fixture = await launchTestApp({ now: visualNow, aiFailure: '401' })
for (const appearanceTheme of ['light', 'dark'] as const) {
test(`REPORT-01 report retry controls ${appearanceTheme} visual @visual`, async () => {
const fixture = await launchTestApp({
now: visualNow,
aiFailure: '401',
appearanceTheme,
stableUserData: '/tmp/tracememo-e2e-report-user-data'
})
const pageErrors: Error[] = []
fixture.page.on('pageerror', (error) => pageErrors.push(error))
try {
@@ -322,14 +364,18 @@ test('REPORT-01 report retry controls visual @visual', async () => {
).toBe(true)
expect(pageErrors).toEqual([])
await clearScreenshotFocus(fixture.page)
await expect(fixture.page).toHaveScreenshot('report-retry-controls.png', {
await expect(fixture.page).toHaveScreenshot(
`${appearanceTheme === 'light' ? 'report-retry-controls' : 'report-retry-controls-dark'}.png`,
{
animations: 'disabled',
caret: 'hide'
})
}
)
} finally {
await fixture.close()
}
})
}
for (const appearanceTheme of ['light', 'dark'] as const) {
test(`CHAT-02 personal WeChat send dialog ${appearanceTheme} visual @visual`, async () => {
@@ -568,6 +614,10 @@ for (const appearanceTheme of ['light', 'dark'] as const) {
await expect(fixture.page.getByRole('heading', { name: '新增供应商' })).toBeVisible()
await fixture.page.getByLabel('供应商 ID').fill('fixture-new-provider')
await expect(fixture.page.getByRole('combobox', { name: '快速模板' })).toBeVisible()
// Pin the screenshot after fill() auto-scroll to avoid half-pixel capture drift.
await fixture.page
.locator('.settings-page-scroll')
.evaluate((element) => (element.scrollTop = 396))
expect(
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
).toBe(true)