mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-20 23:03:44 +08:00
feat: implement SMS management features including frontend UI and backend API handlers
This commit is contained in:
+121
-139
@@ -314,7 +314,7 @@ export default function SmsPage() {
|
||||
const selectContact = useCallback(
|
||||
async (key: string, opts: { syncRoute?: boolean; silent?: boolean; scrollToBottom?: boolean } = {}) => {
|
||||
const { syncRoute = true, silent = false, scrollToBottom = true } = opts;
|
||||
if (!key || (keyRef.current === key && messagesRef.current.length > 0)) return;
|
||||
if (!key) return;
|
||||
setKey(key);
|
||||
if (syncRoute) syncQuery(deviceRef.current, key);
|
||||
const thread = contactsRef.current.find((t) => t.key === key) || null;
|
||||
@@ -338,8 +338,8 @@ export default function SmsPage() {
|
||||
contactsList: SmsThread[],
|
||||
opts: { syncRoute?: boolean; silent?: boolean; scrollToBottom?: boolean } = {},
|
||||
) => {
|
||||
const { syncRoute = false, silent = false, scrollToBottom = false } = opts;
|
||||
const active = contactsList.find((t) => t.key === keyRef.current) || null;
|
||||
const { silent = false, scrollToBottom = false } = opts;
|
||||
const active = (keyRef.current && contactsList.find((t) => t.key === keyRef.current)) || null;
|
||||
if (active) {
|
||||
const ok = await loadThreadFor(active, device, silent);
|
||||
if (ok) {
|
||||
@@ -350,16 +350,8 @@ export default function SmsPage() {
|
||||
}
|
||||
setMessagesState([]);
|
||||
setHasMoreState(false);
|
||||
if (keyRef.current) {
|
||||
setKey("");
|
||||
if (syncRoute) syncQuery(device, "");
|
||||
}
|
||||
const filtered = filterThreads(contactsList, searchRef.current);
|
||||
if (!isMobileRef.current && filtered.length > 0) {
|
||||
await selectContact(filtered[0].key, { syncRoute, silent, scrollToBottom });
|
||||
}
|
||||
},
|
||||
[loadThreadFor, selectContact, syncQuery, scrollToBottomNow],
|
||||
[loadThreadFor, scrollToBottomNow],
|
||||
);
|
||||
|
||||
const clearSelection = useCallback(
|
||||
@@ -479,7 +471,7 @@ export default function SmsPage() {
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
}, [composer, devices, refreshCurrent, scrollToBottomNow]);
|
||||
}, [composer, devices, refreshCurrent, scrollToBottomNow, t]);
|
||||
|
||||
const openNewSms = useCallback(() => {
|
||||
setNewSmsDevice(deviceRef.current !== "all" ? deviceRef.current : devices[0]?.id || "");
|
||||
@@ -506,7 +498,7 @@ export default function SmsPage() {
|
||||
setSending(false);
|
||||
}
|
||||
},
|
||||
[refreshCurrent],
|
||||
[refreshCurrent, t],
|
||||
);
|
||||
|
||||
const deleteMessageAction = useCallback(
|
||||
@@ -530,7 +522,7 @@ export default function SmsPage() {
|
||||
setDeletingMessageId(null);
|
||||
}
|
||||
},
|
||||
[deletingMessageId, refreshCurrent, clearSelection],
|
||||
[deletingMessageId, refreshCurrent, clearSelection, t],
|
||||
);
|
||||
|
||||
const deleteThreadAction = useCallback(
|
||||
@@ -560,7 +552,7 @@ export default function SmsPage() {
|
||||
setDeletingThreadKey(null);
|
||||
}
|
||||
},
|
||||
[deletingThreadKey, clearSelection, loadContacts],
|
||||
[deletingThreadKey, clearSelection, loadContacts, lang],
|
||||
);
|
||||
|
||||
const closeActionSheet = useCallback(() => {
|
||||
@@ -622,16 +614,6 @@ export default function SmsPage() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const prevIsMobile = useRef(isMobile);
|
||||
useEffect(() => {
|
||||
const was = prevIsMobile.current;
|
||||
prevIsMobile.current = isMobile;
|
||||
if (was && !isMobile && !keyRef.current) {
|
||||
const filtered = filterThreads(contactsRef.current, searchRef.current);
|
||||
if (filtered.length > 0) void selectContact(filtered[0].key, { syncRoute: true, scrollToBottom: false });
|
||||
}
|
||||
}, [isMobile, selectContact]);
|
||||
|
||||
useEffect(() => () => clearLongPress(), [clearLongPress]);
|
||||
|
||||
return (
|
||||
@@ -670,121 +652,121 @@ export default function SmsPage() {
|
||||
onRetry={refreshAll}
|
||||
/>
|
||||
) : null}
|
||||
<div className="relative flex-1 overflow-hidden ui-card">
|
||||
{contactsLoading && contacts.length === 0 ? (
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-white/50 backdrop-blur-sm dark:bg-black/20">
|
||||
<Spinner className="h-7 w-7 text-[#0ea5e9]" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="sms-main-layout">
|
||||
{isDesktop ? (
|
||||
<div className="flex flex-col border-r border-gray-100 dark:border-white/10">
|
||||
<div className="border-b border-gray-100 p-4 dark:border-white/10">
|
||||
<div className="text-xs font-bold uppercase tracking-wider text-gray-500">{t("设备")}</div>
|
||||
</div>
|
||||
<div className="space-y-1 overflow-auto p-3">
|
||||
{deviceFilters.map((d) => (
|
||||
<button
|
||||
key={d.id}
|
||||
type="button"
|
||||
onClick={() => void selectDevice(d.id)}
|
||||
className={cx(
|
||||
"flex w-full items-center justify-between gap-3 rounded-xl border px-3 py-2 text-left transition-all",
|
||||
selectedDevice === d.id
|
||||
? "border-indigo-200 bg-indigo-50/70 dark:border-indigo-500/30 dark:bg-indigo-500/10"
|
||||
: "border-transparent hover:bg-gray-50/60 dark:hover:bg-white/5",
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-bold text-gray-800 dark:text-gray-100">{d.label}</div>
|
||||
<div className="truncate text-xs text-gray-400">{d.id === "all" ? t("汇总全部设备检测记录") : d.id}</div>
|
||||
<div className="relative flex-1 overflow-hidden ui-card">
|
||||
{contactsLoading && contacts.length === 0 ? (
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-white/50 backdrop-blur-sm dark:bg-black/20">
|
||||
<Spinner className="h-7 w-7 text-[#0ea5e9]" />
|
||||
</div>
|
||||
{d.id !== "all" ? (
|
||||
<span className={cx("h-2 w-2 rounded-full", d.healthy ? "bg-green-500" : "bg-red-500")} />
|
||||
) : null}
|
||||
<div className="sms-main-layout">
|
||||
{isDesktop ? (
|
||||
<div className="flex flex-col border-r border-gray-100 dark:border-white/10">
|
||||
<div className="border-b border-gray-100 p-4 dark:border-white/10">
|
||||
<div className="text-xs font-bold uppercase tracking-wider text-gray-500">{t("设备")}</div>
|
||||
</div>
|
||||
<div className="space-y-1 overflow-auto p-3">
|
||||
{deviceFilters.map((d) => (
|
||||
<button
|
||||
key={d.id}
|
||||
type="button"
|
||||
onClick={() => void selectDevice(d.id)}
|
||||
className={cx(
|
||||
"flex w-full items-center justify-between gap-3 rounded-xl border px-3 py-2 text-left transition-all",
|
||||
selectedDevice === d.id
|
||||
? "border-indigo-200 bg-indigo-50/70 dark:border-indigo-500/30 dark:bg-indigo-500/10"
|
||||
: "border-transparent hover:bg-gray-50/60 dark:hover:bg-white/5",
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-bold text-gray-800 dark:text-gray-100">{d.label}</div>
|
||||
<div className="truncate text-xs text-gray-400">{d.id === "all" ? t("汇总全部设备检测记录") : d.id}</div>
|
||||
</div>
|
||||
{d.id !== "all" ? (
|
||||
<span className={cx("h-2 w-2 rounded-full", d.healthy ? "bg-green-500" : "bg-red-500")} />
|
||||
) : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{showContactColumn ? (
|
||||
<ContactList
|
||||
isMobile={isMobile}
|
||||
isDesktop={isDesktop}
|
||||
selectedDevice={selectedDevice}
|
||||
deviceOptions={deviceSelectOptions}
|
||||
onSelectDevice={(id) => void selectDevice(id)}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearch}
|
||||
loading={contactsLoading}
|
||||
contacts={filteredContacts}
|
||||
activeKey={selectedKey}
|
||||
isUnread={isUnread}
|
||||
deletingKey={deletingThreadKey}
|
||||
canHover={canHover}
|
||||
onSelect={(key) => void selectContact(key)}
|
||||
onDelete={(t) => void deleteThreadAction(t)}
|
||||
onRowPointerDown={onThreadPointerDown}
|
||||
onRowPointerMove={moveLongPress}
|
||||
onRowPointerEnd={clearLongPress}
|
||||
/>
|
||||
) : null}
|
||||
{showDetailColumn ? (
|
||||
<ThreadPanel
|
||||
isMobile={isMobile}
|
||||
isDesktop={isDesktop}
|
||||
selectedDevice={selectedDevice}
|
||||
activeThread={activeThread}
|
||||
canLoadMore={!!activeThread && hasMore}
|
||||
loadingMore={loadingMore}
|
||||
groups={groups}
|
||||
deletingMessageId={deletingMessageId}
|
||||
canHover={canHover}
|
||||
composer={composer}
|
||||
composerInfo={composerInfo}
|
||||
composerLength={composerLength}
|
||||
sending={sending}
|
||||
detailRef={detailRef}
|
||||
composerRef={composerRef}
|
||||
onBack={onBack}
|
||||
onScrollToBottom={scrollToBottomNow}
|
||||
onLoadMore={() => void loadMore()}
|
||||
onDeleteMessage={(m) => void deleteMessageAction(m)}
|
||||
onComposerChange={setComposer}
|
||||
onSend={() => void sendReply()}
|
||||
onDetailScroll={onDetailScroll}
|
||||
onMsgPointerDown={onMsgPointerDown}
|
||||
onMsgPointerMove={moveLongPress}
|
||||
onMsgPointerEnd={clearLongPress}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{actionSheetOpen && isMobile && actionTarget ? (
|
||||
<div className="sms-action-sheet-mask animate-[fade-slide-in_0.18s_ease]" onClick={closeActionSheet}>
|
||||
<div className="sms-action-sheet" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="sms-action-sheet-title">{t("操作")}</div>
|
||||
<Button
|
||||
className="sms-danger-ghost-btn !w-full !justify-center"
|
||||
icon={<DeleteRegular />}
|
||||
onClick={() => void confirmSheetAction()}
|
||||
>
|
||||
{actionTarget.type === "thread" ? t("删除对话") : t("删除短信")}
|
||||
</Button>
|
||||
<Button className="!w-full !justify-center" onClick={closeActionSheet}>
|
||||
{t("取消")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<NewSmsModal
|
||||
open={newSmsOpen}
|
||||
devices={devices}
|
||||
defaultDeviceId={newSmsDevice}
|
||||
sending={sending}
|
||||
onClose={() => setNewSmsOpen(false)}
|
||||
onSend={sendNewSms}
|
||||
/>
|
||||
{showContactColumn ? (
|
||||
<ContactList
|
||||
isMobile={isMobile}
|
||||
isDesktop={isDesktop}
|
||||
selectedDevice={selectedDevice}
|
||||
deviceOptions={deviceSelectOptions}
|
||||
onSelectDevice={(id) => void selectDevice(id)}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearch}
|
||||
loading={contactsLoading}
|
||||
contacts={filteredContacts}
|
||||
activeKey={selectedKey}
|
||||
isUnread={isUnread}
|
||||
deletingKey={deletingThreadKey}
|
||||
canHover={canHover}
|
||||
onSelect={(key) => void selectContact(key)}
|
||||
onDelete={(t) => void deleteThreadAction(t)}
|
||||
onRowPointerDown={onThreadPointerDown}
|
||||
onRowPointerMove={moveLongPress}
|
||||
onRowPointerEnd={clearLongPress}
|
||||
/>
|
||||
) : null}
|
||||
{showDetailColumn ? (
|
||||
<ThreadPanel
|
||||
isMobile={isMobile}
|
||||
isDesktop={isDesktop}
|
||||
selectedDevice={selectedDevice}
|
||||
activeThread={activeThread}
|
||||
canLoadMore={!!activeThread && hasMore}
|
||||
loadingMore={loadingMore}
|
||||
groups={groups}
|
||||
deletingMessageId={deletingMessageId}
|
||||
canHover={canHover}
|
||||
composer={composer}
|
||||
composerInfo={composerInfo}
|
||||
composerLength={composerLength}
|
||||
sending={sending}
|
||||
detailRef={detailRef}
|
||||
composerRef={composerRef}
|
||||
onBack={onBack}
|
||||
onScrollToBottom={scrollToBottomNow}
|
||||
onLoadMore={() => void loadMore()}
|
||||
onDeleteMessage={(m) => void deleteMessageAction(m)}
|
||||
onComposerChange={setComposer}
|
||||
onSend={() => void sendReply()}
|
||||
onDetailScroll={onDetailScroll}
|
||||
onMsgPointerDown={onMsgPointerDown}
|
||||
onMsgPointerMove={moveLongPress}
|
||||
onMsgPointerEnd={clearLongPress}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{actionSheetOpen && isMobile && actionTarget ? (
|
||||
<div className="sms-action-sheet-mask animate-[fade-slide-in_0.18s_ease]" onClick={closeActionSheet}>
|
||||
<div className="sms-action-sheet" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="sms-action-sheet-title">{t("操作")}</div>
|
||||
<Button
|
||||
className="sms-danger-ghost-btn !w-full !justify-center"
|
||||
icon={<DeleteRegular />}
|
||||
onClick={() => void confirmSheetAction()}
|
||||
>
|
||||
{actionTarget.type === "thread" ? t("删除对话") : t("删除短信")}
|
||||
</Button>
|
||||
<Button className="!w-full !justify-center" onClick={closeActionSheet}>
|
||||
{t("取消")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<NewSmsModal
|
||||
open={newSmsOpen}
|
||||
devices={devices}
|
||||
defaultDeviceId={newSmsDevice}
|
||||
sending={sending}
|
||||
onClose={() => setNewSmsOpen(false)}
|
||||
onSend={sendNewSms}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user