mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-07 15:08:41 +00:00
新增资源管理并修复了朋友圈的资源缓存路径
This commit is contained in:
620
src/pages/ResourcesPage.scss
Normal file
620
src/pages/ResourcesPage.scss
Normal file
@@ -0,0 +1,620 @@
|
||||
.resources-page.stream-rebuild {
|
||||
--stream-columns: 4;
|
||||
--stream-grid-gap: 12px;
|
||||
--stream-card-width: 272px;
|
||||
--stream-card-height: 356px;
|
||||
--stream-visual-height: 236px;
|
||||
--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;
|
||||
position: relative;
|
||||
background: var(--bg-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
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;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.media-tabs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
padding: 4px;
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--bg-secondary) 85%, transparent);
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary, #5f6674);
|
||||
border-radius: 9px;
|
||||
padding: 7px 14px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.filter-field {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
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;
|
||||
padding: 0 10px;
|
||||
min-height: 36px;
|
||||
box-sizing: border-box;
|
||||
|
||||
svg {
|
||||
color: var(--text-tertiary, #8a92a3);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.filter-date {
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: var(--text-primary, #1c2230);
|
||||
font-size: 13px;
|
||||
min-width: 0;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
font-family: "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.contact-select {
|
||||
width: 100%;
|
||||
min-width: 220px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
width: 128px;
|
||||
min-width: 128px;
|
||||
}
|
||||
|
||||
.sep {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
border: 1px solid var(--border-color);
|
||||
background: transparent;
|
||||
color: var(--text-secondary, #5f6674);
|
||||
border-radius: 10px;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
border-color: color-mix(in srgb, var(--border-color, #d2d7df) 95%, transparent);
|
||||
background: var(--bg-secondary, #f3f5f8);
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
|
||||
button {
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary, #f3f5f8);
|
||||
color: var(--text-secondary, #5f6674);
|
||||
border-radius: 10px;
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.58;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
border-color: color-mix(in srgb, var(--danger) 45%, var(--border-color));
|
||||
color: var(--danger);
|
||||
background: color-mix(in srgb, var(--danger) 10%, var(--bg-secondary));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stream-summary {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
padding: 0 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stream-state {
|
||||
height: 120px;
|
||||
border: 1px dashed var(--border-color);
|
||||
border-radius: 12px;
|
||||
background: var(--card-bg);
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
&.error {
|
||||
color: var(--danger);
|
||||
border-color: color-mix(in srgb, var(--danger) 45%, var(--border-color));
|
||||
}
|
||||
}
|
||||
|
||||
.stream-grid-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent);
|
||||
border-radius: 16px;
|
||||
background: color-mix(in srgb, var(--card-bg) 94%, transparent);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stream-grid {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
overflow-anchor: none;
|
||||
}
|
||||
|
||||
.stream-grid-list,
|
||||
.virtuoso-grid-list {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
align-content: flex-start;
|
||||
padding: 10px 0 2px;
|
||||
width: var(--stream-grid-width);
|
||||
min-width: var(--stream-grid-width);
|
||||
max-width: var(--stream-grid-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.stream-grid-item,
|
||||
.virtuoso-grid-item {
|
||||
box-sizing: border-box;
|
||||
width: var(--stream-slot-width);
|
||||
min-width: var(--stream-slot-width);
|
||||
max-width: var(--stream-slot-width);
|
||||
flex: 0 0 var(--stream-slot-width);
|
||||
height: var(--stream-slot-height);
|
||||
padding-right: var(--stream-grid-gap);
|
||||
padding-bottom: var(--stream-grid-gap);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.stream-grid-item > *,
|
||||
.virtuoso-grid-item > * {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.media-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.16s ease;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--primary) 34%, var(--border-color));
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: color-mix(in srgb, var(--primary) 56%, var(--border-color));
|
||||
outline: 1px solid color-mix(in srgb, var(--primary) 30%, transparent);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
&.decrypting {
|
||||
.card-visual {
|
||||
opacity: 0.68;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.floating-delete {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 4;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 9px;
|
||||
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);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transform: translateY(-2px) scale(0.96);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.16s ease, transform 0.16s ease;
|
||||
}
|
||||
|
||||
.media-card:hover .floating-delete,
|
||||
.media-card:focus-within .floating-delete {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.floating-update {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 4;
|
||||
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;
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
font-size: 11px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.card-visual {
|
||||
width: 100%;
|
||||
height: var(--stream-visual-height);
|
||||
min-height: var(--stream-visual-height);
|
||||
max-height: var(--stream-visual-height);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: color-mix(in srgb, var(--bg-tertiary) 70%, transparent);
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.image img,
|
||||
&.video img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.image img.long-image {
|
||||
object-fit: cover;
|
||||
object-position: top center;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: var(--text-tertiary);
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
max-width: 90%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.decrypting-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
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));
|
||||
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;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.decrypting-spinner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
padding: 9px 10px 8px;
|
||||
min-height: 66px;
|
||||
margin-top: auto;
|
||||
cursor: pointer;
|
||||
border-top: 1px solid color-mix(in srgb, var(--border-color) 70%, transparent);
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--bg-secondary) 68%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.title-row,
|
||||
.sub-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
.session {
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 62%;
|
||||
}
|
||||
.time {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.sub-row {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.grid-loading-more,
|
||||
.grid-end {
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: resources-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.action-message {
|
||||
color: color-mix(in srgb, var(--primary) 75%, var(--text-secondary));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.resource-dialog-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(8, 11, 18, 0.24);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.resource-dialog {
|
||||
width: min(420px, calc(100% - 32px));
|
||||
background: var(--card-bg, #ffffff);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 90%, transparent);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.22);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-secondary) 85%, transparent);
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 16px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
padding: 0 14px 14px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dialog-btn {
|
||||
min-width: 72px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
|
||||
&.ghost {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
&.solid {
|
||||
background: color-mix(in srgb, var(--primary) 16%, var(--bg-secondary));
|
||||
border-color: color-mix(in srgb, var(--primary) 45%, var(--border-color));
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.resources-page.stream-rebuild {
|
||||
.stream-toolbar {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.filters {
|
||||
.filter-select {
|
||||
min-width: 220px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.resources-page.stream-rebuild {
|
||||
--stream-grid-width: calc(var(--stream-slot-width) * var(--stream-columns));
|
||||
|
||||
.stream-grid-list,
|
||||
.virtuoso-grid-list {
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes resources-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes decrypt-sheen {
|
||||
from {
|
||||
transform: translateX(-45%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(45%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes decrypt-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes decrypt-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.92;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.68;
|
||||
}
|
||||
}
|
||||
1265
src/pages/ResourcesPage.tsx
Normal file
1265
src/pages/ResourcesPage.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1642,6 +1642,202 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sns-cache-migration-dialog {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--sns-border-radius-lg);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
|
||||
width: 540px;
|
||||
max-width: 92vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--border-color);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
animation: slide-up-fade 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.sns-cache-migration-close {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.sns-cache-migration-header {
|
||||
padding: 18px 20px 12px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.sns-cache-migration-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.sns-cache-migration-subtitle {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.sns-cache-migration-body {
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sns-cache-migration-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
|
||||
strong {
|
||||
font-size: 16px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.sns-cache-migration-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sns-cache-migration-progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--bg-tertiary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sns-cache-migration-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #34d399, #10b981);
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.sns-cache-migration-progress-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.sns-cache-migration-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.sns-cache-migration-item {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-tertiary);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.sns-cache-migration-item-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.sns-cache-migration-item-detail {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
word-break: break-all;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.sns-cache-migration-error,
|
||||
.sns-cache-migration-success {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.sns-cache-migration-error {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
color: var(--color-error, #f44336);
|
||||
}
|
||||
|
||||
.sns-cache-migration-success {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
color: var(--color-success, #4caf50);
|
||||
}
|
||||
|
||||
.sns-cache-migration-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
padding: 0 20px 18px;
|
||||
}
|
||||
|
||||
.sns-cache-migration-btn {
|
||||
min-width: 110px;
|
||||
height: 38px;
|
||||
border-radius: 9px;
|
||||
border: 1px solid transparent;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&.primary {
|
||||
background: var(--primary, #576b95);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.author-timeline-dialog {
|
||||
background: var(--sns-card-bg);
|
||||
border-radius: var(--sns-border-radius-lg);
|
||||
|
||||
@@ -66,6 +66,33 @@ type OverviewStatsStatus = 'loading' | 'ready' | 'error'
|
||||
type SnsExportScope = { kind: 'all' } | { kind: 'selected'; usernames: string[] }
|
||||
|
||||
const SIDEBAR_USER_PROFILE_CACHE_KEY = 'sidebar_user_profile_cache_v1'
|
||||
const SNS_CACHE_MIGRATION_PROMPT_SESSION_KEY = 'sns_cache_migration_prompted_v1'
|
||||
|
||||
interface SnsCacheMigrationItem {
|
||||
label: string
|
||||
sourceDir: string
|
||||
targetDir: string
|
||||
fileCount: number
|
||||
}
|
||||
|
||||
interface SnsCacheMigrationStatus {
|
||||
totalFiles: number
|
||||
legacyBaseDir?: string
|
||||
currentBaseDir?: string
|
||||
items: SnsCacheMigrationItem[]
|
||||
}
|
||||
|
||||
interface SnsCacheMigrationProgress {
|
||||
status: 'running' | 'done' | 'error'
|
||||
phase: 'copying' | 'cleanup' | 'done' | 'error'
|
||||
current: number
|
||||
total: number
|
||||
copied: number
|
||||
skipped: number
|
||||
remaining: number
|
||||
message?: string
|
||||
currentItemLabel?: string
|
||||
}
|
||||
|
||||
const readSidebarUserProfileCache = (): SidebarUserProfile | null => {
|
||||
try {
|
||||
@@ -162,6 +189,12 @@ export default function SnsPage() {
|
||||
const [triggerInstalled, setTriggerInstalled] = useState<boolean | null>(null)
|
||||
const [triggerLoading, setTriggerLoading] = useState(false)
|
||||
const [triggerMessage, setTriggerMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
||||
const [showCacheMigrationDialog, setShowCacheMigrationDialog] = useState(false)
|
||||
const [cacheMigrationStatus, setCacheMigrationStatus] = useState<SnsCacheMigrationStatus | null>(null)
|
||||
const [cacheMigrationProgress, setCacheMigrationProgress] = useState<SnsCacheMigrationProgress | null>(null)
|
||||
const [cacheMigrationRunning, setCacheMigrationRunning] = useState(false)
|
||||
const [cacheMigrationDone, setCacheMigrationDone] = useState(false)
|
||||
const [cacheMigrationError, setCacheMigrationError] = useState<string | null>(null)
|
||||
|
||||
const postsContainerRef = useRef<HTMLDivElement>(null)
|
||||
const jumpCalendarWrapRef = useRef<HTMLDivElement | null>(null)
|
||||
@@ -185,6 +218,7 @@ export default function SnsPage() {
|
||||
const contactsCountBatchTimerRef = useRef<number | null>(null)
|
||||
const jumpDateCountsCacheRef = useRef<Map<string, Record<string, number>>>(new Map())
|
||||
const jumpDateRequestSeqRef = useRef(0)
|
||||
const checkedCacheMigrationRef = useRef(false)
|
||||
|
||||
// Sync posts ref
|
||||
useEffect(() => {
|
||||
@@ -595,6 +629,133 @@ export default function SnsPage() {
|
||||
}
|
||||
}, [persistSnsPageCache])
|
||||
|
||||
const markCacheMigrationPrompted = useCallback(() => {
|
||||
try {
|
||||
window.sessionStorage.setItem(SNS_CACHE_MIGRATION_PROMPT_SESSION_KEY, '1')
|
||||
} catch {
|
||||
// ignore session storage failures
|
||||
}
|
||||
}, [])
|
||||
|
||||
const hasCacheMigrationPrompted = useCallback(() => {
|
||||
try {
|
||||
return window.sessionStorage.getItem(SNS_CACHE_MIGRATION_PROMPT_SESSION_KEY) === '1'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const checkCacheMigrationStatus = useCallback(async () => {
|
||||
if (checkedCacheMigrationRef.current) return
|
||||
checkedCacheMigrationRef.current = true
|
||||
if (hasCacheMigrationPrompted()) return
|
||||
try {
|
||||
const result = await window.electronAPI.sns.getCacheMigrationStatus()
|
||||
if (!result?.success || !result.needed) return
|
||||
const totalFiles = Math.max(0, Number(result.totalFiles || 0))
|
||||
const items = Array.isArray(result.items)
|
||||
? result.items.map((item) => ({
|
||||
label: String(item.label || '').trim(),
|
||||
sourceDir: String(item.sourceDir || '').trim(),
|
||||
targetDir: String(item.targetDir || '').trim(),
|
||||
fileCount: Math.max(0, Number(item.fileCount || 0))
|
||||
})).filter((item) => item.label && item.sourceDir && item.targetDir && item.fileCount > 0)
|
||||
: []
|
||||
if (totalFiles <= 0 || items.length === 0) return
|
||||
setCacheMigrationStatus({
|
||||
totalFiles,
|
||||
legacyBaseDir: result.legacyBaseDir,
|
||||
currentBaseDir: result.currentBaseDir,
|
||||
items
|
||||
})
|
||||
setCacheMigrationProgress(null)
|
||||
setCacheMigrationDone(false)
|
||||
setCacheMigrationError(null)
|
||||
setShowCacheMigrationDialog(true)
|
||||
markCacheMigrationPrompted()
|
||||
} catch (error) {
|
||||
console.error('Failed to check SNS cache migration status:', error)
|
||||
}
|
||||
}, [hasCacheMigrationPrompted, markCacheMigrationPrompted])
|
||||
|
||||
const startCacheMigration = useCallback(async () => {
|
||||
const total = Math.max(0, cacheMigrationStatus?.totalFiles || 0)
|
||||
setCacheMigrationError(null)
|
||||
setCacheMigrationDone(false)
|
||||
setCacheMigrationRunning(true)
|
||||
setCacheMigrationProgress({
|
||||
status: 'running',
|
||||
phase: 'copying',
|
||||
current: 0,
|
||||
total,
|
||||
copied: 0,
|
||||
skipped: 0,
|
||||
remaining: total,
|
||||
message: '准备迁移...'
|
||||
})
|
||||
|
||||
const removeProgress = window.electronAPI.sns.onCacheMigrationProgress((payload) => {
|
||||
if (!payload) return
|
||||
setCacheMigrationProgress({
|
||||
status: payload.status,
|
||||
phase: payload.phase,
|
||||
current: Math.max(0, Number(payload.current || 0)),
|
||||
total: Math.max(0, Number(payload.total || 0)),
|
||||
copied: Math.max(0, Number(payload.copied || 0)),
|
||||
skipped: Math.max(0, Number(payload.skipped || 0)),
|
||||
remaining: Math.max(0, Number(payload.remaining || 0)),
|
||||
message: payload.message,
|
||||
currentItemLabel: payload.currentItemLabel
|
||||
})
|
||||
if (payload.status === 'done') {
|
||||
setCacheMigrationDone(true)
|
||||
setCacheMigrationError(null)
|
||||
} else if (payload.status === 'error') {
|
||||
setCacheMigrationError(payload.message || '迁移失败')
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.sns.startCacheMigration()
|
||||
if (!result?.success) {
|
||||
setCacheMigrationError(result?.error || '迁移失败')
|
||||
} else {
|
||||
const totalFiles = Math.max(0, Number(result.totalFiles || 0))
|
||||
if (totalFiles === 0) {
|
||||
setCacheMigrationDone(true)
|
||||
setCacheMigrationProgress({
|
||||
status: 'done',
|
||||
phase: 'done',
|
||||
current: 0,
|
||||
total: 0,
|
||||
copied: 0,
|
||||
skipped: 0,
|
||||
remaining: 0,
|
||||
message: result.message || '无需迁移'
|
||||
})
|
||||
} else {
|
||||
// 兜底:若 done 事件因时序原因未到达,仍以返回结果收敛到完成态。
|
||||
setCacheMigrationDone(true)
|
||||
setCacheMigrationProgress((prev) => prev || {
|
||||
status: 'done',
|
||||
phase: 'done',
|
||||
current: totalFiles,
|
||||
total: totalFiles,
|
||||
copied: Math.max(0, Number(result.copied || 0)),
|
||||
skipped: Math.max(0, Number(result.skipped || 0)),
|
||||
remaining: 0,
|
||||
message: '迁移完成'
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setCacheMigrationError(String((error as Error)?.message || error || '迁移失败'))
|
||||
} finally {
|
||||
removeProgress()
|
||||
setCacheMigrationRunning(false)
|
||||
}
|
||||
}, [cacheMigrationStatus?.totalFiles])
|
||||
|
||||
const renderOverviewRangeText = () => {
|
||||
if (overviewStatsStatus === 'error') {
|
||||
return (
|
||||
@@ -1256,7 +1417,8 @@ export default function SnsPage() {
|
||||
void hydrateSnsPageCache()
|
||||
loadContacts()
|
||||
loadOverviewStats()
|
||||
}, [hydrateSnsPageCache, loadContacts, loadOverviewStats])
|
||||
void checkCacheMigrationStatus()
|
||||
}, [checkCacheMigrationStatus, hydrateSnsPageCache, loadContacts, loadOverviewStats])
|
||||
|
||||
useEffect(() => {
|
||||
const syncCurrentUserProfile = async () => {
|
||||
@@ -1659,6 +1821,117 @@ export default function SnsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showCacheMigrationDialog && cacheMigrationStatus && (
|
||||
<div
|
||||
className="modal-overlay"
|
||||
onClick={() => {
|
||||
if (cacheMigrationRunning) return
|
||||
setShowCacheMigrationDialog(false)
|
||||
}}
|
||||
>
|
||||
<div className="sns-cache-migration-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
className="close-btn sns-cache-migration-close"
|
||||
onClick={() => !cacheMigrationRunning && setShowCacheMigrationDialog(false)}
|
||||
disabled={cacheMigrationRunning}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
|
||||
<div className="sns-cache-migration-header">
|
||||
<div className="sns-cache-migration-title">发现旧版朋友圈缓存</div>
|
||||
<div className="sns-cache-migration-subtitle">
|
||||
建议迁移到当前缓存目录,避免目录分散和重复占用空间
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sns-cache-migration-body">
|
||||
<div className="sns-cache-migration-meta">
|
||||
<span>待处理文件</span>
|
||||
<strong>{cacheMigrationStatus.totalFiles}</strong>
|
||||
</div>
|
||||
|
||||
{cacheMigrationProgress && (
|
||||
<div className="sns-cache-migration-progress">
|
||||
<div className="sns-cache-migration-progress-bar">
|
||||
<div
|
||||
className="sns-cache-migration-progress-fill"
|
||||
style={{
|
||||
width: cacheMigrationProgress.total > 0
|
||||
? `${Math.min(100, Math.round((cacheMigrationProgress.current / cacheMigrationProgress.total) * 100))}%`
|
||||
: '100%'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="sns-cache-migration-progress-text">
|
||||
<span>{cacheMigrationProgress.message || '迁移中...'}</span>
|
||||
<span>
|
||||
已迁移 {cacheMigrationProgress.copied},剩余 {cacheMigrationProgress.remaining},跳过重复 {cacheMigrationProgress.skipped}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!cacheMigrationProgress && (
|
||||
<div className="sns-cache-migration-items">
|
||||
{cacheMigrationStatus.items.map((item, idx) => (
|
||||
<div className="sns-cache-migration-item" key={`${item.label}-${idx}`}>
|
||||
<div className="sns-cache-migration-item-title">{item.label}</div>
|
||||
<div className="sns-cache-migration-item-detail">
|
||||
{item.fileCount} 个文件 · {item.sourceDir} → {item.targetDir}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cacheMigrationError && (
|
||||
<div className="sns-cache-migration-error">
|
||||
<AlertCircle size={14} />
|
||||
<span>{cacheMigrationError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cacheMigrationDone && !cacheMigrationError && (
|
||||
<div className="sns-cache-migration-success">
|
||||
<CheckCircle size={14} />
|
||||
<span>迁移完成,旧目录已清理。</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="sns-cache-migration-actions">
|
||||
{!cacheMigrationDone ? (
|
||||
<>
|
||||
<button
|
||||
className="sns-cache-migration-btn secondary"
|
||||
onClick={() => setShowCacheMigrationDialog(false)}
|
||||
disabled={cacheMigrationRunning}
|
||||
>
|
||||
稍后再说
|
||||
</button>
|
||||
<button
|
||||
className="sns-cache-migration-btn primary"
|
||||
onClick={() => { void startCacheMigration() }}
|
||||
disabled={cacheMigrationRunning}
|
||||
>
|
||||
{cacheMigrationRunning ? '迁移中...' : '开始迁移'}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
className="sns-cache-migration-btn primary"
|
||||
onClick={() => setShowCacheMigrationDialog(false)}
|
||||
disabled={cacheMigrationRunning}
|
||||
>
|
||||
完成
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 朋友圈防删除插件对话框 */}
|
||||
{showTriggerDialog && (
|
||||
<div className="modal-overlay" onClick={() => { setShowTriggerDialog(false); setTriggerMessage(null) }}>
|
||||
|
||||
@@ -16,6 +16,7 @@ const isLinux = navigator.userAgent.toLowerCase().includes('linux')
|
||||
const isWindows = !isMac && !isLinux
|
||||
|
||||
const dbDirName = isMac ? '2.0b4.0.9 目录' : 'xwechat_files 目录'
|
||||
const DB_PATH_CHINESE_ERROR = '路径包含中文字符,迁移至全英文目录后再试'
|
||||
const dbPathPlaceholder = isMac
|
||||
? '例如: ~/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9'
|
||||
: isLinux
|
||||
@@ -221,10 +222,23 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
if (!path) return null
|
||||
// 检测中文字符和其他可能有问题的特殊字符
|
||||
if (/[\u4e00-\u9fa5]/.test(path)) {
|
||||
return '路径包含中文字符,请迁移至全英文目录'
|
||||
return DB_PATH_CHINESE_ERROR
|
||||
}
|
||||
return null
|
||||
}
|
||||
const dbPathValidationError = validatePath(dbPath)
|
||||
|
||||
const handleDbPathChange = (value: string) => {
|
||||
setDbPath(value)
|
||||
const validationError = validatePath(value)
|
||||
if (validationError) {
|
||||
setError(validationError)
|
||||
return
|
||||
}
|
||||
if (error === DB_PATH_CHINESE_ERROR) {
|
||||
setError('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectPath = async () => {
|
||||
try {
|
||||
@@ -236,10 +250,10 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
if (!result.canceled && result.filePaths.length > 0) {
|
||||
const selectedPath = result.filePaths[0]
|
||||
const validationError = validatePath(selectedPath)
|
||||
setDbPath(selectedPath)
|
||||
if (validationError) {
|
||||
setError(validationError)
|
||||
} else {
|
||||
setDbPath(selectedPath)
|
||||
setError('')
|
||||
}
|
||||
}
|
||||
@@ -256,10 +270,10 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
const result = await window.electronAPI.dbPath.autoDetect()
|
||||
if (result.success && result.path) {
|
||||
const validationError = validatePath(result.path)
|
||||
setDbPath(result.path)
|
||||
if (validationError) {
|
||||
setError(validationError)
|
||||
} else {
|
||||
setDbPath(result.path)
|
||||
setError('')
|
||||
}
|
||||
} else {
|
||||
@@ -426,7 +440,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
|
||||
const canGoNext = () => {
|
||||
if (currentStep.id === 'intro') return true
|
||||
if (currentStep.id === 'db') return Boolean(dbPath)
|
||||
if (currentStep.id === 'db') return Boolean(dbPath) && !dbPathValidationError
|
||||
if (currentStep.id === 'cache') return true
|
||||
if (currentStep.id === 'key') return decryptKey.length === 64 && Boolean(wxid)
|
||||
if (currentStep.id === 'image') return true
|
||||
@@ -442,6 +456,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
const handleNext = () => {
|
||||
if (!canGoNext()) {
|
||||
if (currentStep.id === 'db' && !dbPath) setError('请先选择数据库目录')
|
||||
else if (currentStep.id === 'db' && dbPathValidationError) setError(dbPathValidationError)
|
||||
if (currentStep.id === 'key') {
|
||||
if (decryptKey.length !== 64) setError('密钥长度必须为 64 个字符')
|
||||
else if (!wxid) setError('未能自动识别 wxid,请尝试重新获取或检查目录')
|
||||
@@ -664,7 +679,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
className="field-input"
|
||||
placeholder={dbPathPlaceholder}
|
||||
value={dbPath}
|
||||
onChange={(e) => setDbPath(e.target.value)}
|
||||
onChange={(e) => handleDbPathChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="action-row">
|
||||
|
||||
Reference in New Issue
Block a user