This commit is contained in:
wong
2026-01-05 10:44:08 +08:00
parent ce1c434ea8
commit aabd8037a6
9 changed files with 151 additions and 77 deletions

4
Cunkebao/.gitignore vendored
View File

@@ -5,3 +5,7 @@ yarn.lock
.env .env
.DS_Store .DS_Store
dist/* dist/*
.cursorindexingignore
*.zip
.idea/
.next/

7
Server/.gitignore vendored
View File

@@ -9,3 +9,10 @@ vendor
/404.html /404.html
# SpecStory explanation file # SpecStory explanation file
.specstory/.what-is-this.md .specstory/.what-is-this.md
/
.cursorindexingignore
.user.ini
nginx.htaccess
.cursor/
thinkphp/
public/static/

View File

@@ -1,40 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>恭喜,站点创建成功!</title>
<style>
.container {
width: 60%;
margin: 10% auto 0;
background-color: #f0f0f0;
padding: 2% 5%;
border-radius: 10px
}
ul {
padding-left: 20px;
}
ul li {
line-height: 2.3
}
a {
color: #20a53a
}
</style>
</head>
<body>
<div class="container">
<h1>恭喜, 站点创建成功!</h1>
<h3>这是默认index.html本页面由系统自动生成</h3>
<ul>
<li>本页面在FTP根目录下的index.html</li>
<li>您可以修改、删除或覆盖本页面</li>
<li>FTP相关信息请到“面板系统后台 > FTP” 查看</li>
</ul>
</div>
</body>
</html>

View File

@@ -1 +0,0 @@
Pa_Vv8VOhdcPRmNHh_J_flKMHVwUywOIwT-F9iiA8p4.2wKeISBpPoGcCt72TdgVrBMxtnnieXof2OZkRdHKCJI

4
Store_vue/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.cursorindexingignore
.specstory/
node_modules/
unpackage/

View File

@@ -3,4 +3,6 @@ dist/
build/ build/
yarn.lock yarn.lock
.env .env
.DS_Store .DS_Store
.specstory/
.cursorindexingignore

View File

@@ -31,41 +31,86 @@
.accountCard { .accountCard {
flex: 1; flex: 1;
min-width: 120px; max-width: 140px;
cursor: pointer; cursor: pointer;
border: 2px solid #f0f0f0; border: 2px solid #e8e8e8;
border-radius: 8px; border-radius: 12px;
transition: all 0.3s; transition: all 0.3s;
position: relative; position: relative;
background: #fff;
&:hover:not(.disabled) { &:hover:not(.disabled):not(.selected) {
border-color: #d9d9d9; border-color: #d9d9d9;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
} }
&.selected { &.selected {
border-color: #1890ff; border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); background: linear-gradient(135deg, #e6f7ff 0%, #f0f8ff 100%);
box-shadow:
0 0 0 3px rgba(24, 144, 255, 0.15),
0 4px 12px rgba(24, 144, 255, 0.2);
} }
&.disabled { &.disabled {
opacity: 0.6; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
background-color: #fafafa; background-color: #fafafa;
border-color: #f0f0f0;
} }
:global(.ant-card-body) { padding: 20px 16px;
padding: 16px;
}
.accountInfo { .accountInfo {
text-align: center; text-align: center;
display: flex;
flex-direction: column;
align-items: center;
.avatarWrapper {
position: relative;
margin-bottom: 12px;
.accountAvatar {
border: 3px solid #e8e8e8;
transition: all 0.3s;
}
.selectedIndicator {
position: absolute;
bottom: -2px;
right: -2px;
width: 24px;
height: 24px;
background: #1890ff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid #fff;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.4);
.checkIcon {
color: #fff;
font-size: 14px;
font-weight: bold;
line-height: 1;
}
}
}
.accountCard.selected .avatarWrapper .accountAvatar {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.accountName { .accountName {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: #262626; color: #262626;
margin-bottom: 8px; margin-bottom: 8px;
transition: color 0.3s;
} }
.usageBadge { .usageBadge {
@@ -75,6 +120,7 @@
height: 20px; height: 20px;
line-height: 18px; line-height: 18px;
border-radius: 10px; border-radius: 10px;
padding: 0 8px;
} }
} }
@@ -84,6 +130,19 @@
margin-top: 4px; margin-top: 4px;
} }
} }
// 选中状态下的样式增强
&.selected .accountInfo {
.accountName {
color: #1890ff;
font-weight: 600;
}
.avatarWrapper .accountAvatar {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
} }
} }

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { Button, Input, Select, message, Card, Badge } from "antd"; import { Button, Input, Select, message, Badge, Avatar } from "antd";
import { useCkChatStore } from "@/store/module/ckchat/ckchat"; import { useCustomerStore, updateCustomerList } from "@weChatStore/customer";
import { getCustomerList } from "@apiModule/wechat";
import { addMoment } from "./api"; import { addMoment } from "./api";
import styles from "./MomentPublish.module.scss"; import styles from "./MomentPublish.module.scss";
import { KfUserListData } from "@/pages/pc/ckbox/data"; import { KfUserListData } from "@/pages/pc/ckbox/data";
@@ -24,44 +25,60 @@ const MomentPublish: React.FC<MomentPublishProps> = ({ onPublishSuccess }) => {
const [linkUrl, setLinkUrl] = useState<string>(""); const [linkUrl, setLinkUrl] = useState<string>("");
const [accounts, setAccounts] = useState<KfUserListData[]>([]); const [accounts, setAccounts] = useState<KfUserListData[]>([]);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [customerListLoading, setCustomerListLoading] =
useState<boolean>(false);
// 从store获取客服列表 // 从store获取客服列表
const kfUserList = useCkChatStore(state => state.kfUserList); const customerList = useCustomerStore(state => state.customerList);
// 初始化获取客服列表
useEffect(() => {
const fetchCustomerList = async () => {
setCustomerListLoading(true);
try {
const res = await getCustomerList();
updateCustomerList(res);
} catch (error) {
console.error("获取客服列表失败:", error);
message.error("获取客服列表失败");
} finally {
setCustomerListLoading(false);
}
};
// 如果客服列表为空,则获取
if (customerList.length === 0) {
fetchCustomerList();
}
}, []);
// 获取账号使用情况 // 获取账号使用情况
const fetchAccountUsage = useCallback(async () => { const fetchAccountUsage = useCallback(async () => {
if (kfUserList.length === 0) return; if (customerList.length === 0) return;
// 直接使用客服列表数据不需要额外的API调用 // 直接使用客服列表数据不需要额外的API调用
const accountData = kfUserList.map((kf, index) => ({ const accountData = customerList.map((customer, index) => ({
...kf, ...customer,
name: kf.nickname || `客服${index + 1}`, name: customer.nickname || `客服${index + 1}`,
isSelected: selectedAccounts.includes(kf.id.toString()), isSelected: selectedAccounts.includes(customer.id.toString()),
isDisabled: kf.momentsNum >= kf.momentsMax, isDisabled: customer.momentsNum >= customer.momentsMax,
})); }));
setAccounts(accountData); setAccounts(accountData);
}, [kfUserList, selectedAccounts]); }, [customerList, selectedAccounts]);
// 如果没有选中的账号且有可用账号,自动选择第一个可用账号 // 移除自动选择逻辑,允许用户手动选择或取消选择
useEffect(() => {
if (selectedAccounts.length === 0 && accounts.length > 0) {
const firstAvailable = accounts.find(acc => !acc.isDisabled);
if (firstAvailable) {
setSelectedAccounts([firstAvailable.id.toString()]);
}
}
}, [accounts, selectedAccounts.length]);
// 当客服列表变化时,重新获取使用情况 // 当客服列表变化时,重新获取使用情况
useEffect(() => { useEffect(() => {
fetchAccountUsage(); fetchAccountUsage();
}, [kfUserList, selectedAccounts, fetchAccountUsage]); }, [customerList, selectedAccounts, fetchAccountUsage]);
const handleAccountSelect = (accountId: string) => { const handleAccountSelect = (accountId: string) => {
const account = accounts.find(acc => acc.id.toString() === accountId); const account = accounts.find(acc => acc.id.toString() === accountId);
if (!account || account.isDisabled) return; if (!account || account.isDisabled) return;
// 允许取消选择:如果已选中则取消,未选中则选中
setSelectedAccounts(prev => setSelectedAccounts(prev =>
prev.includes(accountId) prev.includes(accountId)
? prev.filter(id => id !== accountId) ? prev.filter(id => id !== accountId)
@@ -146,8 +163,13 @@ const MomentPublish: React.FC<MomentPublishProps> = ({ onPublishSuccess }) => {
setLinkImage(""); setLinkImage("");
setLinkUrl(""); setLinkUrl("");
setTimingTime(""); setTimingTime("");
// 重新获取账号使用情况 // 重新获取客服列表以更新使用情况
await fetchAccountUsage(); try {
const res = await getCustomerList();
updateCustomerList(res);
} catch (error) {
console.error("刷新客服列表失败:", error);
}
// 触发父组件的刷新回调 // 触发父组件的刷新回调
onPublishSuccess?.(); onPublishSuccess?.();
} catch (error) { } catch (error) {
@@ -167,7 +189,7 @@ const MomentPublish: React.FC<MomentPublishProps> = ({ onPublishSuccess }) => {
<h4 className={styles.sectionTitle}></h4> <h4 className={styles.sectionTitle}></h4>
<div className={styles.accountList}> <div className={styles.accountList}>
{accounts.map(account => ( {accounts.map(account => (
<Card <div
key={account.id} key={account.id}
className={`${styles.accountCard} ${ className={`${styles.accountCard} ${
selectedAccounts.includes(account.id.toString()) selectedAccounts.includes(account.id.toString())
@@ -177,6 +199,23 @@ const MomentPublish: React.FC<MomentPublishProps> = ({ onPublishSuccess }) => {
onClick={() => handleAccountSelect(account.id.toString())} onClick={() => handleAccountSelect(account.id.toString())}
> >
<div className={styles.accountInfo}> <div className={styles.accountInfo}>
<div className={styles.avatarWrapper}>
<Avatar
src={account.avatar}
size={56}
className={styles.accountAvatar}
style={{
backgroundColor: !account.avatar ? "#1890ff" : undefined,
}}
>
{!account.avatar && account.name?.charAt(0)}
</Avatar>
{selectedAccounts.includes(account.id.toString()) && (
<div className={styles.selectedIndicator}>
<span className={styles.checkIcon}></span>
</div>
)}
</div>
<div className={styles.accountName}>{account.name}</div> <div className={styles.accountName}>{account.name}</div>
<Badge <Badge
count={`${account.momentsNum}/${account.momentsMax}`} count={`${account.momentsNum}/${account.momentsMax}`}
@@ -189,7 +228,7 @@ const MomentPublish: React.FC<MomentPublishProps> = ({ onPublishSuccess }) => {
<div className={styles.limitText}></div> <div className={styles.limitText}></div>
)} )}
</div> </div>
</Card> </div>
))} ))}
</div> </div>
</div> </div>

View File

@@ -15,7 +15,7 @@ export default defineConfig({
}, },
server: { server: {
open: true, open: true,
port: 3000, port: 8888,
host: "0.0.0.0", host: "0.0.0.0",
}, },
build: { build: {