更新設備選擇組件,新增設備頭像和好友數據顯示,調整樣式以提升用戶體驗,並更新相關數據結構以支持新屬性。

This commit is contained in:
超级老白兔
2025-08-13 12:18:43 +08:00
parent 4e78eb6c5e
commit e2dfd44874
11 changed files with 489 additions and 119 deletions

View File

@@ -33,7 +33,7 @@
"name": "vendor" "name": "vendor"
}, },
"index.html": { "index.html": {
"file": "assets/index-D3HSx5Yt.js", "file": "assets/index-DTZ_ow5W.js",
"name": "index", "name": "index",
"src": "index.html", "src": "index.html",
"isEntry": true, "isEntry": true,

View File

@@ -11,7 +11,7 @@
</style> </style>
<!-- 引入 uni-app web-view SDK必须 --> <!-- 引入 uni-app web-view SDK必须 -->
<script type="text/javascript" src="./websdk.js"></script> <script type="text/javascript" src="./websdk.js"></script>
<script type="module" crossorigin src="/assets/index-D3HSx5Yt.js"></script> <script type="module" crossorigin src="/assets/index-DTZ_ow5W.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vendor-2vc8h_ct.js"> <link rel="modulepreload" crossorigin href="/assets/vendor-2vc8h_ct.js">
<link rel="modulepreload" crossorigin href="/assets/ui-D1w-jetn.js"> <link rel="modulepreload" crossorigin href="/assets/ui-D1w-jetn.js">
<link rel="modulepreload" crossorigin href="/assets/utils-6WF66_dS.js"> <link rel="modulepreload" crossorigin href="/assets/utils-6WF66_dS.js">

View File

@@ -8,6 +8,8 @@ export interface DeviceSelectionItem {
wxid?: string; wxid?: string;
nickname?: string; nickname?: string;
usedInPlans?: number; usedInPlans?: number;
avatar?: string;
totalFriend?: number;
} }
// 组件属性接口 // 组件属性接口

View File

@@ -67,60 +67,154 @@
} }
.deviceItem { .deviceItem {
display: flex; display: flex;
align-items: flex-start; flex-direction: column;
gap: 12px; gap: 8px;
padding: 16px; padding: 12px;
border-radius: 12px;
border: 1px solid #f0f0f0;
background: #fff; background: #fff;
cursor: pointer; border-radius: 12px;
transition: background 0.2s; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;
border: 1px solid #f5f5f5;
&:hover { &:hover {
background: #f5f6fa; transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
}
.headerRow {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.checkboxContainer {
flex-shrink: 0;
}
.imeiText {
font-size: 13px;
color: #666;
font-family: monospace;
flex: 1;
}
.mainContent {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
padding: 8px;
border-radius: 8px;
transition: background-color 0.2s ease;
&:hover {
background-color: #f8f9fa;
} }
} }
.deviceCheckbox { .deviceCheckbox {
margin-top: 4px; flex-shrink: 0;
} }
.deviceInfo { .deviceInfo {
flex: 1; flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 12px;
} }
.deviceAvatar {
width: 64px;
height: 64px;
border-radius: 6px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatarText {
font-size: 18px;
color: #fff;
font-weight: 700;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
}
.deviceContent {
flex: 1;
min-width: 0;
}
.deviceInfoRow { .deviceInfoRow {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: 6px;
margin-bottom: 6px;
} }
.deviceName { .deviceName {
font-weight: 500;
font-size: 16px; font-size: 16px;
color: #222; font-weight: 600;
color: #1a1a1a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.statusOnline { .statusOnline {
width: 56px; font-size: 11px;
height: 24px; padding: 1px 6px;
border-radius: 12px; border-radius: 8px;
background: #52c41a; color: #52c41a;
color: #fff; background: #f6ffed;
font-size: 13px; border: 1px solid #b7eb8f;
display: flex; font-weight: 500;
align-items: center;
justify-content: center;
} }
.statusOffline { .statusOffline {
width: 56px; font-size: 11px;
height: 24px; padding: 1px 6px;
border-radius: 12px; border-radius: 8px;
background: #e5e6eb; color: #ff4d4f;
color: #888; background: #fff2f0;
font-size: 13px; border: 1px solid #ffccc7;
display: flex; font-weight: 500;
align-items: center;
justify-content: center;
} }
.deviceInfoDetail { .deviceInfoDetail {
display: flex;
flex-direction: column;
gap: 4px;
}
.infoItem {
display: flex;
align-items: center;
gap: 8px;
}
.infoLabel {
font-size: 13px; font-size: 13px;
color: #888; color: #666;
margin-top: 4px; min-width: 50px;
}
.infoValue {
font-size: 13px;
color: #333;
&.imei {
font-family: monospace;
}
&.friendCount {
font-weight: 500;
}
} }
.loadingBox { .loadingBox {
display: flex; display: flex;

View File

@@ -86,11 +86,52 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
padding: "4px 8px", padding: "8px 12px",
borderBottom: "1px solid #f0f0f0", borderBottom: "1px solid #f0f0f0",
fontSize: 14, fontSize: 14,
}} }}
> >
{/* 头像 */}
<div
style={{
width: 40,
height: 40,
borderRadius: "6px",
background:
"linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.25)",
marginRight: "12px",
flexShrink: 0,
}}
>
{device.avatar ? (
<img
src={device.avatar}
alt="头像"
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
) : (
<span
style={{
fontSize: 16,
color: "#fff",
fontWeight: 700,
textShadow: "0 1px 3px rgba(0,0,0,0.3)",
}}
>
{(device.memo || device.wechatId || "设")[0]}
</span>
)}
</div>
<div <div
style={{ style={{
flex: 1, flex: 1,
@@ -100,7 +141,7 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
textOverflow: "ellipsis", textOverflow: "ellipsis",
}} }}
> >
{device.memo} - {device.wechatId} {device.memo} - {device.wechatId}
</div> </div>
{!readonly && ( {!readonly && (
<Button <Button

View File

@@ -51,6 +51,8 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
wxid: d.wechatId || "", wxid: d.wechatId || "",
nickname: d.nickname || "", nickname: d.nickname || "",
usedInPlans: d.usedInPlans || 0, usedInPlans: d.usedInPlans || 0,
avatar: d.avatar || "",
totalFriend: d.totalFriend || 0,
})), })),
); );
setTotal(res.total || 0); setTotal(res.total || 0);
@@ -161,13 +163,36 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
) : ( ) : (
<div className={style.deviceListInner}> <div className={style.deviceListInner}>
{filteredDevices.map(device => ( {filteredDevices.map(device => (
<label key={device.id} className={style.deviceItem}> <div key={device.id} className={style.deviceItem}>
{/* 顶部行选择框和IMEI */}
<div className={style.headerRow}>
<div className={style.checkboxContainer}>
<Checkbox <Checkbox
checked={selectedOptions.some(v => v.id === device.id)} checked={selectedOptions.some(v => v.id === device.id)}
onChange={() => handleDeviceToggle(device)} onChange={() => handleDeviceToggle(device)}
className={style.deviceCheckbox} className={style.deviceCheckbox}
/> />
<div className={style.deviceInfo}> </div>
<span className={style.imeiText}>
IMEI: {device.imei?.toUpperCase()}
</span>
</div>
{/* 主要内容区域:头像和详细信息 */}
<div className={style.mainContent}>
{/* 头像 */}
<div className={style.deviceAvatar}>
{device.avatar ? (
<img src={device.avatar} alt="头像" />
) : (
<span className={style.avatarText}>
{(device.memo || device.wechatId || "设")[0]}
</span>
)}
</div>
{/* 设备信息 */}
<div className={style.deviceContent}>
<div className={style.deviceInfoRow}> <div className={style.deviceInfoRow}>
<span className={style.deviceName}>{device.memo}</span> <span className={style.deviceName}>{device.memo}</span>
<div <div
@@ -181,11 +206,24 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
</div> </div>
</div> </div>
<div className={style.deviceInfoDetail}> <div className={style.deviceInfoDetail}>
<div>IMEI: {device.imei}</div> <div className={style.infoItem}>
<div>: {device.wechatId}</div> <span className={style.infoLabel}>:</span>
<span className={style.infoValue}>
{device.wechatId}
</span>
</div>
<div className={style.infoItem}>
<span className={style.infoLabel}>:</span>
<span
className={`${style.infoValue} ${style.friendCount}`}
>
{device.totalFriend ?? "-"}
</span>
</div>
</div>
</div>
</div> </div>
</div> </div>
</label>
))} ))}
</div> </div>
)} )}

View File

@@ -62,9 +62,7 @@ export default function ContentForm() {
setSelectedFriends(data.sourceFriends || []); setSelectedFriends(data.sourceFriends || []);
setSelectedGroups(data.selectedGroups || []); setSelectedGroups(data.selectedGroups || []);
setSelectedGroupsOptions(data.selectedGroupsOptions || []); setSelectedGroupsOptions(data.selectedGroupsOptions || []);
setSelectedFriendsOptions(data.friendsGroupsOptions || []);
setSelectedFriendsOptions(data.sourceFriendsOptions || []);
setKeywordsInclude((data.keywordInclude || []).join(",")); setKeywordsInclude((data.keywordInclude || []).join(","));
setKeywordsExclude((data.keywordExclude || []).join(",")); setKeywordsExclude((data.keywordExclude || []).join(","));
setAIPrompt(data.aiPrompt || ""); setAIPrompt(data.aiPrompt || "");

View File

@@ -132,7 +132,7 @@ const ContentLibraryList: React.FC = () => {
}; };
const handleEdit = (id: string) => { const handleEdit = (id: string) => {
navigate(`/content/edit/${id}`); navigate(`/mine/content/edit/${id}`);
}; };
const handleDelete = async (id: string) => { const handleDelete = async (id: string) => {

View File

@@ -0,0 +1,173 @@
.deviceList {
display: flex;
flex-direction: column;
gap: 12px;
}
.deviceCard {
background: #fff;
border-radius: 12px;
padding: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;
position: relative;
overflow: hidden;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
&.selected {
border: 2px solid #1677ff;
}
&:not(.selected) {
border: 1px solid #f5f5f5;
}
}
.headerRow {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.checkboxContainer {
flex-shrink: 0;
}
.imeiText {
font-size: 13px;
color: #666;
font-family: monospace;
flex: 1;
}
.mainContent {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
border-radius: 8px;
transition: background-color 0.2s ease;
&:hover {
background-color: #f8f9fa;
}
}
.avatar {
width: 64px;
height: 64px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
flex-shrink: 0;
border-radius: 6px;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatarText {
font-size: 18px;
color: #fff;
font-weight: 700;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
}
.deviceInfo {
flex: 1;
min-width: 0;
}
.deviceHeader {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.deviceName {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.statusBadge {
font-size: 11px;
padding: 1px 6px;
border-radius: 8px;
font-weight: 500;
&.online {
color: #52c41a;
background: #f6ffed;
border: 1px solid #b7eb8f;
}
&.offline {
color: #ff4d4f;
background: #fff2f0;
border: 1px solid #ffccc7;
}
}
.infoList {
display: flex;
flex-direction: column;
gap: 4px;
}
.infoItem {
display: flex;
align-items: center;
gap: 8px;
}
.infoLabel {
font-size: 13px;
color: #666;
min-width: 50px;
}
.infoValue {
font-size: 13px;
color: #333;
&.friendCount {
font-weight: 500;
}
}
.arrowIcon {
color: #999;
font-size: 14px;
margin-left: auto;
transition: transform 0.2s ease;
}
.mainContent:hover .arrowIcon {
transform: translateX(3px);
color: #1677ff;
}
.paginationContainer {
padding: 16px;
background: #fff;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: center;
}

View File

@@ -7,6 +7,7 @@ import {
ReloadOutlined, ReloadOutlined,
SearchOutlined, SearchOutlined,
QrcodeOutlined, QrcodeOutlined,
RightOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout"; import Layout from "@/components/Layout/Layout";
import { import {
@@ -19,6 +20,7 @@ import type { Device } from "@/types/device";
import { comfirm } from "@/utils/common"; import { comfirm } from "@/utils/common";
import { useUserStore } from "@/store/module/user"; import { useUserStore } from "@/store/module/user";
import NavCommon from "@/components/NavCommon"; import NavCommon from "@/components/NavCommon";
import styles from "./index.module.scss";
const Devices: React.FC = () => { const Devices: React.FC = () => {
// 设备列表相关 // 设备列表相关
@@ -250,7 +252,7 @@ const Devices: React.FC = () => {
</> </>
} }
footer={ footer={
<div className="pagination-container"> <div className={styles.paginationContainer}>
<Pagination <Pagination
current={page} current={page}
pageSize={20} pageSize={20}
@@ -264,24 +266,12 @@ const Devices: React.FC = () => {
> >
<div style={{ padding: 12 }}> <div style={{ padding: 12 }}>
{/* 设备列表 */} {/* 设备列表 */}
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}> <div className={styles.deviceList}>
{filtered.map(device => ( {filtered.map(device => (
<div <div key={device.id} className={styles.deviceCard}>
key={device.id} {/* 顶部行选择框和IMEI */}
style={{ <div className={styles.headerRow}>
background: "#fff", <div className={styles.checkboxContainer}>
borderRadius: 12,
padding: 12,
boxShadow: "0 1px 4px #eee",
display: "flex",
alignItems: "center",
cursor: "pointer",
border: selected.includes(device.id)
? "1.5px solid #1677ff"
: "1px solid #f0f0f0",
}}
onClick={() => goDetail(device.id!)}
>
<Checkbox <Checkbox
checked={selected.includes(device.id)} checked={selected.includes(device.id)}
onChange={e => { onChange={e => {
@@ -293,37 +283,70 @@ const Devices: React.FC = () => {
); );
}} }}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
style={{ marginRight: 12 }}
/> />
<div style={{ flex: 1 }}> </div>
<div style={{ fontWeight: 600, fontSize: 16 }}> <span className={styles.imeiText}>
IMEI: {device.imei?.toUpperCase()}
</span>
</div>
{/* 主要内容区域:头像和详细信息 */}
<div className={styles.mainContent}>
{/* 头像 */}
<div className={styles.avatar}>
{device.avatar ? (
<img src={device.avatar} alt="头像" />
) : (
<span className={styles.avatarText}>
{(device.memo || device.wechatId || "设")[0]}
</span>
)}
</div>
{/* 设备信息 */}
<div className={styles.deviceInfo}>
<div className={styles.deviceHeader}>
<h3 className={styles.deviceName}>
{device.memo || "未命名设备"} {device.memo || "未命名设备"}
</div> </h3>
<div style={{ fontSize: 14, color: "#999", marginTop: 2 }}>
IMEI: {device.imei}
</div>
<div style={{ fontSize: 14, color: "#999", marginTop: 2 }}>
: {device.wechatId || "未绑定"}
</div>
<div style={{ fontSize: 14, color: "#999", marginTop: 2 }}>
: {device.totalFriend ?? "-"}
</div>
</div>
<span <span
style={{ className={`${styles.statusBadge} ${
fontSize: 12,
color:
device.status === "online" || device.alive === 1 device.status === "online" || device.alive === 1
? "#52c41a" ? styles.online
: "#aaa", : styles.offline
marginLeft: 8, }`}
}}
> >
{device.status === "online" || device.alive === 1 {device.status === "online" || device.alive === 1
? "在线" ? "在线"
: "离线"} : "离线"}
</span> </span>
</div> </div>
<div className={styles.infoList}>
<div className={styles.infoItem}>
<span className={styles.infoLabel}>:</span>
<span className={styles.infoValue}>
{device.wechatId || "未绑定"}
</span>
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}>:</span>
<span
className={`${styles.infoValue} ${styles.friendCount}`}
>
{device.totalFriend ?? "-"}
</span>
</div>
</div>
</div>
{/* 箭头图标 */}
<RightOutlined
className={styles.arrowIcon}
onClick={() => goDetail(device.id!)}
/>
</div>
</div>
))} ))}
{/* 无限滚动提示(仅在不分页时显示) */} {/* 无限滚动提示(仅在不分页时显示) */}

View File

@@ -11,6 +11,7 @@ export interface Device {
nickname?: string; nickname?: string;
battery?: number; battery?: number;
lastActive?: string; lastActive?: string;
avatar?: string;
features?: { features?: {
autoAddFriend?: boolean; autoAddFriend?: boolean;
autoReply?: boolean; autoReply?: boolean;