Merge branch 'yongpxu-dev' into yongpxu-dev2

# Conflicts:
#	nkebao/src/pages/mobile/mine/traffic-pool/detail/api.ts   resolved by yongpxu-dev version
#	nkebao/src/pages/mobile/mine/traffic-pool/detail/data.ts   resolved by yongpxu-dev version
#	nkebao/src/pages/mobile/mine/traffic-pool/detail/index.tsx   resolved by yongpxu-dev version
This commit is contained in:
2025-07-29 19:24:44 +08:00
184 changed files with 24833 additions and 16917 deletions

View File

@@ -1,6 +1,4 @@
# 基础环境变量示例 # 基础环境变量示例
VITE_API_BASE_URL=http://www.yishi.com # VITE_API_BASE_URL=http://www.yishi.com
# VITE_API_BASE_URL=https://ckbapi.quwanzhi.com VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
VITE_APP_TITLE=Nkebao Base VITE_APP_TITLE=Nkebao Base

View File

@@ -1,21 +1,64 @@
module.exports = { module.exports = {
root: true, root: true,
env: { env: {
browser: true, browser: true,
es2021: true, es2021: true,
node: true, node: true,
},
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended", // 这个配置会自动处理大部分冲突
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true,
}, },
extends: [ ecmaVersion: 12,
'react-app', sourceType: "module",
'plugin:react/recommended', },
'plugin:@typescript-eslint/recommended', plugins: ["react", "react-hooks", "@typescript-eslint", "prettier"],
'plugin:prettier/recommended', rules: {
], "prettier/prettier": "error",
parser: '@typescript-eslint/parser', "react/react-in-jsx-scope": "off",
plugins: ['react', '@typescript-eslint', 'prettier'], "@typescript-eslint/no-unused-vars": "warn",
rules: { "@typescript-eslint/no-explicit-any": "off",
'prettier/prettier': 'warn', "@typescript-eslint/no-unnecessary-type-constraint": "warn",
'react/react-in-jsx-scope': 'off', "react/prop-types": "off",
'@typescript-eslint/no-unused-vars': 'warn', "linebreak-style": "off",
"eol-last": "off",
"no-empty": "warn",
"prefer-const": "warn",
// 确保与 Prettier 完全兼容
"comma-dangle": "off",
"comma-spacing": "off",
"comma-style": "off",
"object-curly-spacing": "off",
"array-bracket-spacing": "off",
indent: "off",
quotes: "off",
semi: "off",
"arrow-parens": "off",
"no-multiple-empty-lines": "off",
"max-len": "off",
"space-before-function-paren": "off",
"space-before-blocks": "off",
"keyword-spacing": "off",
"space-infix-ops": "off",
"space-in-parens": "off",
"space-in-brackets": "off",
"object-property-newline": "off",
"array-element-newline": "off",
"function-paren-newline": "off",
"object-curly-newline": "off",
"array-bracket-newline": "off",
},
settings: {
react: {
version: "detect",
}, },
},
}; };

27
nkebao/.gitattributes vendored Normal file
View File

@@ -0,0 +1,27 @@
# 设置默认行为如果core.autocrlf没有设置Git会自动处理行尾符
* text=auto
# 明确指定文本文件使用LF
*.js text eol=lf
*.jsx text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
*.json text eol=lf
*.css text eol=lf
*.scss text eol=lf
*.html text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
# 二进制文件
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.svg binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary

13
nkebao/.prettierrc Normal file
View File

@@ -0,0 +1,13 @@
{
"semi": true,
"trailingComma": "all",
"singleQuote": false,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"endOfLine": "lf",
"bracketSpacing": true,
"arrowParens": "avoid",
"jsxSingleQuote": false,
"quoteProps": "as-needed"
}

11
nkebao/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss",
"ms-vscode.vscode-typescript-next",
"formulahendry.auto-rename-tag",
"christian-kohler.path-intellisense",
"ms-vscode.vscode-json"
]
}

45
nkebao/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,45 @@
{
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"eslint.format.enable": false,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"typescript.preferences.importModuleSpecifier": "relative",
"typescript.suggest.autoImports": true,
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.detectIndentation": false
}

6459
nkebao/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@
"antd": "^5.13.1", "antd": "^5.13.1",
"antd-mobile": "^5.39.1", "antd-mobile": "^5.39.1",
"axios": "^1.6.7", "axios": "^1.6.7",
"dayjs": "^1.11.13",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"echarts-for-react": "^3.0.2", "echarts-for-react": "^3.0.2",
"react": "^18.2.0", "react": "^18.2.0",
@@ -27,6 +28,7 @@
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.1", "eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^5.2.0",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"postcss-pxtorem": "^6.0.0", "postcss-pxtorem": "^6.0.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
@@ -37,7 +39,11 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"build:check": "tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix" "lint": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,scss,css}\"",
"lint:check": "eslint src --ext .js,.jsx,.ts,.tsx",
"format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx,json,scss,css}\""
} }
} }

View File

@@ -17,7 +17,7 @@ const instance: AxiosInstance = axios.create({
}, },
}); });
instance.interceptors.request.use((config) => { instance.interceptors.request.use((config: any) => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
if (token) { if (token) {
config.headers = config.headers || {}; config.headers = config.headers || {};
@@ -44,7 +44,7 @@ instance.interceptors.response.use(
} }
return Promise.reject(msg || "接口错误"); return Promise.reject(msg || "接口错误");
}, },
(err) => { err => {
Toast.show({ content: err.message || "网络异常", position: "top" }); Toast.show({ content: err.message || "网络异常", position: "top" });
return Promise.reject(err); return Promise.reject(err);
} }
@@ -73,6 +73,12 @@ export function request(
method, method,
...config, ...config,
}; };
// 如果是FormData不设置Content-Type让浏览器自动设置
if (data instanceof FormData) {
delete axiosConfig.headers?.["Content-Type"];
}
if (method.toUpperCase() === "GET") { if (method.toUpperCase() === "GET") {
axiosConfig.params = data; axiosConfig.params = data;
} else { } else {

View File

@@ -114,7 +114,7 @@ export default function AccountSelection({
// 渲染和过滤都依赖内部accountsList // 渲染和过滤都依赖内部accountsList
const filteredAccounts = accountsList.filter( const filteredAccounts = accountsList.filter(
(acc) => acc =>
acc.userName.includes(searchQuery) || acc.userName.includes(searchQuery) ||
acc.realName.includes(searchQuery) || acc.realName.includes(searchQuery) ||
acc.departmentName.includes(searchQuery) acc.departmentName.includes(searchQuery)
@@ -123,24 +123,27 @@ export default function AccountSelection({
// 处理账号选择 // 处理账号选择
const handleAccountToggle = (accountId: number) => { const handleAccountToggle = (accountId: number) => {
if (readonly) return; if (readonly) return;
const newSelected = value.includes(accountId) const uniqueValue = [...new Set(value)];
? value.filter((id) => id !== accountId) const newSelected = uniqueValue.includes(accountId)
: [...value, accountId]; ? uniqueValue.filter(id => id !== accountId)
: [...uniqueValue, accountId];
onChange(newSelected); onChange(newSelected);
}; };
// 获取显示文本 // 获取显示文本
const getDisplayText = () => { const getDisplayText = () => {
if (value.length === 0) return ""; const uniqueValue = [...new Set(value)];
return `已选择 ${value.length} 个账号`; if (uniqueValue.length === 0) return "";
return `已选择 ${uniqueValue.length} 个账号`;
}; };
// 获取已选账号详细信息 // 获取已选账号详细信息 - 去重处理
const uniqueValue = [...new Set(value)];
const selectedAccountObjs = [ const selectedAccountObjs = [
...accountsList.filter((acc) => value.includes(acc.id)), ...accountsList.filter(acc => uniqueValue.includes(acc.id)),
...value ...uniqueValue
.filter((id) => !accountsList.some((acc) => acc.id === id)) .filter(id => !accountsList.some(acc => acc.id === id))
.map((id) => ({ .map(id => ({
id, id,
userName: String(id), userName: String(id),
realName: "", realName: "",
@@ -151,13 +154,15 @@ export default function AccountSelection({
// 删除已选账号 // 删除已选账号
const handleRemoveAccount = (id: number) => { const handleRemoveAccount = (id: number) => {
if (readonly) return; if (readonly) return;
onChange(value.filter((d) => d !== id)); const uniqueValue = [...new Set(value)];
onChange(uniqueValue.filter(d => d !== id));
}; };
// 确认选择 // 确认选择
const handleConfirm = () => { const handleConfirm = () => {
if (onConfirm) { if (onConfirm) {
onConfirm(value, selectedAccountObjs); const uniqueValue = [...new Set(value)];
onConfirm(uniqueValue, selectedAccountObjs);
} }
setRealVisible(false); setRealVisible(false);
}; };
@@ -195,7 +200,7 @@ export default function AccountSelection({
background: "#fff", background: "#fff",
}} }}
> >
{selectedAccountObjs.map((acc) => ( {selectedAccountObjs.map(acc => (
<div <div
key={acc.id} key={acc.id}
className={style.selectedListRow} className={style.selectedListRow}
@@ -265,7 +270,7 @@ export default function AccountSelection({
currentPage={currentPage} currentPage={currentPage}
totalPages={1} totalPages={1}
loading={loading} loading={loading}
selectedCount={value.length} selectedCount={uniqueValue.length}
onPageChange={setCurrentPage} onPageChange={setCurrentPage}
onCancel={() => setRealVisible(false)} onCancel={() => setRealVisible(false)}
onConfirm={handleConfirm} onConfirm={handleConfirm}
@@ -279,7 +284,7 @@ export default function AccountSelection({
</div> </div>
) : filteredAccounts.length > 0 ? ( ) : filteredAccounts.length > 0 ? (
<div className={style.friendListInner}> <div className={style.friendListInner}>
{filteredAccounts.map((acc) => ( {filteredAccounts.map(acc => (
<label <label
key={acc.id} key={acc.id}
className={style.friendItem} className={style.friendItem}
@@ -288,12 +293,12 @@ export default function AccountSelection({
<div className={style.radioWrapper}> <div className={style.radioWrapper}>
<div <div
className={ className={
value.includes(acc.id) uniqueValue.includes(acc.id)
? style.radioSelected ? style.radioSelected
: style.radioUnselected : style.radioUnselected
} }
> >
{value.includes(acc.id) && ( {uniqueValue.includes(acc.id) && (
<div className={style.radioDot}></div> <div className={style.radioDot}></div>
)} )}
</div> </div>

View File

@@ -85,14 +85,14 @@ export default function ContentLibrarySelection({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// 获取已选内容库详细信息 // 获取已选内容库详细信息
const selectedLibraryObjs = libraries.filter((item) => const selectedLibraryObjs = libraries.filter(item =>
selectedLibraries.includes(item.id) selectedLibraries.includes(item.id)
); );
// 删除已选内容库 // 删除已选内容库
const handleRemoveLibrary = (id: string) => { const handleRemoveLibrary = (id: string) => {
if (readonly) return; if (readonly) return;
onSelect(selectedLibraries.filter((g) => g !== id)); onSelect(selectedLibraries.filter(g => g !== id));
}; };
// 受控弹窗逻辑 // 受控弹窗逻辑
@@ -132,7 +132,7 @@ export default function ContentLibrarySelection({
const fetchLibraries = async (page: number, keyword: string = "") => { const fetchLibraries = async (page: number, keyword: string = "") => {
setLoading(true); setLoading(true);
try { try {
let params: any = { const params: any = {
page, page,
limit: 20, limit: 20,
}; };
@@ -156,11 +156,11 @@ export default function ContentLibrarySelection({
const handleLibraryToggle = (libraryId: string) => { const handleLibraryToggle = (libraryId: string) => {
if (readonly) return; if (readonly) return;
const newSelected = selectedLibraries.includes(libraryId) const newSelected = selectedLibraries.includes(libraryId)
? selectedLibraries.filter((id) => id !== libraryId) ? selectedLibraries.filter(id => id !== libraryId)
: [...selectedLibraries, libraryId]; : [...selectedLibraries, libraryId];
onSelect(newSelected); onSelect(newSelected);
if (onSelectDetail) { if (onSelectDetail) {
const selectedObjs = libraries.filter((item) => const selectedObjs = libraries.filter(item =>
newSelected.includes(item.id) newSelected.includes(item.id)
); );
onSelectDetail(selectedObjs); onSelectDetail(selectedObjs);
@@ -214,7 +214,7 @@ export default function ContentLibrarySelection({
background: "#fff", background: "#fff",
}} }}
> >
{selectedLibraryObjs.map((item) => ( {selectedLibraryObjs.map(item => (
<div <div
key={item.id} key={item.id}
className={style.selectedListRow} className={style.selectedListRow}
@@ -298,7 +298,7 @@ export default function ContentLibrarySelection({
</div> </div>
) : libraries.length > 0 ? ( ) : libraries.length > 0 ? (
<div className={style.libraryListInner}> <div className={style.libraryListInner}>
{libraries.map((item) => ( {libraries.map(item => (
<label key={item.id} className={style.libraryItem}> <label key={item.id} className={style.libraryItem}>
<Checkbox <Checkbox
checked={selectedLibraries.includes(item.id)} checked={selectedLibraries.includes(item.id)}

View File

@@ -19,7 +19,6 @@
background: #f8f9fa; background: #f8f9fa;
} }
.popupHeader { .popupHeader {
padding: 16px; padding: 16px;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;

View File

@@ -43,7 +43,7 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
// 删除已选设备 // 删除已选设备
const handleRemoveDevice = (id: string) => { const handleRemoveDevice = (id: string) => {
if (readonly) return; if (readonly) return;
onSelect(selectedDevices.filter((d) => d !== id)); onSelect(selectedDevices.filter(d => d !== id));
}; };
return ( return (
@@ -79,7 +79,7 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
background: "#fff", background: "#fff",
}} }}
> >
{selectedDevices.map((deviceId) => ( {selectedDevices.map(deviceId => (
<div <div
key={deviceId} key={deviceId}
className={style.selectedListRow} className={style.selectedListRow}

View File

@@ -101,7 +101,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
}, [currentPage]); }, [currentPage]);
// 过滤设备(只保留状态过滤) // 过滤设备(只保留状态过滤)
const filteredDevices = devices.filter((device) => { const filteredDevices = devices.filter(device => {
const matchesStatus = const matchesStatus =
statusFilter === "all" || statusFilter === "all" ||
(statusFilter === "online" && device.status === "online") || (statusFilter === "online" && device.status === "online") ||
@@ -114,7 +114,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
// 处理设备选择 // 处理设备选择
const handleDeviceToggle = (deviceId: string) => { const handleDeviceToggle = (deviceId: string) => {
if (selectedDevices.includes(deviceId)) { if (selectedDevices.includes(deviceId)) {
onSelect(selectedDevices.filter((id) => id !== deviceId)); onSelect(selectedDevices.filter(id => id !== deviceId));
} else { } else {
onSelect([...selectedDevices, deviceId]); onSelect([...selectedDevices, deviceId]);
} }
@@ -169,7 +169,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
</div> </div>
) : ( ) : (
<div className={style.deviceListInner}> <div className={style.deviceListInner}>
{filteredDevices.map((device) => ( {filteredDevices.map(device => (
<label key={device.id} className={style.deviceItem}> <label key={device.id} className={style.deviceItem}>
<Checkbox <Checkbox
checked={selectedDevices.includes(device.id)} checked={selectedDevices.includes(device.id)}

View File

@@ -98,7 +98,7 @@ export default function FriendSelection({
const fetchFriends = async (page: number, keyword: string = "") => { const fetchFriends = async (page: number, keyword: string = "") => {
setLoading(true); setLoading(true);
try { try {
let params: any = { const params: any = {
page, page,
limit: 20, limit: 20,
}; };
@@ -129,14 +129,14 @@ export default function FriendSelection({
if (readonly) return; if (readonly) return;
const newSelectedFriends = selectedFriends.includes(friendId) const newSelectedFriends = selectedFriends.includes(friendId)
? selectedFriends.filter((id) => id !== friendId) ? selectedFriends.filter(id => id !== friendId)
: [...selectedFriends, friendId]; : [...selectedFriends, friendId];
onSelect(newSelectedFriends); onSelect(newSelectedFriends);
// 如果有 onSelectDetail 回调,传递完整的好友对象 // 如果有 onSelectDetail 回调,传递完整的好友对象
if (onSelectDetail) { if (onSelectDetail) {
const selectedFriendObjs = friends.filter((friend) => const selectedFriendObjs = friends.filter(friend =>
newSelectedFriends.includes(friend.id) newSelectedFriends.includes(friend.id)
); );
onSelectDetail(selectedFriendObjs); onSelectDetail(selectedFriendObjs);
@@ -151,10 +151,10 @@ export default function FriendSelection({
// 获取已选好友详细信息 // 获取已选好友详细信息
const selectedFriendObjs = [ const selectedFriendObjs = [
...friends.filter((friend) => selectedFriends.includes(friend.id)), ...friends.filter(friend => selectedFriends.includes(friend.id)),
...selectedFriends ...selectedFriends
.filter((id) => !friends.some((friend) => friend.id === id)) .filter(id => !friends.some(friend => friend.id === id))
.map((id) => ({ .map(id => ({
id, id,
nickname: id, nickname: id,
wechatId: id, wechatId: id,
@@ -166,7 +166,7 @@ export default function FriendSelection({
// 删除已选好友 // 删除已选好友
const handleRemoveFriend = (id: string) => { const handleRemoveFriend = (id: string) => {
if (readonly) return; if (readonly) return;
onSelect(selectedFriends.filter((d) => d !== id)); onSelect(selectedFriends.filter(d => d !== id));
}; };
// 确认选择 // 确认选择
@@ -210,7 +210,7 @@ export default function FriendSelection({
background: "#fff", background: "#fff",
}} }}
> >
{selectedFriendObjs.map((friend) => ( {selectedFriendObjs.map(friend => (
<div <div
key={friend.id} key={friend.id}
className={style.selectedListRow} className={style.selectedListRow}
@@ -294,7 +294,7 @@ export default function FriendSelection({
</div> </div>
) : friends.length > 0 ? ( ) : friends.length > 0 ? (
<div className={style.friendListInner}> <div className={style.friendListInner}>
{friends.map((friend) => ( {friends.map(friend => (
<label <label
key={friend.id} key={friend.id}
className={style.friendItem} className={style.friendItem}

View File

@@ -58,14 +58,14 @@ export default function GroupSelection({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// 获取已选群聊详细信息 // 获取已选群聊详细信息
const selectedGroupObjs = groups.filter((group) => const selectedGroupObjs = groups.filter(group =>
selectedGroups.includes(group.id) selectedGroups.includes(group.id)
); );
// 删除已选群聊 // 删除已选群聊
const handleRemoveGroup = (id: string) => { const handleRemoveGroup = (id: string) => {
if (readonly) return; if (readonly) return;
onSelect(selectedGroups.filter((g) => g !== id)); onSelect(selectedGroups.filter(g => g !== id));
}; };
// 受控弹窗逻辑 // 受控弹窗逻辑
@@ -106,7 +106,7 @@ export default function GroupSelection({
const fetchGroups = async (page: number, keyword: string = "") => { const fetchGroups = async (page: number, keyword: string = "") => {
setLoading(true); setLoading(true);
try { try {
let params: any = { const params: any = {
page, page,
limit: 20, limit: 20,
}; };
@@ -133,14 +133,14 @@ export default function GroupSelection({
if (readonly) return; if (readonly) return;
const newSelectedGroups = selectedGroups.includes(groupId) const newSelectedGroups = selectedGroups.includes(groupId)
? selectedGroups.filter((id) => id !== groupId) ? selectedGroups.filter(id => id !== groupId)
: [...selectedGroups, groupId]; : [...selectedGroups, groupId];
onSelect(newSelectedGroups); onSelect(newSelectedGroups);
// 如果有 onSelectDetail 回调,传递完整的群聊对象 // 如果有 onSelectDetail 回调,传递完整的群聊对象
if (onSelectDetail) { if (onSelectDetail) {
const selectedGroupObjs = groups.filter((group) => const selectedGroupObjs = groups.filter(group =>
newSelectedGroups.includes(group.id) newSelectedGroups.includes(group.id)
); );
onSelectDetail(selectedGroupObjs); onSelectDetail(selectedGroupObjs);
@@ -194,7 +194,7 @@ export default function GroupSelection({
background: "#fff", background: "#fff",
}} }}
> >
{selectedGroupObjs.map((group) => ( {selectedGroupObjs.map(group => (
<div <div
key={group.id} key={group.id}
className={style.selectedListRow} className={style.selectedListRow}
@@ -278,7 +278,7 @@ export default function GroupSelection({
</div> </div>
) : groups.length > 0 ? ( ) : groups.length > 0 ? (
<div className={style.groupListInner}> <div className={style.groupListInner}>
{groups.map((group) => ( {groups.map(group => (
<label <label
key={group.id} key={group.id}
className={style.groupItem} className={style.groupItem}

View File

@@ -42,12 +42,12 @@ const MeauMobile: React.FC<MeauMobileProps> = ({ activeKey }) => {
<TabBar <TabBar
style={{ background: "#fff" }} style={{ background: "#fff" }}
activeKey={activeKey} activeKey={activeKey}
onChange={(key) => { onChange={key => {
const tab = tabs.find((t) => t.key === key); const tab = tabs.find(t => t.key === key);
if (tab && tab.path) navigate(tab.path); if (tab && tab.path) navigate(tab.path);
}} }}
> >
{tabs.map((item) => ( {tabs.map(item => (
<TabBar.Item key={item.key} icon={item.icon} title={item.title} /> <TabBar.Item key={item.key} icon={item.icon} title={item.title} />
))} ))}
</TabBar> </TabBar>

View File

@@ -31,7 +31,6 @@
font-size: 18px; font-size: 18px;
} }
.refreshBtn { .refreshBtn {
width: 36px; width: 36px;
height: 36px; height: 36px;

View File

@@ -45,7 +45,7 @@ const PopupHeader: React.FC<PopupHeaderProps> = ({
<Input <Input
placeholder={searchPlaceholder} placeholder={searchPlaceholder}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={e => setSearchQuery(e.target.value)}
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
size="large" size="large"
/> />
@@ -74,7 +74,7 @@ const PopupHeader: React.FC<PopupHeaderProps> = ({
onChange={tabsConfig.onChange} onChange={tabsConfig.onChange}
style={{ marginTop: 8 }} style={{ marginTop: 8 }}
> >
{tabsConfig.tabs.map((tab) => ( {tabsConfig.tabs.map(tab => (
<Tabs.Tab key={tab.key} title={tab.title} /> <Tabs.Tab key={tab.key} title={tab.title} />
))} ))}
</Tabs> </Tabs>

View File

@@ -0,0 +1,112 @@
# Upload 上传组件
基于 antd-mobile 的 ImageUploader 组件封装的上传组件,支持图片上传、预览、删除等功能。
## 功能特性
- ✅ 支持单张/多张图片上传
- ✅ 文件类型和大小验证
- ✅ 上传进度显示
- ✅ 图片预览功能
- ✅ 删除确认
- ✅ 数量限制
- ✅ 编辑和新增状态支持
- ✅ 响应式设计
## 使用方法
### 基础用法
```tsx
import React, { useState } from "react";
import UploadComponent from "@/components/Upload";
const MyComponent = () => {
const [images, setImages] = useState<string[]>([]);
return (
<UploadComponent
value={images}
onChange={setImages}
count={5}
accept="image/*"
/>
);
};
```
### 编辑模式
```tsx
const EditComponent = () => {
const [images, setImages] = useState<string[]>([
"https://example.com/image1.jpg",
"https://example.com/image2.jpg",
]);
return (
<UploadComponent
value={images}
onChange={setImages}
count={9}
disabled={false}
/>
);
};
```
### 禁用状态
```tsx
<UploadComponent value={images} onChange={setImages} disabled={true} />
```
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| --------- | -------------- | -------------------------- | ----------- |
| value | 图片URL数组 | `string[]` | `[]` |
| onChange | 图片变化回调 | `(urls: string[]) => void` | - |
| count | 最大上传数量 | `number` | `9` |
| accept | 接受的文件类型 | `string` | `"image/*"` |
| disabled | 是否禁用 | `boolean` | `false` |
| className | 自定义类名 | `string` | - |
### 事件
| 事件名 | 说明 | 回调参数 |
| -------- | ------------------ | -------------------------- |
| onChange | 图片列表变化时触发 | `(urls: string[]) => void` |
## 注意事项
1. **文件大小限制**: 默认限制为 5MB
2. **文件类型**: 默认只接受图片文件
3. **上传接口**: 使用 `/v1/attachment/upload` 接口
4. **认证**: 自动携带 token 进行认证
5. **预览**: 点击图片可预览
6. **删除**: 删除图片会有确认提示
## 样式定制
组件支持通过 CSS 模块进行样式定制:
```scss
.uploadContainer {
// 自定义样式
:global {
.adm-image-uploader {
// 覆盖 antd-mobile 默认样式
}
}
}
```
## 错误处理
- 文件类型不匹配时会显示错误提示
- 文件大小超限时会显示错误提示
- 上传失败时会显示错误提示
- 网络错误时会显示错误提示

View File

@@ -0,0 +1,145 @@
import React, { useState } from "react";
import { Upload, message } from "antd";
import { LoadingOutlined, PlusOutlined } from "@ant-design/icons";
import type { UploadProps, UploadFile } from "antd/es/upload/interface";
import style from "./index.module.scss";
interface VideoUploadProps {
value?: string;
onChange?: (url: string) => void;
disabled?: boolean;
className?: string;
}
const VideoUpload: React.FC<VideoUploadProps> = ({
value = "",
onChange,
disabled = false,
className,
}) => {
const [loading, setLoading] = useState(false);
const [fileList, setFileList] = useState<UploadFile[]>([]);
React.useEffect(() => {
if (value) {
const file: UploadFile = {
uid: "-1",
name: "video",
status: "done",
url: value || "", // 确保 URL 不为 undefined
};
setFileList([file]);
} else {
setFileList([]);
}
}, [value]);
// 文件验证
const beforeUpload = (file: File) => {
const isVideo = file.type.startsWith("video/");
if (!isVideo) {
message.error("只能上传视频文件!");
return false;
}
const isLt50M = file.size / 1024 / 1024 < 50;
if (!isLt50M) {
message.error("视频大小不能超过50MB");
return false;
}
return true; // 允许上传
};
// 处理文件变化
const handleChange: UploadProps["onChange"] = info => {
console.log("VideoUpload handleChange info:", info);
// 更新 fileList确保所有 URL 都是字符串
const updatedFileList = info.fileList.map(file => ({
...file,
url:
file.url ||
file.response?.data ||
file.response?.url ||
file.response ||
"",
}));
setFileList(updatedFileList);
// 处理上传状态
if (info.file.status === "uploading") {
setLoading(true);
} else if (info.file.status === "done") {
setLoading(false);
message.success("上传成功");
// 从响应中获取上传后的URL
const uploadedUrl =
info.file.response?.data ||
info.file.response?.url ||
info.file.response ||
"";
if (uploadedUrl) {
// 调用onChange
onChange?.(uploadedUrl);
}
} else if (info.file.status === "error") {
setLoading(false);
message.error("上传失败,请重试");
} else if (info.file.status === "removed") {
// 文件被删除
onChange?.("");
}
};
// 删除文件
const handleRemove = () => {
setFileList([]);
onChange?.("");
return true;
};
const uploadButton = (
<div className={style["upload-button"]}>
{loading ? (
<div className={style["uploading"]}>
<LoadingOutlined className={style["upload-icon"]} />
<div className={style["upload-text"]}>...</div>
</div>
) : (
<>
<PlusOutlined className={style["upload-icon"]} />
<div className={style["upload-text"]}></div>
</>
)}
</div>
);
const action = import.meta.env.VITE_API_BASE_URL + "/v1/attachment/upload";
return (
<div className={`${style["upload-container"]} ${className || ""}`}>
<Upload
name="file"
headers={{
Authorization: `Bearer ${localStorage.getItem("token")}`,
}}
action={action}
multiple={false}
fileList={fileList}
accept="video/*"
listType="text"
showUploadList={true}
disabled={disabled || loading}
beforeUpload={beforeUpload}
onChange={handleChange}
onRemove={handleRemove}
maxCount={1}
>
{fileList.length >= 1 ? null : uploadButton}
</Upload>
</div>
);
};
export default VideoUpload;

View File

@@ -0,0 +1,108 @@
.uploadContainer {
width: 100%;
// 自定义上传组件样式
:global {
.adm-image-uploader {
.adm-image-uploader-upload-button {
width: 100px;
height: 100px;
border: 1px dashed #d9d9d9;
border-radius: 8px;
background: #fafafa;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #1677ff;
background: #f0f8ff;
}
.adm-image-uploader-upload-button-icon {
font-size: 32px;
color: #999;
}
}
.adm-image-uploader-item {
width: 100px;
height: 100px;
border-radius: 8px;
overflow: hidden;
position: relative;
.adm-image-uploader-item-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.adm-image-uploader-item-delete {
position: absolute;
top: 4px;
right: 4px;
width: 24px;
height: 24px;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 14px;
cursor: pointer;
}
.adm-image-uploader-item-loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
}
// 禁用状态
.uploadContainer.disabled {
opacity: 0.6;
pointer-events: none;
}
// 错误状态
.uploadContainer.error {
:global {
.adm-image-uploader-upload-button {
border-color: #ff4d4f;
background: #fff2f0;
}
}
}
// 响应式设计
@media (max-width: 768px) {
.uploadContainer {
:global {
.adm-image-uploader {
.adm-image-uploader-upload-button,
.adm-image-uploader-item {
width: 80px;
height: 80px;
}
.adm-image-uploader-upload-button-icon {
font-size: 28px;
}
}
}
}
}

View File

@@ -0,0 +1,134 @@
import React, { useState, useEffect } from "react";
import { ImageUploader, Toast, Dialog } from "antd-mobile";
import type { ImageUploadItem } from "antd-mobile/es/components/image-uploader";
import style from "./index.module.scss";
interface UploadComponentProps {
value?: string[];
onChange?: (urls: string[]) => void;
count?: number; // 最大上传数量
accept?: string; // 文件类型
disabled?: boolean;
className?: string;
}
const UploadComponent: React.FC<UploadComponentProps> = ({
value = [],
onChange,
count = 9,
accept = "image/*",
disabled = false,
className,
}) => {
const [fileList, setFileList] = useState<ImageUploadItem[]>([]);
// 将value转换为fileList格式
useEffect(() => {
if (value && value.length > 0) {
const files = value.map((url, index) => ({
url: url || "",
uid: `file-${index}`,
}));
setFileList(files);
} else {
setFileList([]);
}
}, [value]);
// 文件验证
const beforeUpload = (file: File) => {
// 检查文件类型
const isValidType = file.type.startsWith(accept.replace("*", ""));
if (!isValidType) {
Toast.show(`只能上传${accept}格式的文件!`);
return null;
}
// 检查文件大小 (5MB)
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
Toast.show("文件大小不能超过5MB");
return null;
}
return file;
};
// 上传函数
const upload = async (file: File): Promise<{ url: string }> => {
const formData = new FormData();
formData.append("file", file);
try {
const response = await fetch(
`${import.meta.env.VITE_API_BASE_URL}/v1/attachment/upload`,
{
method: "POST",
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
body: formData,
}
);
if (!response.ok) {
throw new Error("上传失败");
}
const result = await response.json();
if (result.code === 200) {
Toast.show("上传成功");
return { url: result.data.url || result.data };
} else {
throw new Error(result.msg || "上传失败");
}
} catch (error) {
Toast.show("上传失败,请重试");
throw error;
}
};
// 处理文件变化
const handleChange = (files: ImageUploadItem[]) => {
setFileList(files);
// 提取URL数组并传递给父组件
const urls = files
.map(file => file.url)
.filter(url => Boolean(url)) as string[];
onChange?.(urls);
};
// 删除确认
const handleDelete = () => {
return Dialog.confirm({
content: "确定要删除这张图片吗?",
});
};
// 数量超出限制
const handleCountExceed = (exceed: number) => {
Toast.show(`最多选择 ${count} 张图片,你多选了 ${exceed}`);
};
return (
<div className={`${style.uploadContainer} ${className || ""}`}>
<ImageUploader
value={fileList}
onChange={handleChange}
upload={upload}
beforeUpload={beforeUpload}
onDelete={handleDelete}
onCountExceed={handleCountExceed}
multiple={count > 1}
maxCount={count}
showUpload={fileList.length < count && !disabled}
accept={accept}
/>
</div>
);
};
export default UploadComponent;

View File

@@ -1,4 +1,4 @@
import request from '@/api/request'; import request from "@/api/request";
export interface LoginParams { export interface LoginParams {
phone: string; phone: string;
password?: string; password?: string;
@@ -28,26 +28,26 @@ export interface SendCodeResponse {
} }
// 密码登录 // 密码登录
export function loginWithPassword(params:any) { export function loginWithPassword(params: any) {
return request('/v1/auth/login', params, 'POST'); return request("/v1/auth/login", params, "POST");
} }
// 验证码登录 // 验证码登录
export function loginWithCode(params:any) { export function loginWithCode(params: any) {
return request('/v1/auth/login-code', params, 'POST'); return request("/v1/auth/login-code", params, "POST");
} }
// 发送验证码 // 发送验证码
export function sendVerificationCode(params:any) { export function sendVerificationCode(params: any) {
return request('/v1/auth/code',params, 'POST'); return request("/v1/auth/code", params, "POST");
} }
// 退出登录 // 退出登录
export function logout() { export function logout() {
return request('/v1/auth/logout', {}, 'POST'); return request("/v1/auth/logout", {}, "POST");
} }
// 获取用户信息 // 获取用户信息
export function getUserInfo() { export function getUserInfo() {
return request('/v1/auth/user-info', {}, 'GET'); return request("/v1/auth/user-info", {}, "GET");
} }

View File

@@ -52,7 +52,8 @@
} }
@keyframes float { @keyframes float {
0%, 100% { 0%,
100% {
transform: translateY(0px) rotate(0deg); transform: translateY(0px) rotate(0deg);
} }
50% { 50% {
@@ -325,7 +326,7 @@
margin: 24px 0; margin: 24px 0;
&::before { &::before {
content: ''; content: "";
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 0; left: 0;

View File

@@ -15,7 +15,7 @@
.form-card { .form-card {
border-radius: 16px; border-radius: 16px;
box-shadow: 0 4px 24px rgba(0,0,0,0.06); box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
padding: 24px 18px 18px 18px; padding: 24px 18px 18px 18px;
background: #fff; background: #fff;
} }
@@ -64,7 +64,7 @@
border-radius: 10px; border-radius: 10px;
padding: 18px 14px 10px 14px; padding: 18px 14px 10px 14px;
margin-top: 2px; margin-top: 2px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
} }
.form-section { .form-section {
margin-bottom: 22px; margin-bottom: 22px;
@@ -83,7 +83,8 @@
} }
} }
.ai-row, .section-block { .ai-row,
.section-block {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
@@ -95,7 +96,8 @@
flex: 1; flex: 1;
} }
.date-row, .section-block { .date-row,
.section-block {
display: flex; display: flex;
gap: 12px; gap: 12px;
align-items: center; align-items: center;

View File

@@ -48,7 +48,7 @@ export default function ContentForm() {
if (isEdit && id) { if (isEdit && id) {
setLoading(true); setLoading(true);
getContentLibraryDetail(id) getContentLibraryDetail(id)
.then((data) => { .then(data => {
setName(data.name || ""); setName(data.name || "");
setSourceType(data.sourceType === 1 ? "friends" : "groups"); setSourceType(data.sourceType === 1 ? "friends" : "groups");
setSelectedFriends(data.sourceFriends || []); setSelectedFriends(data.sourceFriends || []);
@@ -59,14 +59,14 @@ export default function ContentForm() {
setUseAI(!!data.aiPrompt); setUseAI(!!data.aiPrompt);
setEnabled(data.status === 1); setEnabled(data.status === 1);
// 时间范围 // 时间范围
let start = data.timeStart || data.startTime; const start = data.timeStart || data.startTime;
let end = data.timeEnd || data.endTime; const end = data.timeEnd || data.endTime;
setDateRange([ setDateRange([
start ? new Date(start) : null, start ? new Date(start) : null,
end ? new Date(end) : null, end ? new Date(end) : null,
]); ]);
}) })
.catch((e) => { .catch(e => {
Toast.show({ Toast.show({
content: e?.message || "获取详情失败", content: e?.message || "获取详情失败",
position: "top", position: "top",
@@ -92,11 +92,11 @@ export default function ContentForm() {
groupMembers: {}, groupMembers: {},
keywordInclude: keywordsInclude keywordInclude: keywordsInclude
.split(/,||\n|\s+/) .split(/,||\n|\s+/)
.map((s) => s.trim()) .map(s => s.trim())
.filter(Boolean), .filter(Boolean),
keywordExclude: keywordsExclude keywordExclude: keywordsExclude
.split(/,||\n|\s+/) .split(/,||\n|\s+/)
.map((s) => s.trim()) .map(s => s.trim())
.filter(Boolean), .filter(Boolean),
aiPrompt, aiPrompt,
timeEnabled: dateRange[0] || dateRange[1] ? 1 : 0, timeEnabled: dateRange[0] || dateRange[1] ? 1 : 0,
@@ -148,7 +148,7 @@ export default function ContentForm() {
<div className={style["form-page"]}> <div className={style["form-page"]}>
<form <form
className={style["form-main"]} className={style["form-main"]}
onSubmit={(e) => e.preventDefault()} onSubmit={e => e.preventDefault()}
autoComplete="off" autoComplete="off"
> >
<div className={style["form-section"]}> <div className={style["form-section"]}>
@@ -159,7 +159,7 @@ export default function ContentForm() {
<AntdInput <AntdInput
placeholder="请输入内容库名称" placeholder="请输入内容库名称"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={e => setName(e.target.value)}
className={style["input"]} className={style["input"]}
/> />
</div> </div>
@@ -168,7 +168,7 @@ export default function ContentForm() {
<div className={style["form-section"]}> <div className={style["form-section"]}>
<Tabs <Tabs
activeKey={sourceType} activeKey={sourceType}
onChange={(key) => setSourceType(key as "friends" | "groups")} onChange={key => setSourceType(key as "friends" | "groups")}
className={style["tabs-bar"]} className={style["tabs-bar"]}
> >
<Tabs.Tab title="选择微信好友" key="friends"> <Tabs.Tab title="选择微信好友" key="friends">
@@ -201,7 +201,7 @@ export default function ContentForm() {
<TextArea <TextArea
placeholder="多个关键词用逗号分隔" placeholder="多个关键词用逗号分隔"
value={keywordsInclude} value={keywordsInclude}
onChange={(e) => setKeywordsInclude(e.target.value)} onChange={e => setKeywordsInclude(e.target.value)}
className={style["input"]} className={style["input"]}
autoSize={{ minRows: 2, maxRows: 4 }} autoSize={{ minRows: 2, maxRows: 4 }}
/> />
@@ -211,7 +211,7 @@ export default function ContentForm() {
<TextArea <TextArea
placeholder="多个关键词用逗号分隔" placeholder="多个关键词用逗号分隔"
value={keywordsExclude} value={keywordsExclude}
onChange={(e) => setKeywordsExclude(e.target.value)} onChange={e => setKeywordsExclude(e.target.value)}
className={style["input"]} className={style["input"]}
autoSize={{ minRows: 2, maxRows: 4 }} autoSize={{ minRows: 2, maxRows: 4 }}
/> />
@@ -235,7 +235,7 @@ export default function ContentForm() {
<AntdInput <AntdInput
placeholder="请输入AI提示词" placeholder="请输入AI提示词"
value={aiPrompt} value={aiPrompt}
onChange={(e) => setAIPrompt(e.target.value)} onChange={e => setAIPrompt(e.target.value)}
className={style["input"]} className={style["input"]}
/> />
</div> </div>
@@ -260,7 +260,7 @@ export default function ContentForm() {
title="开始时间" title="开始时间"
value={dateRange[0]} value={dateRange[0]}
onClose={() => setShowStartPicker(false)} onClose={() => setShowStartPicker(false)}
onConfirm={(val) => { onConfirm={val => {
setDateRange([val, dateRange[1]]); setDateRange([val, dateRange[1]]);
setShowStartPicker(false); setShowStartPicker(false);
}} }}
@@ -280,7 +280,7 @@ export default function ContentForm() {
title="结束时间" title="结束时间"
value={dateRange[1]} value={dateRange[1]}
onClose={() => setShowEndPicker(false)} onClose={() => setShowEndPicker(false)}
onConfirm={(val) => { onConfirm={val => {
setDateRange([dateRange[0], val]); setDateRange([dateRange[0], val]);
setShowEndPicker(false); setShowEndPicker(false);
}} }}

View File

@@ -40,8 +40,6 @@
} }
} }
.create-btn { .create-btn {
border-radius: 20px; border-radius: 20px;
padding: 0 16px; padding: 0 16px;
@@ -60,14 +58,10 @@
} }
} }
.tabs { .tabs {
flex:1; flex: 1;
} }
.library-list { .library-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -55,7 +55,7 @@ const CardMenu: React.FC<CardMenuProps> = ({
return ( return (
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>
<button onClick={() => setOpen((v) => !v)} className={style["menu-btn"]}> <button onClick={() => setOpen(v => !v)} className={style["menu-btn"]}>
<MoreOutlined /> <MoreOutlined />
</button> </button>
{open && ( {open && (
@@ -180,7 +180,7 @@ const ContentLibraryList: React.FC = () => {
}; };
const filteredLibraries = libraries.filter( const filteredLibraries = libraries.filter(
(library) => library =>
library.name.toLowerCase().includes(searchQuery.toLowerCase()) || library.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
library.creatorName?.toLowerCase().includes(searchQuery.toLowerCase()) library.creatorName?.toLowerCase().includes(searchQuery.toLowerCase())
); );
@@ -204,7 +204,7 @@ const ContentLibraryList: React.FC = () => {
<Input <Input
placeholder="搜索内容库" placeholder="搜索内容库"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={e => setSearchQuery(e.target.value)}
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
allowClear allowClear
size="large" size="large"
@@ -254,7 +254,7 @@ const ContentLibraryList: React.FC = () => {
</Button> </Button>
</div> </div>
) : ( ) : (
filteredLibraries.map((library) => ( filteredLibraries.map(library => (
<Card key={library.id} className={style["library-card"]}> <Card key={library.id} className={style["library-card"]}>
<div className={style["card-header"]}> <div className={style["card-header"]}>
<div className={style["library-info"]}> <div className={style["library-info"]}>

View File

@@ -1,32 +1,20 @@
import request from "@/api/request"; import request from "@/api/request";
import {
ContentItem,
ContentLibrary,
CreateContentItemParams,
UpdateContentItemParams,
} from "./data";
// 获取素材详情 // 获取素材详情
export function getContentItemDetail(id: string): Promise<any> { export function getContentItemDetail(id: string) {
return request("/v1/content/item/detail", { id }, "GET"); return request("/v1/content/library/get-item-detail", { id }, "GET");
} }
// 创建素材 // 创建素材
export function createContentItem( export function createContentItem(params: any) {
params: CreateContentItemParams return request("/v1/content/library/create-item", params, "POST");
): Promise<any> {
return request("/v1/content/item/create", params, "POST");
} }
// 更新素材 // 更新素材
export function updateContentItem( export function updateContentItem(params: any) {
params: UpdateContentItemParams return request(`/v1/content/library/update-item`, params, "POST");
): Promise<any> {
const { id, ...data } = params;
return request(`/v1/content/item/update`, { id, ...data }, "POST");
} }
// 获取内容库详情 // 获取内容库详情
export function getContentLibraryDetail(id: string): Promise<any> { export function getContentLibraryDetail(id: string) {
return request("/v1/content/library/detail", { id }, "GET"); return request("/v1/content/library/detail", { id }, "GET");
} }

View File

@@ -1,28 +1,36 @@
// 素材数据类型定义 // 素材数据类型定义
export interface ContentItem { export interface ContentItem {
id: string; id: number; // 修改为number类型
libraryId: string; libraryId: number; // 修改为number类型
type?: string;
contentType: number; // 0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文
title: string; title: string;
content: string; content: string;
contentType: number; // 0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文 contentAi?: string;
contentTypeName?: string; contentData?: any;
resUrls?: string[]; snsId?: string | null;
urls?: string[]; msgId?: string | null;
comment?: string; wechatId?: string | null;
sendTime?: string; friendId?: string | null;
createMomentTime?: number;
createTime: string; createTime: string;
updateTime: string; updateTime: string;
wechatId?: string; coverImage?: string;
wechatNickname?: string; resUrls?: string[];
wechatAvatar?: string; urls?: any[];
snsId?: string; location?: string | null;
msgId?: string; lat?: string;
type?: string; lng?: string;
contentData?: string; status?: number;
createMomentTime?: string; isDel?: number;
createMessageTime?: string; delTime?: number;
createMomentTimeFormatted?: string; wechatChatroomId?: string | null;
createMessageTimeFormatted?: string; senderNickname?: string;
createMessageTime?: string | null;
comment?: string;
sendTime?: string; // 字符串格式的时间
sendTimes?: number;
contentTypeName?: string;
} }
// 内容库类型 // 内容库类型

View File

@@ -34,9 +34,30 @@
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
} }
.textarea { .form-item {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
.required {
color: #ff4d4f;
margin-right: 4px;
}
}
.form-input {
width: 100%;
height: 40px;
border-radius: 6px; border-radius: 6px;
border: 1px solid #d9d9d9; border: 1px solid #d9d9d9;
padding: 0 12px;
font-size: 14px;
&:focus { &:focus {
border-color: #1677ff; border-color: #1677ff;
@@ -44,7 +65,7 @@
} }
} }
.time-picker { .form-select {
width: 100%; width: 100%;
border-radius: 6px; border-radius: 6px;
@@ -54,6 +75,20 @@
} }
} }
.form-textarea {
width: 100%;
border-radius: 6px;
border: 1px solid #d9d9d9;
padding: 8px 12px;
font-size: 14px;
resize: vertical;
&:focus {
border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
}
}
.select-option { .select-option {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -1,11 +1,10 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { Button, Toast, SpinLoading, Form, Card } from "antd-mobile"; import { Button, Toast, SpinLoading, Card } from "antd-mobile";
import { Input, TimePicker, Select, Upload } from "antd"; import { Input, Select } from "antd";
import { import {
ArrowLeftOutlined, ArrowLeftOutlined,
SaveOutlined, SaveOutlined,
UploadOutlined,
PictureOutlined, PictureOutlined,
LinkOutlined, LinkOutlined,
VideoCameraOutlined, VideoCameraOutlined,
@@ -14,13 +13,13 @@ import {
} from "@ant-design/icons"; } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout"; import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon"; import NavCommon from "@/components/NavCommon";
import UploadComponent from "@/components/Upload";
import VideoUpload from "@/components/Upload/VideoUpload";
import { import {
getContentItemDetail, getContentItemDetail,
createContentItem, createContentItem,
updateContentItem, updateContentItem,
getContentLibraryDetail,
} from "./api"; } from "./api";
import { ContentItem, ContentLibrary } from "./data";
import style from "./index.module.scss"; import style from "./index.module.scss";
const { Option } = Select; const { Option } = Select;
@@ -33,7 +32,6 @@ const contentTypeOptions = [
{ value: 3, label: "视频", icon: <VideoCameraOutlined /> }, { value: 3, label: "视频", icon: <VideoCameraOutlined /> },
{ value: 4, label: "文本", icon: <FileTextOutlined /> }, { value: 4, label: "文本", icon: <FileTextOutlined /> },
{ value: 5, label: "小程序", icon: <AppstoreOutlined /> }, { value: 5, label: "小程序", icon: <AppstoreOutlined /> },
{ value: 6, label: "图文", icon: <PictureOutlined /> },
]; ];
const MaterialForm: React.FC = () => { const MaterialForm: React.FC = () => {
@@ -42,119 +40,129 @@ const MaterialForm: React.FC = () => {
id: string; id: string;
materialId: string; materialId: string;
}>(); }>();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [material, setMaterial] = useState<ContentItem | null>(null);
const [library, setLibrary] = useState<ContentLibrary | null>(null); // 表单状态
const [contentType, setContentType] = useState<number>(4); const [contentType, setContentType] = useState<number>(4);
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [comment, setComment] = useState("");
const [sendTime, setSendTime] = useState("");
const [resUrls, setResUrls] = useState<string[]>([]);
// 链接相关状态
const [linkDesc, setLinkDesc] = useState("");
const [linkImage, setLinkImage] = useState("");
const [linkUrl, setLinkUrl] = useState("");
// 小程序相关状态
const [appTitle, setAppTitle] = useState("");
const [appId, setAppId] = useState("");
const isEdit = !!materialId; const isEdit = !!materialId;
// 获取内容库详情
useEffect(() => {
if (libraryId) {
fetchLibraryDetail();
}
}, [libraryId]);
// 获取素材详情 // 获取素材详情
useEffect(() => { const fetchMaterialDetail = useCallback(async () => {
if (isEdit && materialId) {
fetchMaterialDetail();
}
}, [isEdit, materialId]);
const fetchLibraryDetail = async () => {
if (!libraryId) return;
try {
const response = await getContentLibraryDetail(libraryId);
if (response.code === 200 && response.data) {
setLibrary(response.data);
}
} catch (error) {
console.error("获取内容库详情失败:", error);
}
};
const fetchMaterialDetail = async () => {
if (!materialId) return; if (!materialId) return;
setLoading(true); setLoading(true);
try { try {
const response = await getContentItemDetail(materialId); const response = await getContentItemDetail(materialId);
if (response.code === 200 && response.data) { // 填充表单数据
setMaterial(response.data); setTitle(response.title || "");
setContentType(response.data.contentType); setContent(response.content || "");
setContentType(response.contentType || 4);
setComment(response.comment || "");
// 填充表单数据 // 处理时间格式 - sendTime是字符串格式需要转换为datetime-local格式
form.setFieldsValue({ if (response.sendTime) {
title: response.data.title, // 将 "2025-07-28 16:11:00" 转换为 "2025-07-28T16:11"
content: response.data.content, const dateTime = new Date(response.sendTime);
contentType: response.data.contentType, setSendTime(dateTime.toISOString().slice(0, 16));
comment: response.data.comment || "",
sendTime: response.data.sendTime || "",
resUrls: response.data.resUrls || [],
urls: response.data.urls || [],
});
} else { } else {
Toast.show({ setSendTime("");
content: response.msg || "获取素材详情失败",
position: "top",
});
} }
} catch (error: any) {
setResUrls(response.resUrls || []);
// 设置链接相关数据
if (response.urls && response.urls.length > 0) {
const firstUrl = response.urls[0];
if (typeof firstUrl === "object" && firstUrl !== null) {
setLinkDesc(firstUrl.desc || "");
setLinkImage(firstUrl.image || "");
setLinkUrl(firstUrl.url || "");
}
}
} catch (error: unknown) {
console.error("获取素材详情失败:", error); console.error("获取素材详情失败:", error);
Toast.show({
content: error?.message || "请检查网络连接",
position: "top",
});
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [materialId]);
const handleSubmit = async (values: any) => { useEffect(() => {
if (isEdit && materialId) {
fetchMaterialDetail();
}
}, [isEdit, materialId, fetchMaterialDetail]);
const handleSubmit = async () => {
if (!libraryId) return; if (!libraryId) return;
if (!content.trim()) {
Toast.show({
content: "请输入素材内容",
position: "top",
});
return;
}
setSaving(true); setSaving(true);
try { try {
// 构建urls数据
let finalUrls: { desc: string; image: string; url: string }[] = [];
if (contentType === 2 && linkUrl) {
finalUrls = [
{
desc: linkDesc,
image: linkImage,
url: linkUrl,
},
];
}
const params = { const params = {
libraryId, libraryId,
title: values.title, title,
content: values.content, content,
contentType: values.contentType, contentType,
comment: values.comment || "", comment,
sendTime: values.sendTime || "", sendTime: sendTime || "",
resUrls: values.resUrls || [], resUrls,
urls: values.urls || [], urls: finalUrls,
type: contentType,
}; };
let response;
if (isEdit) { if (isEdit) {
response = await updateContentItem({ await updateContentItem({
id: materialId!, id: materialId!,
...params, ...params,
}); });
} else { } else {
response = await createContentItem(params); await createContentItem(params);
} }
if (response.code === 200) { // 直接使用返回数据无需判断code
Toast.show({ Toast.show({
content: isEdit ? "更新成功" : "创建成功", content: isEdit ? "更新成功" : "创建成功",
position: "top", position: "top",
}); });
navigate(`/content/materials/${libraryId}`); navigate(`/content/materials/${libraryId}`);
} else { } catch (error: unknown) {
Toast.show({
content: response.msg || (isEdit ? "更新失败" : "创建失败"),
position: "top",
});
}
} catch (error: any) {
console.error("保存素材失败:", error); console.error("保存素材失败:", error);
Toast.show({ Toast.show({
content: error?.message || "请检查网络连接", content: error instanceof Error ? error.message : "请检查网络连接",
position: "top", position: "top",
}); });
} finally { } finally {
@@ -177,41 +185,58 @@ const MaterialForm: React.FC = () => {
} }
return ( return (
<Layout header={<NavCommon title={isEdit ? "编辑素材" : "新建素材"} />}> <Layout
header={<NavCommon title={isEdit ? "编辑素材" : "新建素材"} />}
footer={
<div className={style["form-actions"]}>
<Button
fill="outline"
onClick={handleBack}
className={style["back-btn"]}
>
<ArrowLeftOutlined />
</Button>
<Button
color="primary"
onClick={handleSubmit}
loading={saving}
className={style["submit-btn"]}
>
<SaveOutlined />
{isEdit ? " 保存修改" : " 保存素材"}
</Button>
</div>
}
>
<div className={style["form-page"]}> <div className={style["form-page"]}>
<Form <div className={style["form"]}>
form={form} {/* 基础信息 */}
layout="vertical"
onFinish={handleSubmit}
className={style["form"]}
initialValues={{
contentType: 4,
resUrls: [],
urls: [],
}}
>
{/* 基本信息 */}
<Card className={style["form-card"]}> <Card className={style["form-card"]}>
<div className={style["card-title"]}></div> <div className={style["card-title"]}></div>
<Form.Item <div className={style["form-item"]}>
name="title" <label className={style["form-label"]}></label>
label="素材标题" <Input
rules={[{ required: true, message: "请输入素材标题" }]} type="datetime-local"
> value={sendTime}
<Input placeholder="请输入素材标题" /> onChange={e => setSendTime(e.target.value)}
</Form.Item> placeholder="请选择发布时间"
className={style["form-input"]}
/>
</div>
<Form.Item <div className={style["form-item"]}>
name="contentType" <label className={style["form-label"]}>
label="内容类型" <span className={style["required"]}>*</span>
rules={[{ required: true, message: "请选择内容类型" }]} </label>
>
<Select <Select
placeholder="请选择内容类型" value={contentType}
onChange={(value) => setContentType(value)} onChange={value => setContentType(value)}
placeholder="请选择类型"
className={style["form-select"]}
> >
{contentTypeOptions.map((option) => ( {contentTypeOptions.map(option => (
<Option key={option.value} value={option.value}> <Option key={option.value} value={option.value}>
<div className={style["select-option"]}> <div className={style["select-option"]}>
{option.icon} {option.icon}
@@ -220,96 +245,156 @@ const MaterialForm: React.FC = () => {
</Option> </Option>
))} ))}
</Select> </Select>
</Form.Item> </div>
</Card>
<Form.Item {/* 内容信息 */}
name="content" <Card className={style["form-card"]}>
label="内容" <div className={style["card-title"]}></div>
rules={[{ required: true, message: "请输入内容" }]}
> <div className={style["form-item"]}>
<label className={style["form-label"]}>
<span className={style["required"]}>*</span>
</label>
<TextArea <TextArea
value={content}
onChange={e => setContent(e.target.value)}
placeholder="请输入内容" placeholder="请输入内容"
rows={6} rows={6}
className={style["textarea"]} className={style["form-textarea"]}
/> />
</Form.Item> </div>
{/* 链接类型特有字段 */}
{contentType === 2 && (
<>
<div className={style["form-item"]}>
<label className={style["form-label"]}>
<span className={style["required"]}>*</span>
</label>
<Input
value={linkDesc}
onChange={e => setLinkDesc(e.target.value)}
placeholder="请输入描述"
className={style["form-input"]}
/>
</div>
<div className={style["form-item"]}>
<label className={style["form-label"]}></label>
<UploadComponent
value={linkImage ? [linkImage] : []}
onChange={urls => setLinkImage(urls[0] || "")}
count={1}
/>
</div>
<div className={style["form-item"]}>
<label className={style["form-label"]}>
<span className={style["required"]}>*</span>
</label>
<Input
value={linkUrl}
onChange={e => setLinkUrl(e.target.value)}
placeholder="请输入链接地址"
className={style["form-input"]}
/>
</div>
</>
)}
{/* 视频类型特有字段 */}
{contentType === 3 && (
<div className={style["form-item"]}>
<label className={style["form-label"]}></label>
<VideoUpload
value={resUrls[0] || ""}
onChange={url => setResUrls([url])}
/>
</div>
)}
</Card> </Card>
{/* 资源设置 */} {/* 素材上传(仅图片类型和小程序类型) */}
{[1, 5].includes(contentType) && (
<Card className={style["form-card"]}>
<div className={style["card-title"]}>
(: {contentType})
</div>
{contentType === 1 && (
<div className={style["form-item"]}>
<label className={style["form-label"]}></label>
<div>
<UploadComponent
value={resUrls}
onChange={setResUrls}
count={9}
/>
</div>
<div
style={{
fontSize: "12px",
color: "#666",
marginTop: "4px",
}}
>
: {contentType}, : {resUrls.length}
</div>
</div>
)}
{contentType === 5 && (
<>
<div className={style["form-item"]}>
<label className={style["form-label"]}></label>
<Input
value={appTitle}
onChange={e => setAppTitle(e.target.value)}
placeholder="请输入小程序名称"
className={style["form-input"]}
/>
</div>
<div className={style["form-item"]}>
<label className={style["form-label"]}>AppID</label>
<Input
value={appId}
onChange={e => setAppId(e.target.value)}
placeholder="请输入AppID"
className={style["form-input"]}
/>
</div>
<div className={style["form-item"]}>
<label className={style["form-label"]}></label>
<UploadComponent
value={resUrls}
onChange={setResUrls}
count={9}
/>
</div>
</>
)}
</Card>
)}
{/* 评论/备注 */}
<Card className={style["form-card"]}> <Card className={style["form-card"]}>
<div className={style["card-title"]}></div> <div className={style["card-title"]}>/</div>
<Form.Item <div className={style["form-item"]}>
name="resUrls" <label className={style["form-label"]}></label>
label="资源链接"
extra="图片、视频等资源链接,多个用换行分隔"
>
<TextArea <TextArea
placeholder="请输入资源链接,多个用换行分隔" value={comment}
onChange={e => setComment(e.target.value)}
placeholder="请输入评论或备注"
rows={4} rows={4}
className={style["textarea"]} className={style["form-textarea"]}
/> />
</Form.Item> </div>
<Form.Item
name="urls"
label="外部链接"
extra="外部网页链接,多个用换行分隔"
>
<TextArea
placeholder="请输入外部链接,多个用换行分隔"
rows={4}
className={style["textarea"]}
/>
</Form.Item>
</Card> </Card>
</div>
{/* 其他设置 */}
<Card className={style["form-card"]}>
<div className={style["card-title"]}></div>
<Form.Item name="comment" label="备注" extra="素材备注信息">
<TextArea
placeholder="请输入备注信息"
rows={3}
className={style["textarea"]}
/>
</Form.Item>
<Form.Item
name="sendTime"
label="发送时间"
extra="计划发送时间(可选)"
>
<TimePicker
format="YYYY-MM-DD HH:mm:ss"
placeholder="选择发送时间"
className={style["time-picker"]}
/>
</Form.Item>
</Card>
{/* 操作按钮 */}
<div className={style["form-actions"]}>
<Button
fill="outline"
onClick={handleBack}
className={style["back-btn"]}
>
<ArrowLeftOutlined />
</Button>
<Button
color="primary"
type="submit"
loading={saving}
className={style["submit-btn"]}
>
<SaveOutlined />
{isEdit ? "更新" : "创建"}
</Button>
</div>
</Form>
</div> </div>
</Layout> </Layout>
); );

View File

@@ -1,45 +1,37 @@
import request from "@/api/request"; import request from "@/api/request";
import { import {
ContentItem,
ContentLibrary,
GetContentItemListParams, GetContentItemListParams,
CreateContentItemParams, CreateContentItemParams,
UpdateContentItemParams, UpdateContentItemParams,
} from "./data"; } from "./data";
// 获取素材列表 // 获取素材列表
export function getContentItemList( export function getContentItemList(params: GetContentItemListParams) {
params: GetContentItemListParams return request("/v1/content/library/item-list", params, "GET");
): Promise<any> {
return request("/v1/content/item/list", params, "GET");
} }
// 获取素材详情 // 获取素材详情
export function getContentItemDetail(id: string): Promise<any> { export function getContentItemDetail(id: string) {
return request("/v1/content/item/detail", { id }, "GET"); return request("/v1/content/item/detail", { id }, "GET");
} }
// 创建素材 // 创建素材
export function createContentItem( export function createContentItem(params: CreateContentItemParams) {
params: CreateContentItemParams
): Promise<any> {
return request("/v1/content/item/create", params, "POST"); return request("/v1/content/item/create", params, "POST");
} }
// 更新素材 // 更新素材
export function updateContentItem( export function updateContentItem(params: UpdateContentItemParams) {
params: UpdateContentItemParams
): Promise<any> {
const { id, ...data } = params; const { id, ...data } = params;
return request(`/v1/content/item/update`, { id, ...data }, "POST"); return request(`/v1/content/item/update`, { id, ...data }, "POST");
} }
// 删除素材 // 删除素材
export function deleteContentItem(id: string): Promise<any> { export function deleteContentItem(id: string) {
return request("/v1/content/item/delete", { id }, "DELETE"); return request("/v1/content/library/delete-item", { id }, "DELETE");
} }
// 获取内容库详情 // 获取内容库详情
export function getContentLibraryDetail(id: string): Promise<any> { export function getContentLibraryDetail(id: string) {
return request("/v1/content/library/detail", { id }, "GET"); return request("/v1/content/library/detail", { id }, "GET");
} }

View File

@@ -1,28 +1,36 @@
// 素材数据类型定义 // 素材数据类型定义
export interface ContentItem { export interface ContentItem {
id: string; id: number;
libraryId: string; libraryId: number;
type: string;
contentType: number; // 0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文
title: string; title: string;
content: string; content: string;
contentType: number; // 0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文 contentAi?: string | null;
contentTypeName?: string; contentData?: string | null;
resUrls?: string[]; snsId?: string | null;
urls?: string[]; msgId?: string | null;
comment?: string; wechatId?: string | null;
sendTime?: string; friendId?: string | null;
createMomentTime: number;
createTime: string; createTime: string;
updateTime: string; updateTime: string;
wechatId?: string; coverImage: string;
wechatNickname?: string; resUrls: string[];
wechatAvatar?: string; urls: { desc: string; image: string; url: string }[];
snsId?: string; location?: string | null;
msgId?: string; lat: string;
type?: string; lng: string;
contentData?: string; status: number;
createMomentTime?: string; isDel: number;
createMessageTime?: string; delTime: number;
createMomentTimeFormatted?: string; wechatChatroomId?: string | null;
createMessageTimeFormatted?: string; senderNickname: string;
createMessageTime?: string | null;
comment: string;
sendTime: number;
sendTimes: number;
contentTypeName: string;
} }
// 内容库类型 // 内容库类型
@@ -86,7 +94,7 @@ export interface CreateContentItemParams {
content: string; content: string;
contentType: number; contentType: number;
resUrls?: string[]; resUrls?: string[];
urls?: string[]; urls?: (string | { desc?: string; image?: string; url: string })[];
comment?: string; comment?: string;
sendTime?: string; sendTime?: string;
} }

View File

@@ -1,7 +1,5 @@
.materials-page { .materials-page {
padding: 16px; padding: 16px;
background: #f5f5f5;
min-height: 100vh;
} }
.search-bar { .search-bar {
@@ -40,7 +38,6 @@
} }
} }
.create-btn { .create-btn {
border-radius: 20px; border-radius: 20px;
padding: 0 16px; padding: 0 16px;
@@ -116,23 +113,60 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
margin-bottom: 12px; margin-bottom: 16px;
} }
.material-info { .avatar-section {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 12px;
flex: 1; flex: 1;
} }
.material-title { .avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: #e6f7ff;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; justify-content: center;
}
.avatar-icon {
font-size: 24px;
color: #1677ff;
}
.header-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.creator-name {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
line-height: 1.2;
}
.material-id {
background: #e6f7ff;
color: #1677ff;
font-size: 12px;
font-weight: 600;
border-radius: 12px;
padding: 2px 8px;
display: inline-block;
}
.material-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
line-height: 1.4;
} }
.content-icon { .content-icon {
@@ -197,57 +231,385 @@
} }
} }
.card-content { .link-preview {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 16px;
border: 1px solid #e9ecef;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: #e9ecef;
border-color: #1677ff;
}
}
.link-icon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.link-content {
flex: 1;
min-width: 0;
}
.link-title {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.link-url {
font-size: 12px;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.action-buttons {
display: flex;
margin-top: 16px;
justify-content: space-between;
}
.action-btn-group {
display: flex; display: flex;
flex-direction: column;
gap: 8px; gap: 8px;
} }
.content-preview { .action-btn {
color: #666; border-radius: 6px;
font-size: 14px; font-size: 16px;
line-height: 1.5; padding: 6px 12px;
max-height: 60px; border: 1px solid #d9d9d9;
overflow: hidden; background: white;
text-overflow: ellipsis; color: #333;
display: -webkit-box;
-webkit-line-clamp: 3; &:hover {
-webkit-box-orient: vertical; border-color: #1677ff;
color: #1677ff;
}
} }
.material-meta { .delete-btn {
display: flex; border-radius: 6px;
flex-direction: column; font-size: 16px;
gap: 4px; padding: 6px 12px;
font-size: 12px; background: #ff4d4f;
color: #999; border-color: #ff4d4f;
} color: white;
.meta-item { &:hover {
display: flex; background: #ff7875;
align-items: center; border-color: #ff7875;
}
} }
.pagination-wrapper { .pagination-wrapper {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-top: 20px; align-items: center;
padding: 16px; padding: 16px;
background: white; background: white;
border-radius: 8px; border-top: 1px solid #f0f0f0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
} }
.pagination { // 内容类型标签样式
:global { .content-type-tag {
.adm-pagination-item { display: inline-flex;
border-radius: 6px; align-items: center;
margin: 0 2px; padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
border: 1px solid currentColor;
}
&.adm-pagination-item-active { // 图片类型预览样式
background: #1677ff; .material-image-preview {
color: white; margin: 12px 0;
.image-grid {
display: grid;
gap: 8px;
width: 100%;
// 1张图片宽度拉伸高度自适应
&.single {
grid-template-columns: 1fr;
img {
width: 100%;
height: auto;
object-fit: cover;
border-radius: 8px;
}
}
// 2张图片左右并列
&.double {
grid-template-columns: 1fr 1fr;
img {
width: 100%;
height: 120px;
object-fit: cover;
border-radius: 8px;
}
}
// 3张图片三张并列
&.triple {
grid-template-columns: 1fr 1fr 1fr;
img {
width: 100%;
height: 100px;
object-fit: cover;
border-radius: 8px;
}
}
// 4张图片2x2网格布局
&.quad {
grid-template-columns: repeat(2, 1fr);
img {
width: 100%;
height: 140px;
object-fit: cover;
border-radius: 8px;
}
}
// 5张及以上网格布局
&.grid {
grid-template-columns: repeat(3, 1fr);
img {
width: 100%;
height: 100px;
object-fit: cover;
border-radius: 8px;
}
.image-more {
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: 8px;
color: #666;
font-size: 12px;
font-weight: 500;
height: 100px;
}
}
}
.no-image {
display: flex;
align-items: center;
justify-content: center;
height: 80px;
background: #f5f5f5;
border-radius: 8px;
color: #999;
font-size: 14px;
}
}
// 链接类型预览样式
.material-link-preview {
margin: 12px 0;
.link-card {
display: flex;
background: #e9f8ff;
border-radius: 8px;
padding: 12px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid #cde6ff;
&:hover {
background: #cde6ff;
}
.link-image {
width: 60px;
height: 60px;
margin-right: 12px;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 6px;
}
}
.link-content {
flex: 1;
min-width: 0;
.link-title {
font-weight: 500;
margin-bottom: 4px;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.link-url {
font-size: 12px;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
} }
} }
} }
// 视频类型预览样式
.material-video-preview {
margin: 12px 0;
.video-thumbnail {
video {
width: 100%;
max-height: 200px;
border-radius: 8px;
}
}
.no-video {
display: flex;
align-items: center;
justify-content: center;
height: 120px;
background: #f5f5f5;
border-radius: 8px;
color: #999;
font-size: 14px;
}
}
// 文本类型预览样式
.material-text-preview {
margin: 12px 0;
.text-content {
background: #f8f9fa;
padding: 12px;
border-radius: 8px;
line-height: 1.6;
color: #333;
font-size: 14px;
}
}
// 小程序类型预览样式
.material-miniprogram-preview {
margin: 12px 0;
.miniprogram-card {
display: flex;
background: #f8f9fa;
border-radius: 8px;
padding: 12px;
width: 100%;
img {
width: 60px;
height: 60px;
border-radius: 8px;
margin-right: 12px;
flex-shrink: 0;
object-fit: cover;
}
.miniprogram-info {
flex: 1;
min-width: 0;
.miniprogram-title {
font-weight: 500;
color: #333;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
// 图文类型预览样式
.material-article-preview {
margin: 12px 0;
.article-image {
margin-bottom: 12px;
img {
width: 100%;
height: 120px;
object-fit: cover;
border-radius: 8px;
}
}
.article-content {
.article-title {
font-weight: 500;
color: #333;
margin-bottom: 8px;
font-size: 16px;
}
.article-text {
color: #666;
line-height: 1.6;
font-size: 14px;
}
}
}
// 默认预览样式
.material-default-preview {
margin: 12px 0;
.default-content {
background: #f8f9fa;
padding: 12px;
border-radius: 8px;
color: #333;
line-height: 1.6;
}
}

View File

@@ -1,23 +1,15 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { import { Toast, SpinLoading, Dialog, Card } from "antd-mobile";
Button, import { Input, Pagination, Button } from "antd";
Toast,
SpinLoading,
Dialog,
Card,
Avatar,
Tag,
} from "antd-mobile";
import { Pagination, Input } from "antd";
import { import {
PlusOutlined, PlusOutlined,
SearchOutlined, SearchOutlined,
ReloadOutlined, ReloadOutlined,
EditOutlined, EditOutlined,
DeleteOutlined, DeleteOutlined,
EyeOutlined, UserOutlined,
MoreOutlined, BarChartOutlined,
PictureOutlined, PictureOutlined,
LinkOutlined, LinkOutlined,
VideoCameraOutlined, VideoCameraOutlined,
@@ -26,140 +18,30 @@ import {
} from "@ant-design/icons"; } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout"; import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon"; import NavCommon from "@/components/NavCommon";
import { import { getContentItemList, deleteContentItem } from "./api";
getContentItemList, import { ContentItem } from "./data";
deleteContentItem,
getContentLibraryDetail,
} from "./api";
import { ContentItem, ContentLibrary } from "./data";
import style from "./index.module.scss"; import style from "./index.module.scss";
// 卡片菜单组件 // 内容类型配置
interface CardMenuProps { const contentTypeConfig = {
onView: () => void; 1: { label: "图片", icon: PictureOutlined, color: "#52c41a" },
onEdit: () => void; 2: { label: "链接", icon: LinkOutlined, color: "#1890ff" },
onDelete: () => void; 3: { label: "视频", icon: VideoCameraOutlined, color: "#722ed1" },
} 4: { label: "文本", icon: FileTextOutlined, color: "#fa8c16" },
5: { label: "小程序", icon: AppstoreOutlined, color: "#eb2f96" },
const CardMenu: React.FC<CardMenuProps> = ({ onView, onEdit, onDelete }) => { 6: { label: "图文", icon: PictureOutlined, color: "#13c2c2" },
const [open, setOpen] = useState(false);
const menuRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpen(false);
}
}
if (open) document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [open]);
return (
<div style={{ position: "relative" }}>
<button onClick={() => setOpen((v) => !v)} className={style["menu-btn"]}>
<MoreOutlined />
</button>
{open && (
<div ref={menuRef} className={style["menu-dropdown"]}>
<div
onClick={() => {
onView();
setOpen(false);
}}
className={style["menu-item"]}
>
<EyeOutlined />
</div>
<div
onClick={() => {
onEdit();
setOpen(false);
}}
className={style["menu-item"]}
>
<EditOutlined />
</div>
<div
onClick={() => {
onDelete();
setOpen(false);
}}
className={`${style["menu-item"]} ${style["danger"]}`}
>
<DeleteOutlined />
</div>
</div>
)}
</div>
);
};
// 内容类型图标映射
const getContentTypeIcon = (type: number) => {
switch (type) {
case 1:
return <PictureOutlined className={style["content-icon"]} />;
case 2:
return <LinkOutlined className={style["content-icon"]} />;
case 3:
return <VideoCameraOutlined className={style["content-icon"]} />;
case 4:
return <FileTextOutlined className={style["content-icon"]} />;
case 5:
return <AppstoreOutlined className={style["content-icon"]} />;
default:
return <FileTextOutlined className={style["content-icon"]} />;
}
};
// 内容类型文字映射
const getContentTypeText = (type: number) => {
switch (type) {
case 1:
return "图片";
case 2:
return "链接";
case 3:
return "视频";
case 4:
return "文本";
case 5:
return "小程序";
case 6:
return "图文";
default:
return "未知";
}
}; };
const MaterialsList: React.FC = () => { const MaterialsList: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const [materials, setMaterials] = useState<ContentItem[]>([]); const [materials, setMaterials] = useState<ContentItem[]>([]);
const [library, setLibrary] = useState<ContentLibrary | null>(null);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const pageSize = 20; const pageSize = 20;
// 获取内容库详情
const fetchLibraryDetail = useCallback(async () => {
if (!id) return;
try {
const response = await getContentLibraryDetail(id);
if (response.code === 200 && response.data) {
setLibrary(response.data);
}
} catch (error) {
console.error("获取内容库详情失败:", error);
}
}, [id]);
// 获取素材列表 // 获取素材列表
const fetchMaterials = useCallback(async () => { const fetchMaterials = useCallback(async () => {
if (!id) return; if (!id) return;
@@ -172,19 +54,12 @@ const MaterialsList: React.FC = () => {
keyword: searchQuery, keyword: searchQuery,
}); });
if (response.code === 200 && response.data) { setMaterials(response.list || []);
setMaterials(response.data.list || []); setTotal(response.total || 0);
setTotal(response.data.total || 0); } catch (error: unknown) {
} else {
Toast.show({
content: response.msg || "获取素材列表失败",
position: "top",
});
}
} catch (error: any) {
console.error("获取素材列表失败:", error); console.error("获取素材列表失败:", error);
Toast.show({ Toast.show({
content: error?.message || "请检查网络连接", content: error instanceof Error ? error.message : "请检查网络连接",
position: "top", position: "top",
}); });
} finally { } finally {
@@ -192,10 +67,6 @@ const MaterialsList: React.FC = () => {
} }
}, [id, currentPage, searchQuery]); }, [id, currentPage, searchQuery]);
useEffect(() => {
fetchLibraryDetail();
}, [fetchLibraryDetail]);
useEffect(() => { useEffect(() => {
fetchMaterials(); fetchMaterials();
}, [fetchMaterials]); }, [fetchMaterials]);
@@ -204,11 +75,11 @@ const MaterialsList: React.FC = () => {
navigate(`/content/materials/new/${id}`); navigate(`/content/materials/new/${id}`);
}; };
const handleEdit = (materialId: string) => { const handleEdit = (materialId: number) => {
navigate(`/content/materials/edit/${id}/${materialId}`); navigate(`/content/materials/edit/${id}/${materialId}`);
}; };
const handleDelete = async (materialId: string) => { const handleDelete = async (materialId: number) => {
const result = await Dialog.confirm({ const result = await Dialog.confirm({
content: "确定要删除这个素材吗?", content: "确定要删除这个素材吗?",
confirmText: "删除", confirmText: "删除",
@@ -217,30 +88,23 @@ const MaterialsList: React.FC = () => {
if (result) { if (result) {
try { try {
const response = await deleteContentItem(materialId); await deleteContentItem(materialId.toString());
if (response.code === 200) { Toast.show({
Toast.show({ content: "删除成功",
content: "删除成功", position: "top",
position: "top", });
}); fetchMaterials();
fetchMaterials(); } catch (error: unknown) {
} else {
Toast.show({
content: response.msg || "删除失败",
position: "top",
});
}
} catch (error: any) {
console.error("删除素材失败:", error); console.error("删除素材失败:", error);
Toast.show({ Toast.show({
content: error?.message || "请检查网络连接", content: error instanceof Error ? error.message : "请检查网络连接",
position: "top", position: "top",
}); });
} }
} }
}; };
const handleView = (materialId: string) => { const handleView = (materialId: number) => {
// 可以跳转到素材详情页面或显示弹窗 // 可以跳转到素材详情页面或显示弹窗
console.log("查看素材:", materialId); console.log("查看素材:", materialId);
}; };
@@ -258,55 +122,215 @@ const MaterialsList: React.FC = () => {
setCurrentPage(page); setCurrentPage(page);
}; };
const filteredMaterials = materials.filter( // 渲染内容类型标签
(material) => const renderContentTypeTag = (contentType: number) => {
material.title?.toLowerCase().includes(searchQuery.toLowerCase()) || const config =
material.content?.toLowerCase().includes(searchQuery.toLowerCase()) contentTypeConfig[contentType as keyof typeof contentTypeConfig];
); if (!config) return null;
const IconComponent = config.icon;
return (
<div
className={style["content-type-tag"]}
style={{ backgroundColor: config.color + "20", color: config.color }}
>
<IconComponent style={{ fontSize: 12, marginRight: 4 }} />
{config.label}
</div>
);
};
// 渲染素材内容预览
const renderContentPreview = (material: ContentItem) => {
const { contentType, content, resUrls, urls, coverImage } = material;
switch (contentType) {
case 1: // 图片
return (
<div className={style["material-image-preview"]}>
{resUrls && resUrls.length > 0 ? (
<div
className={`${style["image-grid"]} ${
resUrls.length === 1
? style.single
: resUrls.length === 2
? style.double
: resUrls.length === 3
? style.triple
: resUrls.length === 4
? style.quad
: style.grid
}`}
>
{resUrls.slice(0, 9).map((url, index) => (
<img key={index} src={url} alt={`图片${index + 1}`} />
))}
{resUrls.length > 9 && (
<div className={style["image-more"]}>
+{resUrls.length - 9}
</div>
)}
</div>
) : coverImage ? (
<div className={`${style["image-grid"]} ${style.single}`}>
<img src={coverImage} alt="封面图" />
</div>
) : (
<div className={style["no-image"]}></div>
)}
</div>
);
case 2: // 链接
return (
<div className={style["material-link-preview"]}>
{urls && urls.length > 0 && (
<div
className={style["link-card"]}
onClick={() => {
window.open(urls[0].url, "_blank");
}}
>
{urls[0].image && (
<div className={style["link-image"]}>
<img src={urls[0].image} alt="链接预览" />
</div>
)}
<div className={style["link-content"]}>
<div className={style["link-title"]}>
{urls[0].desc || "链接"}
</div>
<div className={style["link-url"]}>{urls[0].url}</div>
</div>
</div>
)}
</div>
);
case 3: // 视频
return (
<div className={style["material-video-preview"]}>
{resUrls && resUrls.length > 0 ? (
<div className={style["video-thumbnail"]}>
<video src={resUrls[0]} controls />
</div>
) : (
<div className={style["no-video"]}></div>
)}
</div>
);
case 4: // 文本
return (
<div className={style["material-text-preview"]}>
<div className={style["text-content"]}>
{content.length > 100
? `${content.substring(0, 100)}...`
: content}
</div>
</div>
);
case 5: // 小程序
return (
<div className={style["material-miniprogram-preview"]}>
{resUrls && resUrls.length > 0 && (
<div className={style["miniprogram-card"]}>
<img src={resUrls[0]} alt="小程序封面" />
<div className={style["miniprogram-info"]}>
<div className={style["miniprogram-title"]}>
{material.title || "小程序"}
</div>
</div>
</div>
)}
</div>
);
case 6: // 图文
return (
<div className={style["material-article-preview"]}>
{coverImage && (
<div className={style["article-image"]}>
<img src={coverImage} alt="文章封面" />
</div>
)}
<div className={style["article-content"]}>
<div className={style["article-title"]}>
{material.title || "图文内容"}
</div>
<div className={style["article-text"]}>
{content.length > 80
? `${content.substring(0, 80)}...`
: content}
</div>
</div>
</div>
);
default:
return (
<div className={style["material-default-preview"]}>
<div className={style["default-content"]}>{content}</div>
</div>
);
}
};
return ( return (
<Layout <Layout
header={<NavCommon title={`${library?.name || "内容库"} - 素材管理`} />} header={
<>
<NavCommon
title="素材管理"
right={
<Button type="primary" onClick={handleCreateNew}>
<PlusOutlined />
</Button>
}
/>
{/* 搜索栏 */}
<div className="search-bar">
<div className="search-input-wrapper">
<Input
placeholder="搜索素材内容"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
prefix={<SearchOutlined />}
allowClear
size="large"
/>
</div>
<Button
onClick={handleRefresh}
loading={loading}
icon={<ReloadOutlined />}
size="large"
></Button>
</div>
</>
}
footer={
<div className={style["pagination-wrapper"]}>
<Pagination
current={currentPage}
pageSize={pageSize}
total={total}
onChange={handlePageChange}
showSizeChanger={false}
/>
</div>
}
loading={loading}
> >
<div className={style["materials-page"]}> <div className={style["materials-page"]}>
{/* 搜索和操作栏 */}
<div className={style["search-bar"]}>
<div className={style["search-input-wrapper"]}>
<SearchOutlined className={style["search-icon"]} />
<Input
placeholder="搜索素材..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onPressEnter={handleSearch}
className={style["search-input"]}
/>
</div>
<Button
size="small"
onClick={handleRefresh}
disabled={loading}
className={style["refresh-btn"]}
>
<ReloadOutlined className={loading ? style["spinning"] : ""} />
</Button>
<Button
color="primary"
size="small"
onClick={handleCreateNew}
className={style["create-btn"]}
>
<PlusOutlined />
</Button>
</div>
{/* 素材列表 */} {/* 素材列表 */}
<div className={style["materials-list"]}> <div className={style["materials-list"]}>
{loading ? ( {loading ? (
<div className={style["loading"]}> <div className={style["loading"]}>
<SpinLoading color="primary" style={{ fontSize: 32 }} /> <SpinLoading color="primary" style={{ fontSize: 32 }} />
</div> </div>
) : filteredMaterials.length === 0 ? ( ) : materials.length === 0 ? (
<div className={style["empty-state"]}> <div className={style["empty-state"]}>
<div className={style["empty-icon"]}>📄</div> <div className={style["empty-icon"]}>📄</div>
<div className={style["empty-text"]}> <div className={style["empty-text"]}>
@@ -314,7 +338,6 @@ const MaterialsList: React.FC = () => {
</div> </div>
<Button <Button
color="primary" color="primary"
size="small"
onClick={handleCreateNew} onClick={handleCreateNew}
className={style["empty-btn"]} className={style["empty-btn"]}
> >
@@ -323,59 +346,62 @@ const MaterialsList: React.FC = () => {
</div> </div>
) : ( ) : (
<> <>
{filteredMaterials.map((material) => ( {materials.map(material => (
<Card key={material.id} className={style["material-card"]}> <Card key={material.id} className={style["material-card"]}>
{/* 顶部信息 */}
<div className={style["card-header"]}> <div className={style["card-header"]}>
<div className={style["material-info"]}> <div className={style["avatar-section"]}>
<div className={style["material-title"]}> <div className={style["avatar"]}>
{getContentTypeIcon(material.contentType)} <UserOutlined className={style["avatar-icon"]} />
<span>{material.title || "无标题"}</span>
</div> </div>
<Tag color="blue" className={style["type-tag"]}> <div className={style["header-info"]}>
{getContentTypeText(material.contentType)} <span className={style["creator-name"]}>
</Tag> {material.senderNickname || "系统创建"}
</div>
<CardMenu
onView={() => handleView(material.id)}
onEdit={() => handleEdit(material.id)}
onDelete={() => handleDelete(material.id)}
/>
</div>
<div className={style["card-content"]}>
<div className={style["content-preview"]}>
{material.content?.substring(0, 100)}
{material.content &&
material.content.length > 100 &&
"..."}
</div>
<div className={style["material-meta"]}>
<span className={style["meta-item"]}>
{new Date(material.createTime).toLocaleString("zh-CN")}
</span>
{material.sendTime && (
<span className={style["meta-item"]}>
{new Date(material.sendTime).toLocaleString("zh-CN")}
</span> </span>
)} <span className={style["material-id"]}>
ID: {material.id}
</span>
</div>
</div> </div>
{renderContentTypeTag(material.contentType)}
</div>
{/* 标题 */}
{material.contentType != 4 && (
<div className={style["card-title"]}>
{material.content}
</div>
)}
{/* 内容预览 */}
{renderContentPreview(material)}
{/* 操作按钮区 */}
<div className={style["action-buttons"]}>
<div className={style["action-btn-group"]}>
<Button
onClick={() => handleEdit(material.id)}
className={style["action-btn"]}
>
<EditOutlined />
</Button>
<Button
onClick={() => handleView(material.id)}
className={style["action-btn"]}
>
<BarChartOutlined />
AI改写
</Button>
</div>
<Button
color="danger"
onClick={() => handleDelete(material.id)}
className={style["delete-btn"]}
>
<DeleteOutlined />
</Button>
</div> </div>
</Card> </Card>
))} ))}
{/* 分页 */}
{total > pageSize && (
<div className={style["pagination-wrapper"]}>
<Pagination
total={total}
pageSize={pageSize}
current={currentPage}
onChange={handlePageChange}
className={style["pagination"]}
/>
</div>
)}
</> </>
)} )}
</div> </div>

View File

@@ -1,31 +1,31 @@
import request from '@/api/request'; import request from "@/api/request";
// 设备统计 // 设备统计
export function getDeviceStats() { export function getDeviceStats() {
return request('/v1/dashboard/device-stats', {}, 'GET'); return request("/v1/dashboard/device-stats", {}, "GET");
} }
// 微信号统计 // 微信号统计
export function getWechatStats() { export function getWechatStats() {
return request('/v1/dashboard/wechat-stats', {}, 'GET'); return request("/v1/dashboard/wechat-stats", {}, "GET");
} }
// 今日数据统计 // 今日数据统计
export function getTodayStats() { export function getTodayStats() {
return request('/v1/dashboard/today-stats', {}, 'GET'); return request("/v1/dashboard/today-stats", {}, "GET");
} }
// 首页仪表盘总览 // 首页仪表盘总览
export function getDashboard() { export function getDashboard() {
return request('/v1/dashboard', {}, 'GET'); return request("/v1/dashboard", {}, "GET");
} }
// 获客场景统计 // 获客场景统计
export function getPlanStats(params:any) { export function getPlanStats(params: any) {
return request('/v1/dashboard/plan-stats', params, 'GET'); return request("/v1/dashboard/plan-stats", params, "GET");
} }
// 近七天统计 // 近七天统计
export function getSevenDayStats() { export function getSevenDayStats() {
return request('/v1/dashboard/sevenDay-stats', {}, 'GET'); return request("/v1/dashboard/sevenDay-stats", {}, "GET");
} }

View File

@@ -109,7 +109,7 @@
// Loading状态样式 // Loading状态样式
.stat-card { .stat-card {
.stat-label:empty::before { .stat-label:empty::before {
content: ''; content: "";
display: block; display: block;
width: 60px; width: 60px;
height: 12px; height: 12px;
@@ -120,7 +120,7 @@
.stat-value { .stat-value {
span:empty::before { span:empty::before {
content: ''; content: "";
display: block; display: block;
width: 40px; width: 40px;
height: 20px; height: 20px;
@@ -130,7 +130,7 @@
} }
div:empty::before { div:empty::before {
content: ''; content: "";
display: block; display: block;
width: 20px; width: 20px;
height: 20px; height: 20px;
@@ -142,7 +142,8 @@
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { 0%,
100% {
opacity: 1; opacity: 1;
} }
50% { 50% {
@@ -175,7 +176,7 @@
padding-left: 8px; padding-left: 8px;
&::before { &::before {
content: ''; content: "";
position: absolute; position: absolute;
left: 0; left: 0;
top: 50%; top: 50%;

View File

@@ -199,7 +199,7 @@ const Home: React.FC = () => {
<h2 className={style["section-title"]}></h2> <h2 className={style["section-title"]}></h2>
</div> </div>
<div className={style["scene-grid"]}> <div className={style["scene-grid"]}>
{sceneStats.map((scenario) => ( {sceneStats.map(scenario => (
<div <div
key={scenario.id} key={scenario.id}
className={style["scene-item"]} className={style["scene-item"]}

View File

@@ -85,10 +85,10 @@ const DeviceDetail: React.FC = () => {
checked: boolean checked: boolean
) => { ) => {
if (!id) return; if (!id) return;
setFeatureSaving((prev) => ({ ...prev, [feature]: true })); setFeatureSaving(prev => ({ ...prev, [feature]: true }));
try { try {
await updateDeviceTaskConfig({ deviceId: id, [feature]: checked }); await updateDeviceTaskConfig({ deviceId: id, [feature]: checked });
setDevice((prev) => setDevice(prev =>
prev prev
? { ? {
...prev, ...prev,
@@ -102,7 +102,7 @@ const DeviceDetail: React.FC = () => {
} catch (e: any) { } catch (e: any) {
Toast.show({ content: e.message || "设置失败", position: "top" }); Toast.show({ content: e.message || "设置失败", position: "top" });
} finally { } finally {
setFeatureSaving((prev) => ({ ...prev, [feature]: false })); setFeatureSaving(prev => ({ ...prev, [feature]: false }));
} }
}; };
@@ -199,7 +199,7 @@ const DeviceDetail: React.FC = () => {
}} }}
> >
{["autoAddFriend", "autoReply", "momentsSync", "aiChat"].map( {["autoAddFriend", "autoReply", "momentsSync", "aiChat"].map(
(f) => ( f => (
<div <div
key={f} key={f}
style={{ style={{
@@ -216,7 +216,7 @@ const DeviceDetail: React.FC = () => {
!!device.features?.[f as keyof Device["features"]] !!device.features?.[f as keyof Device["features"]]
} }
loading={!!featureSaving[f]} loading={!!featureSaving[f]}
onChange={(checked) => onChange={checked =>
handleFeatureChange( handleFeatureChange(
f as keyof Device["features"], f as keyof Device["features"],
checked checked
@@ -254,7 +254,7 @@ const DeviceDetail: React.FC = () => {
<div <div
style={{ display: "flex", flexDirection: "column", gap: 12 }} style={{ display: "flex", flexDirection: "column", gap: 12 }}
> >
{accounts.map((acc) => ( {accounts.map(acc => (
<div <div
key={acc.id} key={acc.id}
style={{ style={{
@@ -334,7 +334,7 @@ const DeviceDetail: React.FC = () => {
<div <div
style={{ display: "flex", flexDirection: "column", gap: 12 }} style={{ display: "flex", flexDirection: "column", gap: 12 }}
> >
{logs.map((log) => ( {logs.map(log => (
<div <div
key={log.id} key={log.id}
style={{ style={{

View File

@@ -57,7 +57,7 @@ const Devices: React.FC = () => {
if (search) params.keyword = search; if (search) params.keyword = search;
const res = await fetchDeviceList(params); const res = await fetchDeviceList(params);
const list = Array.isArray(res.list) ? res.list : []; const list = Array.isArray(res.list) ? res.list : [];
setDevices((prev) => (reset ? list : [...prev, ...list])); setDevices(prev => (reset ? list : [...prev, ...list]));
setTotal(res.total || 0); setTotal(res.total || 0);
setHasMore(list.length === 20); setHasMore(list.length === 20);
if (reset) setPage(1); if (reset) setPage(1);
@@ -81,9 +81,9 @@ const Devices: React.FC = () => {
useEffect(() => { useEffect(() => {
if (!hasMore || loading) return; if (!hasMore || loading) return;
const observer = new window.IntersectionObserver( const observer = new window.IntersectionObserver(
(entries) => { entries => {
if (entries[0].isIntersecting && hasMore && !loading) { if (entries[0].isIntersecting && hasMore && !loading) {
setPage((p) => p + 1); setPage(p => p + 1);
} }
}, },
{ threshold: 0.5 } { threshold: 0.5 }
@@ -100,7 +100,7 @@ const Devices: React.FC = () => {
}, [page]); }, [page]);
// 状态筛选 // 状态筛选
const filtered = devices.filter((d) => { const filtered = devices.filter(d => {
if (status === "all") return true; if (status === "all") return true;
if (status === "online") return d.status === "online" || d.alive === 1; if (status === "online") return d.status === "online" || d.alive === 1;
if (status === "offline") return d.status === "offline" || d.alive === 0; if (status === "offline") return d.status === "offline" || d.alive === 0;
@@ -222,7 +222,7 @@ const Devices: React.FC = () => {
<Input <Input
placeholder="搜索设备IMEI/备注" placeholder="搜索设备IMEI/备注"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
allowClear allowClear
style={{ flex: 1 }} style={{ flex: 1 }}
@@ -238,7 +238,7 @@ const Devices: React.FC = () => {
<div style={{ display: "flex", gap: 8 }}> <div style={{ display: "flex", gap: 8 }}>
<Tabs <Tabs
activeKey={status} activeKey={status}
onChange={(k) => setStatus(k as any)} onChange={k => setStatus(k as any)}
style={{ flex: 1 }} style={{ flex: 1 }}
> >
<Tabs.Tab title="全部" key="all" /> <Tabs.Tab title="全部" key="all" />
@@ -277,7 +277,7 @@ const Devices: React.FC = () => {
<div style={{ padding: 12 }}> <div style={{ padding: 12 }}>
{/* 设备列表 */} {/* 设备列表 */}
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}> <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
{filtered.map((device) => ( {filtered.map(device => (
<div <div
key={device.id} key={device.id}
style={{ style={{
@@ -296,15 +296,15 @@ const Devices: React.FC = () => {
> >
<Checkbox <Checkbox
checked={selected.includes(device.id)} checked={selected.includes(device.id)}
onChange={(e) => { onChange={e => {
e.stopPropagation(); e.stopPropagation();
setSelected((prev) => setSelected(prev =>
e.target.checked e.target.checked
? [...prev, device.id!] ? [...prev, device.id!]
: prev.filter((id) => id !== device.id) : prev.filter(id => id !== device.id)
); );
}} }}
onClick={(e) => e.stopPropagation()} onClick={e => e.stopPropagation()}
style={{ marginRight: 12 }} style={{ marginRight: 12 }}
/> />
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
@@ -404,13 +404,13 @@ const Devices: React.FC = () => {
<Input <Input
placeholder="设备名称" placeholder="设备名称"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={e => setName(e.target.value)}
allowClear allowClear
/> />
<Input <Input
placeholder="设备IMEI" placeholder="设备IMEI"
value={imei} value={imei}
onChange={(e) => setImei(e.target.value)} onChange={e => setImei(e.target.value)}
allowClear allowClear
/> />
<Button <Button

View File

@@ -4,7 +4,7 @@
.user-card { .user-card {
border-radius: 16px; border-radius: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin: 16px 0 12px 0; margin: 16px 0 12px 0;
padding: 0 0 0 0; padding: 0 0 0 0;
} }
@@ -89,7 +89,7 @@
border-radius: 8px; border-radius: 8px;
} }
.icon-setting{ .icon-setting {
font-size: 26px; font-size: 26px;
color: #666; color: #666;
position: absolute; position: absolute;
@@ -167,7 +167,6 @@
} }
} }
.logout-btn { .logout-btn {
border-radius: 8px; border-radius: 8px;
height: 48px; height: 48px;

View File

@@ -227,7 +227,7 @@ const Mine: React.FC = () => {
{/* 我的功能 */} {/* 我的功能 */}
<Card className={style["menu-card"]}> <Card className={style["menu-card"]}>
<List> <List>
{functionModules.map((module) => ( {functionModules.map(module => (
<List.Item <List.Item
key={module.id} key={module.id}
prefix={renderModuleIcon(module)} prefix={renderModuleIcon(module)}

View File

@@ -25,7 +25,7 @@ const Recharge: React.FC = () => {
} }
setLoading(true); setLoading(true);
setTimeout(() => { setTimeout(() => {
setBalance((b) => b + selected); setBalance(b => b + selected);
Toast.show({ content: `充值成功,已到账¥${selected}` }); Toast.show({ content: `充值成功,已到账¥${selected}` });
setLoading(false); setLoading(false);
}, 1200); }, 1200);
@@ -48,7 +48,7 @@ const Recharge: React.FC = () => {
<Card className={style["quick-card"]}> <Card className={style["quick-card"]}>
<div className={style["quick-title"]}></div> <div className={style["quick-title"]}></div>
<div className={style["quick-list"]}> <div className={style["quick-list"]}>
{quickAmounts.map((amt) => ( {quickAmounts.map(amt => (
<Button <Button
key={amt} key={amt}
color={selected === amt ? "primary" : "default"} color={selected === amt ? "primary" : "default"}

View File

@@ -1,5 +1,5 @@
import request from "@/api/request"; import request from "@/api/request";
export function getTrafficPoolDetail(wechatId: string): Promise<any> { export function getTrafficPoolDetail(id: string): Promise<any> {
return request("/v1/wechats/getWechatInfo", { wechatId }, "GET"); return request("/v1/workbench/detail", { id }, "GET");
} }

View File

@@ -1,46 +1,32 @@
// 用户详情类型 // 用户详情类型
export interface TrafficPoolUserDetail { export interface TrafficPoolUserDetail {
userInfo: { id: number;
wechatId: string; nickname: string;
weight: number | null; avatar: string;
activity: { wechatId: string;
totalMsgCount: number; status: number | string;
sevenDayMsgCount: number; addTime: string;
thirtyDayMsgCount: number; lastInteraction: string;
yesterdayMsgCount: number; deviceName?: string;
}; wechatAccountName?: string;
friendShip: { customerServiceName?: string;
maleFriend: number; poolNames?: string[];
groupNumber: number; rfmScore?: {
totalFriend: number; recency: number;
femaleFriend: number; frequency: number;
unknowFriend: number; monetary: number;
}; segment?: string;
nickname: string;
alias: string;
avatar: string;
gender: number; // 0-未知, 1-男, 2-女
}; };
accountAge: string; totalSpent?: number;
activityLevel: { interactionCount?: number;
allTimes: number; conversionRate?: number;
dayTimes: number; tags?: string[];
}; packages?: string[];
accountWeight: { interactions?: Array<{
ageWeight: number; id: string;
activityWeigth: number; type: string;
restrictWeight: number; content: string;
realNameWeight: number; timestamp: string;
scope: number; value?: number;
};
statistics: {
todayAdded: number;
addLimit: number;
};
restrictions: Array<{
id: number;
date: number | null;
level: number; // 1-轻微, 2-中等, 3-严重
reason: string;
}>; }>;
} }

View File

@@ -1,376 +1,297 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom"; import { useParams, useNavigate } from "react-router-dom";
import { Card, Button, Avatar, Tag, Tabs, List, Badge } from "antd-mobile";
import {
UserOutlined,
MessageOutlined,
TeamOutlined,
ClockCircleOutlined,
ExclamationCircleOutlined,
PlusOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout"; import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon";
import { getTrafficPoolDetail } from "./api"; import { getTrafficPoolDetail } from "./api";
import type { TrafficPoolUserDetail } from "./data"; import type { TrafficPoolUserDetail } from "./data";
import styles from "./index.module.scss"; import { Card, Button, Avatar, Tag, Spin } from "antd";
const tabList = [
{ key: "base", label: "基本信息" },
{ key: "journey", label: "用户旅程" },
{ key: "tags", label: "用户标签" },
];
const TrafficPoolDetail: React.FC = () => { const TrafficPoolDetail: React.FC = () => {
const { id } = useParams(); const { id } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [user, setUser] = useState<TrafficPoolUserDetail | null>(null); const [user, setUser] = useState<TrafficPoolUserDetail | null>(null);
const [activeTab, setActiveTab] = useState<"base" | "journey" | "tags">(
"base"
);
useEffect(() => { useEffect(() => {
if (!id) return; if (!id) return;
setLoading(true); setLoading(true);
getTrafficPoolDetail(id as string) getTrafficPoolDetail(id as string)
.then((res) => setUser(res)) .then(res => setUser(res))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, [id]); }, [id]);
const getGenderText = (gender: number) => { if (loading) {
switch (gender) { return (
case 1: <Layout>
return "男"; <div style={{ textAlign: "center", padding: "64px 0" }}>
case 2: <Spin size="large" />
return "女"; </div>
default: </Layout>
return "未知"; );
} }
};
const getGenderColor = (gender: number) => {
switch (gender) {
case 1:
return "#1677ff";
case 2:
return "#eb2f96";
default:
return "#999";
}
};
const getRestrictionLevelText = (level: number) => {
switch (level) {
case 1:
return "轻微";
case 2:
return "中等";
case 3:
return "严重";
default:
return "未知";
}
};
const getRestrictionLevelColor = (level: number) => {
switch (level) {
case 1:
return "warning";
case 2:
return "danger";
case 3:
return "danger";
default:
return "default";
}
};
const formatDate = (timestamp: number | null) => {
if (!timestamp) return "--";
try {
const date = new Date(timestamp * 1000);
return date.toLocaleDateString("zh-CN");
} catch (error) {
return "--";
}
};
const formatAccountAge = (dateString: string) => {
if (!dateString) return "--";
try {
const date = new Date(dateString);
return date.toLocaleDateString("zh-CN");
} catch (error) {
return dateString;
}
};
if (!user) { if (!user) {
return ( return (
<Layout header={<NavCommon title="用户详情" />} loading={loading}> <Layout>
<div className={styles.emptyState}> <div style={{ textAlign: "center", color: "#aaa", padding: "64px 0" }}>
<div className={styles.emptyText}></div>
</div> </div>
</Layout> </Layout>
); );
} }
return ( return (
<Layout header={<NavCommon title="用户详情" />} loading={loading}> <Layout
<div className={styles.container}> header={
{/* 用户基本信息 */} <div
<Card className={styles.userCard}> style={{
<div className={styles.userInfo}> display: "flex",
<Avatar alignItems: "center",
src={user.userInfo.avatar} height: 48,
className={styles.avatar} borderBottom: "1px solid #eee",
fallback={<UserOutlined />} background: "#fff",
/> }}
<div className={styles.userDetails}> >
<div className={styles.nickname}>{user.userInfo.nickname}</div> <Button
<div className={styles.wechatId}>{user.userInfo.wechatId}</div> type="link"
<div className={styles.alias}>{user.userInfo.alias}</div> onClick={() => navigate(-1)}
<div className={styles.tags}> style={{ marginRight: 8 }}
<Tag >
color="primary" &lt;
fill="outline" </Button>
className={styles.genderTag} <div style={{ fontWeight: 600, fontSize: 18 }}></div>
style={{ color: getGenderColor(user.userInfo.gender) }} </div>
> }
{getGenderText(user.userInfo.gender)} >
<div style={{ padding: 16 }}>
{/* 顶部信息 */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 16,
marginBottom: 16,
}}
>
<Avatar src={user.avatar} size={64} />
<div>
<div style={{ fontSize: 20, fontWeight: 600 }}>{user.nickname}</div>
<div style={{ color: "#1677ff", fontSize: 14, margin: "4px 0" }}>
{user.wechatId}
</div>
{user.packages &&
user.packages.length > 0 &&
user.packages.map(pkg => (
<Tag color="purple" key={pkg} style={{ marginRight: 4 }}>
{pkg}
</Tag> </Tag>
{user.userInfo.weight && ( ))}
<Tag
color="success"
fill="outline"
className={styles.weightTag}
>
: {user.userInfo.weight}
</Tag>
)}
</div>
</div>
</div> </div>
</Card> </div>
{/* Tab栏 */}
<div
style={{
display: "flex",
gap: 24,
borderBottom: "1px solid #eee",
marginBottom: 16,
}}
>
{tabList.map(tab => (
<div
key={tab.key}
style={{
padding: "8px 0",
fontWeight: 500,
color: activeTab === tab.key ? "#1677ff" : "#888",
borderBottom:
activeTab === tab.key ? "2px solid #1677ff" : "none",
cursor: "pointer",
fontSize: 16,
}}
onClick={() => setActiveTab(tab.key as any)}
>
{tab.label}
</div>
))}
</div>
{/* Tab内容 */} {/* Tab内容 */}
<Tabs className={styles.tabs}> {activeTab === "base" && (
<Tabs.Tab title="基本信息" key="base"> <>
<div className={styles.tabContent}> <Card style={{ marginBottom: 16 }} title="关键信息">
{/* 账户信息 */} <div style={{ display: "flex", flexWrap: "wrap", gap: 24 }}>
<Card title="账户信息" className={styles.infoCard}> <div>{user.deviceName || "--"}</div>
<List> <div>{user.wechatAccountName || "--"}</div>
<List.Item extra={formatAccountAge(user.accountAge)}> <div>{user.customerServiceName || "--"}</div>
<div>{user.addTime || "--"}</div>
</List.Item> <div>{user.lastInteraction || "--"}</div>
<List.Item </div>
extra={`${user.statistics.todayAdded}/${user.statistics.addLimit}`} </Card>
<Card style={{ marginBottom: 16 }} title="RFM评分">
<div style={{ display: "flex", gap: 32 }}>
<div>
<div
style={{ fontSize: 20, fontWeight: 600, color: "#1677ff" }}
> >
{user.rfmScore?.recency ?? "-"}
</List.Item>
<List.Item extra={user.activityLevel.allTimes}>
</List.Item>
<List.Item extra={user.activityLevel.dayTimes}>
</List.Item>
</List>
</Card>
{/* 好友统计 */}
<Card title="好友统计" className={styles.infoCard}>
<div className={styles.statsGrid}>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#1677ff" }}
>
{user.userInfo.friendShip.totalFriend}
</div>
<div className={styles.statLabel}></div>
</div> </div>
<div className={styles.statItem}> <div style={{ fontSize: 12, color: "#888" }}>(R)</div>
<div </div>
className={styles.statValue} <div>
style={{ color: "#1677ff" }} <div
> style={{ fontSize: 20, fontWeight: 600, color: "#52c41a" }}
{user.userInfo.friendShip.maleFriend} >
</div> {user.rfmScore?.frequency ?? "-"}
<div className={styles.statLabel}></div>
</div> </div>
<div className={styles.statItem}> <div style={{ fontSize: 12, color: "#888" }}>(F)</div>
<div </div>
className={styles.statValue} <div>
style={{ color: "#eb2f96" }} <div
> style={{ fontSize: 20, fontWeight: 600, color: "#eb2f96" }}
{user.userInfo.friendShip.femaleFriend} >
</div> {user.rfmScore?.monetary ?? "-"}
<div className={styles.statLabel}></div>
</div> </div>
<div className={styles.statItem}> <div style={{ fontSize: 12, color: "#888" }}>(M)</div>
<div className={styles.statValue} style={{ color: "#999" }}> </div>
{user.userInfo.friendShip.unknowFriend} </div>
</div> </Card>
<div className={styles.statLabel}></div> <Card style={{ marginBottom: 16 }} title="统计数据">
<div style={{ display: "flex", gap: 32 }}>
<div>
<div
style={{ fontSize: 18, fontWeight: 600, color: "#52c41a" }}
>
¥{user.totalSpent ?? "-"}
</div> </div>
<div className={styles.statItem}> <div style={{ fontSize: 12, color: "#888" }}></div>
<div </div>
className={styles.statValue} <div>
style={{ color: "#52c41a" }} <div
> style={{ fontSize: 18, fontWeight: 600, color: "#1677ff" }}
{user.userInfo.friendShip.groupNumber} >
{user.interactionCount ?? "-"}
</div>
<div style={{ fontSize: 12, color: "#888" }}></div>
</div>
<div>
<div
style={{ fontSize: 18, fontWeight: 600, color: "#faad14" }}
>
{user.conversionRate ?? "-"}
</div>
<div style={{ fontSize: 12, color: "#888" }}></div>
</div>
<div>
<div
style={{ fontSize: 18, fontWeight: 600, color: "#ff4d4f" }}
>
{user.status === "failed"
? "添加失败"
: user.status === "added"
? "添加成功"
: "未添加"}
</div>
<div style={{ fontSize: 12, color: "#888" }}></div>
</div>
</div>
</Card>
</>
)}
{activeTab === "journey" && (
<Card title="互动记录">
{user.interactions && user.interactions.length > 0 ? (
user.interactions.slice(0, 4).map(it => (
<div
key={it.id}
style={{
display: "flex",
alignItems: "center",
gap: 12,
borderBottom: "1px solid #f0f0f0",
padding: "12px 0",
}}
>
<div style={{ fontSize: 22 }}>
{it.type === "click" && "📱"}
{it.type === "message" && "💬"}
{it.type === "purchase" && "💲"}
{it.type === "view" && "👁️"}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 500 }}>
{it.type === "click" && "点击行为"}
{it.type === "message" && "消息互动"}
{it.type === "purchase" && "购买行为"}
{it.type === "view" && "页面浏览"}
</div> </div>
<div className={styles.statLabel}></div> <div style={{ color: "#888", fontSize: 13 }}>
{it.content}
{it.type === "purchase" && it.value && (
<span
style={{
color: "#52c41a",
fontWeight: 600,
marginLeft: 4,
}}
>
¥{it.value}
</span>
)}
</div>
</div>
<div
style={{
fontSize: 12,
color: "#aaa",
whiteSpace: "nowrap",
}}
>
{it.timestamp}
</div> </div>
</div> </div>
</Card> ))
) : (
{/* 活跃度统计 */} <div
<Card title="活跃度统计" className={styles.infoCard}> style={{
<div className={styles.statsGrid}> color: "#aaa",
<div className={styles.statItem}> textAlign: "center",
<div padding: "24px 0",
className={styles.statValue} }}
style={{ color: "#52c41a" }} >
>
{user.userInfo.activity.totalMsgCount} </div>
</div> )}
<div className={styles.statLabel}></div> </Card>
</div> )}
<div className={styles.statItem}> {activeTab === "tags" && (
<div <Card title="用户标签">
className={styles.statValue} <div style={{ marginBottom: 12 }}>
style={{ color: "#faad14" }} {user.tags && user.tags.length > 0 ? (
> user.tags.map(tag => (
{user.userInfo.activity.sevenDayMsgCount} <Tag
</div> key={tag}
<div className={styles.statLabel}>7</div> color="blue"
</div> style={{ marginRight: 8, marginBottom: 8 }}
<div className={styles.statItem}> >
<div {tag}
className={styles.statValue} </Tag>
style={{ color: "#722ed1" }} ))
> ) : (
{user.userInfo.activity.thirtyDayMsgCount} <span style={{ color: "#aaa" }}></span>
</div> )}
<div className={styles.statLabel}>30</div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#13c2c2" }}
>
{user.userInfo.activity.yesterdayMsgCount}
</div>
<div className={styles.statLabel}></div>
</div>
</div>
</Card>
{/* 账户权重 */}
<Card title="账户权重" className={styles.infoCard}>
<div className={styles.statsGrid}>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#1677ff" }}
>
{user.accountWeight.ageWeight}
</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#52c41a" }}
>
{user.accountWeight.activityWeigth}
</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#faad14" }}
>
{user.accountWeight.restrictWeight}
</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#722ed1" }}
>
{user.accountWeight.realNameWeight}
</div>
<div className={styles.statLabel}></div>
</div>
<div className={styles.statItem}>
<div
className={styles.statValue}
style={{ color: "#13c2c2" }}
>
{user.accountWeight.scope}
</div>
<div className={styles.statLabel}></div>
</div>
</div>
</Card>
</div> </div>
</Tabs.Tab> <Button type="dashed" block>
<Tabs.Tab title="限制记录" key="restrictions"> </Button>
<div className={styles.tabContent}> </Card>
<Card title="限制记录" className={styles.infoCard}> )}
{user.restrictions && user.restrictions.length > 0 ? (
<List>
{user.restrictions.map((restriction) => (
<List.Item
key={restriction.id}
prefix={
<ExclamationCircleOutlined
style={{ color: "#ff4d4f" }}
/>
}
title={
<div className={styles.restrictionTitle}>
<span>{restriction.reason || "未知原因"}</span>
<Tag
color={getRestrictionLevelColor(
restriction.level
)}
fill="outline"
className={styles.restrictionLevel}
>
{getRestrictionLevelText(restriction.level)}
</Tag>
</div>
}
description={
<div className={styles.restrictionContent}>
<span>ID: {restriction.id}</span>
{restriction.date && (
<span>
: {formatDate(restriction.date)}
</span>
)}
</div>
}
/>
))}
</List>
) : (
<div className={styles.emptyState}>
<div className={styles.emptyText}></div>
</div>
)}
</Card>
</div>
</Tabs.Tab>
<Tabs.Tab title="操作记录" key="actions">
<div className={styles.tabContent}>
<Card title="操作记录" className={styles.infoCard}>
<div className={styles.emptyState}>
<div className={styles.emptyText}></div>
</div>
</Card>
</div>
</Tabs.Tab>
</Tabs>
</div> </div>
</Layout> </Layout>
); );

View File

@@ -33,9 +33,9 @@ const BatchAddModal: React.FC<BatchAddModalProps> = ({
<div style={{ marginBottom: 12 }}> <div style={{ marginBottom: 12 }}>
<div></div> <div></div>
<Selector <Selector
options={packageOptions.map((p) => ({ label: p.name, value: p.id }))} options={packageOptions.map(p => ({ label: p.name, value: p.id }))}
value={[batchTarget]} value={[batchTarget]}
onChange={(v) => setBatchTarget(v[0])} onChange={v => setBatchTarget(v[0])}
/> />
</div> </div>
<div style={{ color: "#888", fontSize: 13 }}> <div style={{ color: "#888", fontSize: 13 }}>

View File

@@ -70,7 +70,7 @@ const FilterModal: React.FC<FilterModalProps> = ({
onChange={setDeviceId} onChange={setDeviceId}
options={[ options={[
{ label: "全部设备", value: "all" }, { label: "全部设备", value: "all" },
...deviceOptions.map((d) => ({ label: d.name, value: d.id })), ...deviceOptions.map(d => ({ label: d.name, value: d.id })),
]} ]}
/> />
</div> </div>
@@ -82,7 +82,7 @@ const FilterModal: React.FC<FilterModalProps> = ({
onChange={setPackageId} onChange={setPackageId}
options={[ options={[
{ label: "全部流量池", value: "all" }, { label: "全部流量池", value: "all" },
...packageOptions.map((p) => ({ label: p.name, value: p.id })), ...packageOptions.map(p => ({ label: p.name, value: p.id })),
]} ]}
/> />
</div> </div>
@@ -91,7 +91,7 @@ const FilterModal: React.FC<FilterModalProps> = ({
<Select <Select
style={{ width: "100%" }} style={{ width: "100%" }}
value={valueLevel} value={valueLevel}
onChange={(v) => setValueLevel(v as ValueLevel)} onChange={v => setValueLevel(v as ValueLevel)}
options={valueLevelOptions} options={valueLevelOptions}
/> />
</div> </div>
@@ -100,7 +100,7 @@ const FilterModal: React.FC<FilterModalProps> = ({
<Select <Select
style={{ width: "100%" }} style={{ width: "100%" }}
value={userStatus} value={userStatus}
onChange={(v) => setUserStatus(v as UserStatus)} onChange={v => setUserStatus(v as UserStatus)}
options={statusOptions} options={statusOptions}
/> />
</div> </div>

View File

@@ -39,12 +39,10 @@ export function useTrafficPoolListLogic() {
const [showStats, setShowStats] = useState(false); const [showStats, setShowStats] = useState(false);
const stats = useMemo(() => { const stats = useMemo(() => {
const total = list.length; const total = list.length;
const highValue = list.filter((u) => const highValue = list.filter(u => u.tags.includes("高价值客户池")).length;
u.tags.includes("高价值客户池") const added = list.filter(u => u.status === 1).length;
).length; const pending = list.filter(u => u.status === 0).length;
const added = list.filter((u) => u.status === 1).length; const failed = list.filter(u => u.status === -1).length;
const pending = list.filter((u) => u.status === 0).length;
const failed = list.filter((u) => u.status === -1).length;
const addSuccessRate = total ? Math.round((added / total) * 100) : 0; const addSuccessRate = total ? Math.round((added / total) * 100) : 0;
return { total, highValue, added, pending, failed, addSuccessRate }; return { total, highValue, added, pending, failed, addSuccessRate };
}, [list]); }, [list]);
@@ -84,15 +82,15 @@ export function useTrafficPoolListLogic() {
// 全选/反选 // 全选/反选
const handleSelectAll = (checked: boolean) => { const handleSelectAll = (checked: boolean) => {
if (checked) { if (checked) {
setSelectedIds(list.map((item) => item.id)); setSelectedIds(list.map(item => item.id));
} else { } else {
setSelectedIds([]); setSelectedIds([]);
} }
}; };
// 单选 // 单选
const handleSelect = (id: number, checked: boolean) => { const handleSelect = (id: number, checked: boolean) => {
setSelectedIds((prev) => setSelectedIds(prev =>
checked ? [...prev, id] : prev.filter((i) => i !== id) checked ? [...prev, id] : prev.filter(i => i !== id)
); );
}; };
@@ -104,7 +102,7 @@ export function useTrafficPoolListLogic() {
} }
// TODO: 调用后端批量接口,这里仅模拟 // TODO: 调用后端批量接口,这里仅模拟
Toast.show({ Toast.show({
content: `已将${selectedIds.length}个用户加入${packageOptions.find((p) => p.id === batchTarget)?.name || ""}`, content: `已将${selectedIds.length}个用户加入${packageOptions.find(p => p.id === batchTarget)?.name || ""}`,
position: "top", position: "top",
}); });
setBatchModal(false); setBatchModal(false);

View File

@@ -1,23 +1,23 @@
.listWrap { .listWrap {
padding: 12px; padding: 12px;
} }
.cardContent{ .cardContent {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
position: relative; position: relative;
} }
.checkbox{ .checkbox {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
} }
.cardWrap{ .cardWrap {
background: #fff; background: #fff;
padding: 16px; padding: 16px;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
margin-bottom: 12px; margin-bottom: 12px;
} }

View File

@@ -79,7 +79,7 @@ const TrafficPoolList: React.FC = () => {
title="流量池用户列表" title="流量池用户列表"
right={ right={
<Button <Button
onClick={() => setShowStats((s) => !s)} onClick={() => setShowStats(s => !s)}
style={{ marginLeft: 8 }} style={{ marginLeft: 8 }}
> >
<BarChartOutlined /> {showStats ? "收起分析" : "数据分析"} <BarChartOutlined /> {showStats ? "收起分析" : "数据分析"}
@@ -92,7 +92,7 @@ const TrafficPoolList: React.FC = () => {
<Input <Input
placeholder="搜索计划名称" placeholder="搜索计划名称"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
allowClear allowClear
size="large" size="large"
@@ -124,7 +124,7 @@ const TrafficPoolList: React.FC = () => {
> >
<Checkbox <Checkbox
checked={selectedIds.length === list.length && list.length > 0} checked={selectedIds.length === list.length && list.length > 0}
onChange={(e) => handleSelectAll(e.target.checked)} onChange={e => handleSelectAll(e.target.checked)}
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
/> />
<span></span> <span></span>
@@ -186,7 +186,7 @@ const TrafficPoolList: React.FC = () => {
<Empty description="暂无数据" /> <Empty description="暂无数据" />
) : ( ) : (
<div> <div>
{list.map((item) => ( {list.map(item => (
<div key={item.id} className={styles.cardWrap}> <div key={item.id} className={styles.cardWrap}>
<div <div
className={styles.card} className={styles.card}
@@ -198,9 +198,9 @@ const TrafficPoolList: React.FC = () => {
<div className={styles.cardContent}> <div className={styles.cardContent}>
<Checkbox <Checkbox
checked={selectedIds.includes(item.id)} checked={selectedIds.includes(item.id)}
onChange={(e) => handleSelect(item.id, e.target.checked)} onChange={e => handleSelect(item.id, e.target.checked)}
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
onClick={(e) => e.stopPropagation()} onClick={e => e.stopPropagation()}
className={styles.checkbox} className={styles.checkbox}
/> />
<Avatar <Avatar

View File

@@ -4,7 +4,7 @@
.user-card { .user-card {
margin: 18px 16px 0 16px; margin: 18px 16px 0 16px;
border-radius: 14px; border-radius: 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
} }
.user-info { .user-info {
display: flex; display: flex;
@@ -73,7 +73,7 @@
overflow: hidden; overflow: hidden;
background: #f5f5f5; background: #f5f5f5;
transition: box-shadow 0.2s; transition: box-shadow 0.2s;
box-shadow: 0 2px 8px rgba(0,0,0,0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
} }
.avatar-upload img { .avatar-upload img {
width: 100%; width: 100%;
@@ -86,7 +86,7 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: rgba(0,0,0,0.45); background: rgba(0, 0, 0, 0.45);
color: #fff; color: #fff;
font-size: 13px; font-size: 13px;
text-align: center; text-align: center;
@@ -110,6 +110,6 @@
background: #fafbfc; background: #fafbfc;
} }
.save-btn { .save-btn {
padding: 12px; padding: 12px;
background: #fff; background: #fff;
} }

View File

@@ -19,7 +19,7 @@ const UserSetting: React.FC = () => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (!file) return; if (!file) return;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (ev) => { reader.onload = ev => {
setAvatar(ev.target?.result as string); setAvatar(ev.target?.result as string);
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);

View File

@@ -30,23 +30,23 @@
border: 4px solid #e8f4fd; border: 4px solid #e8f4fd;
} }
.status-dot { .status-dot {
position: absolute; position: absolute;
bottom: 2px; bottom: 2px;
right: 2px; right: 2px;
width: 16px; width: 16px;
height: 16px; height: 16px;
border-radius: 50%; border-radius: 50%;
border: 2px solid #fff; border: 2px solid #fff;
&.status-normal { &.status-normal {
background: #52c41a; background: #52c41a;
} }
&.status-abnormal { &.status-abnormal {
background: #ff4d4f; background: #ff4d4f;
} }
} }
} }
.info-section { .info-section {
@@ -661,7 +661,7 @@
border-radius: 10px; border-radius: 10px;
padding: 16px 0 8px 0; padding: 16px 0 8px 0;
text-align: center; text-align: center;
box-shadow: 0 1px 2px rgba(0,0,0,0.03); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
} }
.summary-value { .summary-value {
font-size: 24px; font-size: 24px;
@@ -719,7 +719,7 @@
border-radius: 10px; border-radius: 10px;
padding: 16px; padding: 16px;
margin-top: 12px; margin-top: 12px;
box-shadow: 0 1px 2px rgba(0,0,0,0.03); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
} }
.device-title { .device-title {
font-size: 15px; font-size: 15px;

View File

@@ -414,7 +414,7 @@ const WechatAccountDetail: React.FC = () => {
</div> </div>
) : ( ) : (
<> <>
{friends.map((friend) => ( {friends.map(friend => (
<div key={friend.id} className={style["friend-item"]}> <div key={friend.id} className={style["friend-item"]}>
<Avatar <Avatar
src={friend.avatar} src={friend.avatar}
@@ -490,7 +490,7 @@ const WechatAccountDetail: React.FC = () => {
<p className={style["popup-description"]}>24</p> <p className={style["popup-description"]}>24</p>
{accountSummary && accountSummary.restrictions && ( {accountSummary && accountSummary.restrictions && (
<div className={style["restrictions-detail"]}> <div className={style["restrictions-detail"]}>
{accountSummary.restrictions.map((restriction) => ( {accountSummary.restrictions.map(restriction => (
<div <div
key={restriction.id} key={restriction.id}
className={style["restriction-detail-item"]} className={style["restriction-detail-item"]}

View File

@@ -17,13 +17,13 @@
.account-card { .account-card {
background: #fff; background: #fff;
border-radius: 14px; border-radius: 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 14px 14px 10px 14px; padding: 14px 14px 10px 14px;
transition: box-shadow 0.2s; transition: box-shadow 0.2s;
cursor: pointer; cursor: pointer;
border: 1px solid #f0f0f0; border: 1px solid #f0f0f0;
&:hover { &:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.10); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
border-color: #e6f7ff; border-color: #e6f7ff;
} }
} }
@@ -155,7 +155,7 @@
padding: 16px 0 8px 0; padding: 16px 0 8px 0;
} }
.popup-content img { .popup-content img {
box-shadow: 0 2px 8px rgba(0,0,0,0.08); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
} }
.loading { .loading {
display: flex; display: flex;

View File

@@ -93,7 +93,7 @@ const WechatAccounts: React.FC = () => {
<Input <Input
placeholder="搜索微信号/昵称" placeholder="搜索微信号/昵称"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={e => setSearchTerm(e.target.value)}
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
allowClear allowClear
size="large" size="large"
@@ -121,7 +121,7 @@ const WechatAccounts: React.FC = () => {
<div className={style["empty"]}></div> <div className={style["empty"]}></div>
) : ( ) : (
<div className={style["card-list"]}> <div className={style["card-list"]}>
{accounts.map((account) => { {accounts.map(account => {
const percent = const percent =
account.times > 0 account.times > 0
? Math.min((account.addedCount / account.times) * 100, 100) ? Math.min((account.addedCount / account.times) * 100, 100)

View File

@@ -1,26 +1,26 @@
import request from '@/api/request'; import request from "@/api/request";
// 获取场景列表 // 获取场景列表
export function getScenarios(params: any) { export function getScenarios(params: any) {
return request('/v1/plan/scenes', params, 'GET'); return request("/v1/plan/scenes", params, "GET");
} }
// 获取场景详情 // 获取场景详情
export function getScenarioDetail(id: string) { export function getScenarioDetail(id: string) {
return request(`/v1/scenarios/${id}`, {}, 'GET'); return request(`/v1/scenarios/${id}`, {}, "GET");
} }
// 创建场景 // 创建场景
export function createScenario(data: any) { export function createScenario(data: any) {
return request('/v1/scenarios', data, 'POST'); return request("/v1/scenarios", data, "POST");
} }
// 更新场景 // 更新场景
export function updateScenario(id: string, data: any) { export function updateScenario(id: string, data: any) {
return request(`/v1/scenarios/${id}`, data, 'PUT'); return request(`/v1/scenarios/${id}`, data, "PUT");
} }
// 删除场景 // 删除场景
export function deleteScenario(id: string) { export function deleteScenario(id: string) {
return request(`/v1/scenarios/${id}`, {}, 'DELETE'); return request(`/v1/scenarios/${id}`, {}, "DELETE");
} }

View File

@@ -152,12 +152,14 @@
.scenario-card { .scenario-card {
background: #fff; background: #fff;
border-radius: 16px; border-radius: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: box-shadow 0.2s, transform 0.2s; transition:
box-shadow 0.2s,
transform 0.2s;
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
&:hover { &:hover {
box-shadow: 0 6px 16px rgba(0,0,0,0.12); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
transform: translateY(-2px) scale(1.02); transform: translateY(-2px) scale(1.02);
} }
} }
@@ -258,7 +260,6 @@
// 响应式设计 // 响应式设计
@media (max-width: 480px) { @media (max-width: 480px) {
.scenario-card { .scenario-card {
padding: 14px 16px; padding: 14px 16px;
min-height: 70px; min-height: 70px;

View File

@@ -123,7 +123,7 @@ const Scene: React.FC = () => {
> >
<div className={style["scene-page"]}> <div className={style["scene-page"]}>
<div className={style["scenarios-grid"]}> <div className={style["scenarios-grid"]}>
{scenarios.map((scenario) => ( {scenarios.map(scenario => (
<div <div
key={scenario.id} key={scenario.id}
className={style["scenario-card"]} className={style["scenario-card"]}
@@ -136,7 +136,7 @@ const Scene: React.FC = () => {
src={scenario.image} src={scenario.image}
alt={scenario.name} alt={scenario.name}
className={style["card-img"]} className={style["card-img"]}
onError={(e) => { onError={e => {
e.currentTarget.src = e.currentTarget.src =
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png"; "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png";
}} }}

View File

@@ -1,5 +1,5 @@
.scenario-list-page { .scenario-list-page {
padding:0 16px; padding: 0 16px;
} }
.loading { .loading {
@@ -33,7 +33,6 @@
} }
} }
.plan-list { .plan-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -197,7 +196,7 @@
&::before, &::before,
&::after { &::after {
content: ''; content: "";
position: absolute; position: absolute;
top: 50%; top: 50%;
width: 40px; width: 40px;

View File

@@ -104,7 +104,7 @@ const ScenarioList: React.FC = () => {
if (response && response.list) { if (response && response.list) {
if (isLoadMore) { if (isLoadMore) {
// 加载更多时,追加数据 // 加载更多时,追加数据
setTasks((prev) => [...prev, ...response.list]); setTasks(prev => [...prev, ...response.list]);
} else { } else {
// 首次加载或刷新时,替换数据 // 首次加载或刷新时,替换数据
setTasks(response.list); setTasks(response.list);
@@ -158,7 +158,7 @@ const ScenarioList: React.FC = () => {
}; };
const handleCopyPlan = async (taskId: string) => { const handleCopyPlan = async (taskId: string) => {
const taskToCopy = tasks.find((task) => task.id === taskId); const taskToCopy = tasks.find(task => task.id === taskId);
if (!taskToCopy) return; if (!taskToCopy) return;
try { try {
@@ -178,7 +178,7 @@ const ScenarioList: React.FC = () => {
}; };
const handleDeletePlan = async (taskId: string) => { const handleDeletePlan = async (taskId: string) => {
const taskToDelete = tasks.find((task) => task.id === taskId); const taskToDelete = tasks.find(task => task.id === taskId);
if (!taskToDelete) return; if (!taskToDelete) return;
const result = await Dialog.confirm({ const result = await Dialog.confirm({
@@ -285,7 +285,7 @@ const ScenarioList: React.FC = () => {
await fetchPlanList(1, false); await fetchPlanList(1, false);
}; };
const filteredTasks = tasks.filter((task) => const filteredTasks = tasks.filter(task =>
task.name.toLowerCase().includes(searchTerm.toLowerCase()) task.name.toLowerCase().includes(searchTerm.toLowerCase())
); );
@@ -371,7 +371,7 @@ const ScenarioList: React.FC = () => {
<Input <Input
placeholder="搜索计划名称" placeholder="搜索计划名称"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={e => setSearchTerm(e.target.value)}
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
allowClear allowClear
size="large" size="large"
@@ -408,7 +408,7 @@ const ScenarioList: React.FC = () => {
</div> </div>
) : ( ) : (
<> <>
{filteredTasks.map((task) => ( {filteredTasks.map(task => (
<Card key={task.id} className={style["plan-item"]}> <Card key={task.id} className={style["plan-item"]}>
{/* 头部:标题、状态和操作菜单 */} {/* 头部:标题、状态和操作菜单 */}
<div className={style["plan-header"]}> <div className={style["plan-header"]}>
@@ -527,8 +527,8 @@ const ScenarioList: React.FC = () => {
</div> </div>
<div className={style["dialog-content"]}> <div className={style["dialog-content"]}>
{showActionMenu && {showActionMenu &&
getActionMenu(tasks.find((t) => t.id === showActionMenu)!).map( getActionMenu(tasks.find(t => t.id === showActionMenu)!).map(
(item) => ( item => (
<div <div
key={item.key} key={item.key}
className={`${style["action-menu-item"]} ${item.danger ? style["danger"] : ""}`} className={`${style["action-menu-item"]} ${item.danger ? style["danger"] : ""}`}

View File

@@ -230,7 +230,7 @@
background: #e9ecef; background: #e9ecef;
padding: 2px 4px; padding: 2px 4px;
border-radius: 4px; border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
font-size: 11px; font-size: 11px;
} }
} }
@@ -361,7 +361,7 @@
.code { .code {
margin: 0; margin: 0;
padding: 16px; padding: 16px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
font-size: 13px; font-size: 13px;
line-height: 1.5; line-height: 1.5;
color: #24292e; color: #24292e;

View File

@@ -294,7 +294,7 @@ HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.o
return ( return (
<div className={style["code-content"]}> <div className={style["code-content"]}>
<div className={style["language-tabs"]}> <div className={style["language-tabs"]}>
{Object.keys(codeExamples).map((lang) => ( {Object.keys(codeExamples).map(lang => (
<button <button
key={lang} key={lang}
className={`${style["lang-tab"]} ${ className={`${style["lang-tab"]} ${

View File

@@ -71,10 +71,10 @@ export default function NewPlan() {
setSceneLoading(true); setSceneLoading(true);
//获取场景类型 //获取场景类型
getScenarioTypes() getScenarioTypes()
.then((data) => { .then(data => {
setSceneList(data || []); setSceneList(data || []);
}) })
.catch((err) => { .catch(err => {
message.error(err.message || "获取场景类型失败"); message.error(err.message || "获取场景类型失败");
}) })
.finally(() => setSceneLoading(false)); .finally(() => setSceneLoading(false));
@@ -83,7 +83,7 @@ export default function NewPlan() {
//获取计划详情 //获取计划详情
const detail = await getPlanDetail(planId); const detail = await getPlanDetail(planId);
setFormData((prev) => ({ setFormData(prev => ({
...prev, ...prev,
name: detail.name ?? "", name: detail.name ?? "",
scenario: Number(detail.scenario) || 1, scenario: Number(detail.scenario) || 1,
@@ -102,7 +102,7 @@ export default function NewPlan() {
})); }));
} else { } else {
if (scenarioId) { if (scenarioId) {
setFormData((prev) => ({ setFormData(prev => ({
...prev, ...prev,
...{ scenario: Number(scenarioId) || 1 }, ...{ scenario: Number(scenarioId) || 1 },
})); }));
@@ -112,7 +112,7 @@ export default function NewPlan() {
// 更新表单数据 // 更新表单数据
const onChange = (data: any) => { const onChange = (data: any) => {
setFormData((prev) => ({ ...prev, ...data })); setFormData(prev => ({ ...prev, ...data }));
}; };
// 处理保存 // 处理保存
@@ -136,7 +136,7 @@ export default function NewPlan() {
result = await createPlan(formData); result = await createPlan(formData);
} }
message.success(isEdit ? "计划已更新" : "获客计划已创建"); message.success(isEdit ? "计划已更新" : "获客计划已创建");
const sceneItem = sceneList.find((v) => formData.scenario === v.id); const sceneItem = sceneList.find(v => formData.scenario === v.id);
router(`/scenarios/list/${formData.scenario}/${sceneItem.name}`); router(`/scenarios/list/${formData.scenario}/${sceneItem.name}`);
} catch (error) { } catch (error) {
message.error( message.error(
@@ -156,13 +156,13 @@ export default function NewPlan() {
if (currentStep === steps.length) { if (currentStep === steps.length) {
handleSave(); handleSave();
} else { } else {
setCurrentStep((prev) => prev + 1); setCurrentStep(prev => prev + 1);
} }
}; };
// 上一步 // 上一步
const handlePrev = () => { const handlePrev = () => {
setCurrentStep((prev) => Math.max(prev - 1, 1)); setCurrentStep(prev => Math.max(prev - 1, 1));
}; };
// 渲染当前步骤内容 // 渲染当前步骤内容

View File

@@ -82,7 +82,7 @@ const generateRandomAccounts = (count: number): Account[] => {
}; };
const generatePosterMaterials = (): Material[] => { const generatePosterMaterials = (): Material[] => {
return posterTemplates.map((template) => ({ return posterTemplates.map(template => ({
id: template.id, id: template.id,
name: template.name, name: template.name,
type: "poster", type: "poster",
@@ -190,7 +190,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
useEffect(() => { useEffect(() => {
const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, ""); const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, "");
const sceneItem = sceneList.find((v) => formData.scenario === v.id); const sceneItem = sceneList.find(v => formData.scenario === v.id);
onChange({ ...formData, name: `${sceneItem?.name || "海报"}${today}` }); onChange({ ...formData, name: `${sceneItem?.name || "海报"}${today}` });
}, [isEdit]); }, [isEdit]);
@@ -251,15 +251,15 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
type: "poster", type: "poster",
preview: urls[0], preview: urls[0],
}; };
setCustomPosters((prev) => [...prev, newPoster]); setCustomPosters(prev => [...prev, newPoster]);
} }
}; };
// 新增:删除自定义海报 // 新增:删除自定义海报
const handleRemoveCustomPoster = (id: string) => { const handleRemoveCustomPoster = (id: string) => {
setCustomPosters((prev) => prev.filter((p) => p.id !== id)); setCustomPosters(prev => prev.filter(p => p.id !== id));
// 如果选中则取消选中 // 如果选中则取消选中
if (selectedMaterials.some((m) => m.id === id)) { if (selectedMaterials.some(m => m.id === id)) {
setSelectedMaterials([]); setSelectedMaterials([]);
onChange({ ...formData, materials: [] }); onChange({ ...formData, materials: [] });
} }
@@ -267,7 +267,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
// 修改:选中/取消选中海报 // 修改:选中/取消选中海报
const handleMaterialSelect = (material: Material) => { const handleMaterialSelect = (material: Material) => {
const isSelected = selectedMaterials.some((m) => m.id === material.id); const isSelected = selectedMaterials.some(m => m.id === material.id);
if (isSelected) { if (isSelected) {
setSelectedMaterials([]); setSelectedMaterials([]);
onChange({ ...formData, materials: [] }); onChange({ ...formData, materials: [] });
@@ -318,11 +318,11 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
const file = event.target.files?.[0]; const file = event.target.files?.[0];
if (file) { if (file) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = e => {
try { try {
const content = e.target?.result as string; const content = e.target?.result as string;
const rows = content.split("\n").filter((row) => row.trim()); const rows = content.split("\n").filter(row => row.trim());
const tags = rows.slice(1).map((row) => { const tags = rows.slice(1).map(row => {
const [phone, wechat, source, orderAmount, orderDate] = const [phone, wechat, source, orderAmount, orderDate] =
row.split(","); row.split(",");
return { return {
@@ -405,7 +405,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
}; };
// 当前选中的场景对象 // 当前选中的场景对象
const currentScene = sceneList.find((s) => s.id === formData.scenario); const currentScene = sceneList.find(s => s.id === formData.scenario);
//打开订单 //打开订单
const openOrder = const openOrder =
formData.scenario !== 2 ? { display: "none" } : { display: "block" }; formData.scenario !== 2 ? { display: "none" } : { display: "block" };
@@ -430,7 +430,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
</div> </div>
) : ( ) : (
<div className={styles["basic-scene-grid"]}> <div className={styles["basic-scene-grid"]}>
{sceneList.map((scene) => { {sceneList.map(scene => {
const selected = formData.scenario === scene.id; const selected = formData.scenario === scene.id;
return ( return (
<button <button
@@ -453,7 +453,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
<div className={styles["basic-input-block"]}> <div className={styles["basic-input-block"]}>
<Input <Input
value={formData.name} value={formData.name}
onChange={(e) => onChange={e =>
onChange({ ...formData, name: String(e.target.value) }) onChange({ ...formData, name: String(e.target.value) })
} }
placeholder="请输入计划名称" placeholder="请输入计划名称"
@@ -493,7 +493,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
<Input <Input
type="text" type="text"
value={customTagInput} value={customTagInput}
onChange={(e) => setCustomTagInput(e.target.value)} onChange={e => setCustomTagInput(e.target.value)}
placeholder="添加自定义标签" placeholder="添加自定义标签"
/> />
<Button type="primary" onClick={handleAddCustomTag}> <Button type="primary" onClick={handleAddCustomTag}>
@@ -505,7 +505,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
<Input <Input
type="text" type="text"
value={tips} value={tips}
onChange={(e) => { onChange={e => {
setTips(e.target.value); setTips(e.target.value);
onChange({ ...formData, tips: e.target.value }); onChange({ ...formData, tips: e.target.value });
}} }}
@@ -516,9 +516,9 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
<div className={styles["basic-materials"]} style={openPoster}> <div className={styles["basic-materials"]} style={openPoster}>
<div className={styles["basic-label"]}></div> <div className={styles["basic-label"]}></div>
<div className={styles["basic-materials-grid"]}> <div className={styles["basic-materials-grid"]}>
{[...materials, ...customPosters].map((material) => { {[...materials, ...customPosters].map(material => {
const isSelected = selectedMaterials.some( const isSelected = selectedMaterials.some(
(m) => m.id === material.id m => m.id === material.id
); );
const isCustom = material.id.startsWith("custom-"); const isCustom = material.id.startsWith("custom-");
return ( return (
@@ -533,7 +533,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
{/* 预览按钮:自定义海报在左上,内置海报在右上 */} {/* 预览按钮:自定义海报在左上,内置海报在右上 */}
<span <span
className={styles["basic-material-preview"]} className={styles["basic-material-preview"]}
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
handlePreviewImage(material.preview); handlePreviewImage(material.preview);
}} }}
@@ -562,7 +562,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
lineHeight: 20, lineHeight: 20,
color: "#ffffff", color: "#ffffff",
}} }}
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
handleRemoveCustomPoster(material.id); handleRemoveCustomPoster(material.id);
}} }}
@@ -595,7 +595,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
type="file" type="file"
accept="image/*" accept="image/*"
style={{ display: "none" }} style={{ display: "none" }}
onChange={async (e) => { onChange={async e => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
// 直接上传 // 直接上传
@@ -607,7 +607,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
type: "poster", type: "poster",
preview: url, preview: url,
}; };
setCustomPosters((prev) => [...prev, newPoster]); setCustomPosters(prev => [...prev, newPoster]);
} catch (err) { } catch (err) {
// 可加toast提示 // 可加toast提示
} }
@@ -693,9 +693,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
<span></span> <span></span>
<Switch <Switch
checked={phoneSettings.autoAdd} checked={phoneSettings.autoAdd}
onChange={(v) => onChange={v => setPhoneSettings(s => ({ ...s, autoAdd: v }))}
setPhoneSettings((s) => ({ ...s, autoAdd: v }))
}
/> />
</div> </div>
<div <div
@@ -708,8 +706,8 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
<span></span> <span></span>
<Switch <Switch
checked={phoneSettings.speechToText} checked={phoneSettings.speechToText}
onChange={(v) => onChange={v =>
setPhoneSettings((s) => ({ ...s, speechToText: v })) setPhoneSettings(s => ({ ...s, speechToText: v }))
} }
/> />
</div> </div>
@@ -723,8 +721,8 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
<span></span> <span></span>
<Switch <Switch
checked={phoneSettings.questionExtraction} checked={phoneSettings.questionExtraction}
onChange={(v) => onChange={v =>
setPhoneSettings((s) => ({ ...s, questionExtraction: v })) setPhoneSettings(s => ({ ...s, questionExtraction: v }))
} }
/> />
</div> </div>
@@ -758,7 +756,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
<span></span> <span></span>
<Switch <Switch
checked={formData.enabled} checked={formData.enabled}
onChange={(value) => onChange({ ...formData, enabled: value })} onChange={value => onChange({ ...formData, enabled: value })}
/> />
</div> </div>

View File

@@ -94,7 +94,7 @@ const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
<div className={styles["friend-block"]}> <div className={styles["friend-block"]}>
<DeviceSelection <DeviceSelection
selectedDevices={selectedDevices} selectedDevices={selectedDevices}
onSelect={(deviceIds) => { onSelect={deviceIds => {
setSelectedDevices(deviceIds); setSelectedDevices(deviceIds);
onChange({ ...formData, device: deviceIds }); onChange({ ...formData, device: deviceIds });
}} }}
@@ -107,10 +107,10 @@ const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
<div className={styles["friend-block"]} style={{ position: "relative" }}> <div className={styles["friend-block"]} style={{ position: "relative" }}>
<Select <Select
value={formData.remarkType || "phone"} value={formData.remarkType || "phone"}
onChange={(value) => onChange({ ...formData, remarkType: value })} onChange={value => onChange({ ...formData, remarkType: value })}
style={{ width: "100%" }} style={{ width: "100%" }}
> >
{remarkTypes.map((type) => ( {remarkTypes.map(type => (
<Select.Option key={type.value} value={type.value}> <Select.Option key={type.value} value={type.value}>
{type.label} {type.label}
</Select.Option> </Select.Option>
@@ -146,7 +146,7 @@ const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
<div className={styles["friend-block"]}> <div className={styles["friend-block"]}>
<Input <Input
value={formData.greeting} value={formData.greeting}
onChange={(e) => onChange({ ...formData, greeting: e.target.value })} onChange={e => onChange({ ...formData, greeting: e.target.value })}
placeholder="请输入招呼语" placeholder="请输入招呼语"
suffix={ suffix={
<Button <Button
@@ -168,7 +168,7 @@ const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
<Input <Input
type="number" type="number"
value={formData.addFriendInterval || 1} value={formData.addFriendInterval || 1}
onChange={(e) => onChange={e =>
onChange({ onChange({
...formData, ...formData,
addFriendInterval: Number(e.target.value), addFriendInterval: Number(e.target.value),
@@ -185,7 +185,7 @@ const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
<Input <Input
type="time" type="time"
value={formData.addFriendTimeStart || "09:00"} value={formData.addFriendTimeStart || "09:00"}
onChange={(e) => onChange={e =>
onChange({ ...formData, addFriendTimeStart: e.target.value }) onChange({ ...formData, addFriendTimeStart: e.target.value })
} }
style={{ width: 120 }} style={{ width: 120 }}
@@ -194,7 +194,7 @@ const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
<Input <Input
type="time" type="time"
value={formData.addFriendTimeEnd || "18:00"} value={formData.addFriendTimeEnd || "18:00"}
onChange={(e) => onChange={e =>
onChange({ ...formData, addFriendTimeEnd: e.target.value }) onChange({ ...formData, addFriendTimeEnd: e.target.value })
} }
style={{ width: 120 }} style={{ width: 120 }}

View File

@@ -181,7 +181,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
setSelectedGroupId(groupId); setSelectedGroupId(groupId);
setIsGroupSelectOpen(false); setIsGroupSelectOpen(false);
message.success( message.success(
`已选择群组:${mockGroups.find((g) => g.id === groupId)?.name}` `已选择群组:${mockGroups.find(g => g.id === groupId)?.name}`
); );
}; };
@@ -213,7 +213,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
try { try {
const url = await uploadFile(file); const url = await uploadFile(file);
// 更新对应消息的coverImage // 更新对应消息的coverImage
setDayPlans((prev) => { setDayPlans(prev => {
const newPlans = [...prev]; const newPlans = [...prev];
const msg = newPlans[uploadingDay].messages[uploadingMsgIdx]; const msg = newPlans[uploadingDay].messages[uploadingMsgIdx];
msg.coverImage = url; msg.coverImage = url;
@@ -248,7 +248,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
<Input <Input
type="number" type="number"
value={String(message.sendInterval || 5)} value={String(message.sendInterval || 5)}
onChange={(e) => onChange={e =>
handleUpdateMessage(dayIndex, messageIndex, { handleUpdateMessage(dayIndex, messageIndex, {
sendInterval: Number(e.target.value), sendInterval: Number(e.target.value),
}) })
@@ -273,7 +273,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
min={0} min={0}
max={23} max={23}
value={String(message.scheduledTime?.hour || 9)} value={String(message.scheduledTime?.hour || 9)}
onChange={(e) => onChange={e =>
handleUpdateMessage(dayIndex, messageIndex, { handleUpdateMessage(dayIndex, messageIndex, {
scheduledTime: { scheduledTime: {
...(message.scheduledTime || { ...(message.scheduledTime || {
@@ -293,7 +293,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
min={0} min={0}
max={59} max={59}
value={String(message.scheduledTime?.minute || 0)} value={String(message.scheduledTime?.minute || 0)}
onChange={(e) => onChange={e =>
handleUpdateMessage(dayIndex, messageIndex, { handleUpdateMessage(dayIndex, messageIndex, {
scheduledTime: { scheduledTime: {
...(message.scheduledTime || { ...(message.scheduledTime || {
@@ -313,7 +313,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
min={0} min={0}
max={59} max={59}
value={String(message.scheduledTime?.second || 0)} value={String(message.scheduledTime?.second || 0)}
onChange={(e) => onChange={e =>
handleUpdateMessage(dayIndex, messageIndex, { handleUpdateMessage(dayIndex, messageIndex, {
scheduledTime: { scheduledTime: {
...(message.scheduledTime || { ...(message.scheduledTime || {
@@ -340,7 +340,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
</div> </div>
{/* 类型切换按钮 */} {/* 类型切换按钮 */}
<div className={styles["messages-message-type-btns"]}> <div className={styles["messages-message-type-btns"]}>
{messageTypes.map((type) => ( {messageTypes.map(type => (
<Button <Button
key={type.id} key={type.id}
type={message.type === type.id ? "primary" : "default"} type={message.type === type.id ? "primary" : "default"}
@@ -361,7 +361,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
{message.type === "text" && ( {message.type === "text" && (
<Input.TextArea <Input.TextArea
value={message.content} value={message.content}
onChange={(e) => onChange={e =>
handleUpdateMessage(dayIndex, messageIndex, { handleUpdateMessage(dayIndex, messageIndex, {
content: e.target.value, content: e.target.value,
}) })
@@ -375,7 +375,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
<> <>
<Input <Input
value={message.title} value={message.title}
onChange={(e) => onChange={e =>
handleUpdateMessage(dayIndex, messageIndex, { handleUpdateMessage(dayIndex, messageIndex, {
title: e.target.value, title: e.target.value,
}) })
@@ -385,7 +385,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
/> />
<Input <Input
value={message.description} value={message.description}
onChange={(e) => onChange={e =>
handleUpdateMessage(dayIndex, messageIndex, { handleUpdateMessage(dayIndex, messageIndex, {
description: e.target.value, description: e.target.value,
}) })
@@ -395,7 +395,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
/> />
<Input <Input
value={message.address} value={message.address}
onChange={(e) => onChange={e =>
handleUpdateMessage(dayIndex, messageIndex, { handleUpdateMessage(dayIndex, messageIndex, {
address: e.target.value, address: e.target.value,
}) })
@@ -449,7 +449,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
<> <>
<Input <Input
value={message.title} value={message.title}
onChange={(e) => onChange={e =>
handleUpdateMessage(dayIndex, messageIndex, { handleUpdateMessage(dayIndex, messageIndex, {
title: e.target.value, title: e.target.value,
}) })
@@ -459,7 +459,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
/> />
<Input <Input
value={message.description} value={message.description}
onChange={(e) => onChange={e =>
handleUpdateMessage(dayIndex, messageIndex, { handleUpdateMessage(dayIndex, messageIndex, {
description: e.target.value, description: e.target.value,
}) })
@@ -469,7 +469,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
/> />
<Input <Input
value={message.linkUrl} value={message.linkUrl}
onChange={(e) => onChange={e =>
handleUpdateMessage(dayIndex, messageIndex, { handleUpdateMessage(dayIndex, messageIndex, {
linkUrl: e.target.value, linkUrl: e.target.value,
}) })
@@ -523,7 +523,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
<div style={{ marginBottom: 8 }}> <div style={{ marginBottom: 8 }}>
<Button onClick={() => setIsGroupSelectOpen(true)}> <Button onClick={() => setIsGroupSelectOpen(true)}>
{selectedGroupId {selectedGroupId
? mockGroups.find((g) => g.id === selectedGroupId)?.name ? mockGroups.find(g => g.id === selectedGroupId)?.name
: "选择邀请入的群"} : "选择邀请入的群"}
</Button> </Button>
</div> </div>
@@ -614,7 +614,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
}} }}
> >
<div> <div>
{mockGroups.map((group) => ( {mockGroups.map(group => (
<div <div
key={group.id} key={group.id}
className={ className={

View File

@@ -20,14 +20,14 @@
font-size: 16px; font-size: 16px;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
background: rgba(#1677ff,0.1); background: rgba(#1677ff, 0.1);
color: #1677ff; color: #1677ff;
transition: all 0.2s; transition: all 0.2s;
} }
.basic-scene-btn.selected { .basic-scene-btn.selected {
background: #1677ff; background: #1677ff;
color: #fff; color: #fff;
box-shadow: 0 2px 8px rgba(22,119,255,0.08); box-shadow: 0 2px 8px rgba(22, 119, 255, 0.08);
} }
.basic-label { .basic-label {
margin-bottom: 12px; margin-bottom: 12px;
@@ -43,7 +43,7 @@
gap: 5px; gap: 5px;
padding-bottom: 16px; padding-bottom: 16px;
} }
.basic-tag-item{ .basic-tag-item {
margin-bottom: 6px; margin-bottom: 6px;
} }
.basic-custom-tag-input { .basic-custom-tag-input {
@@ -63,20 +63,19 @@
gap: 12px; gap: 12px;
} }
.basic-material-preview {
.basic-material-preview{
position: absolute; position: absolute;
top: 8px; top: 8px;
padding-left: 2px; padding-left: 2px;
right: 8px; right: 8px;
background:rgba(0,0,0,0.5); background: rgba(0, 0, 0, 0.5);
border-radius: 50%; border-radius: 50%;
height: 20px; height: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 2; z-index: 2;
cursor:pointer; cursor: pointer;
} }
.basic-material-card { .basic-material-card {
border: 2px solid #eee; border: 2px solid #eee;
@@ -106,7 +105,7 @@
left: 0; left: 0;
bottom: 0; bottom: 0;
width: 100%; width: 100%;
background: rgba(0,0,0,0.5); background: rgba(0, 0, 0, 0.5);
color: #fff; color: #fff;
font-size: 14px; font-size: 14px;
padding: 4px 0; padding: 4px 0;
@@ -150,7 +149,7 @@
background: #f7f8fa; background: #f7f8fa;
border-radius: 10px; border-radius: 10px;
padding: 20px; padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
margin-bottom: 12px; margin-bottom: 12px;
} }
.basic-wechat-group { .basic-wechat-group {

View File

@@ -18,7 +18,7 @@
border-radius: 6px; border-radius: 6px;
padding: 12px; padding: 12px;
width: 220px; width: 220px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
} }
.friend-remark-q { .friend-remark-q {
position: absolute; position: absolute;

View File

@@ -22,34 +22,40 @@
.messages-message-card { .messages-message-card {
background: #fff; background: #fff;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 4px 16px rgba(22, 119, 255, 0.06), 0 1.5px 4px rgba(0,0,0,0.04); box-shadow:
0 4px 16px rgba(22, 119, 255, 0.06),
0 1.5px 4px rgba(0, 0, 0, 0.04);
padding: 20px 12px 16px 12px; padding: 20px 12px 16px 12px;
margin-bottom: 20px; margin-bottom: 20px;
border: 1.5px solid #f0f3fa; border: 1.5px solid #f0f3fa;
transition: box-shadow 0.2s, border 0.2s, transform 0.2s; transition:
box-shadow 0.2s,
border 0.2s,
transform 0.2s;
position: relative; position: relative;
} }
.messages-message-card:hover { .messages-message-card:hover {
box-shadow: 0 8px 24px rgba(22, 119, 255, 0.12), 0 2px 8px rgba(0,0,0,0.08); box-shadow:
0 8px 24px rgba(22, 119, 255, 0.12),
0 2px 8px rgba(0, 0, 0, 0.08);
border: 1.5px solid #1677ff; border: 1.5px solid #1677ff;
transform: translateY(-2px) scale(1.01); transform: translateY(-2px) scale(1.01);
} }
.messages-message-header { .messages-message-header {
} }
.messages-message-header-content{ .messages-message-header-content {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 8px; margin-bottom: 8px;
} }
.messages-message-type-btns { .messages-message-type-btns {
display: flex; display: flex;
gap: 5px; gap: 5px;
margin-bottom: 8px; margin-bottom: 8px;
} }
.messages-message-type-btn{ .messages-message-type-btn {
width: 20px; width: 20px;
} }
.messages-message-content { .messages-message-content {
margin-bottom: 10px; margin-bottom: 10px;
@@ -95,7 +101,9 @@
background: #fff; background: #fff;
margin-bottom: 8px; margin-bottom: 8px;
border: 1px solid #eee; border: 1px solid #eee;
transition: border 0.2s, background 0.2s; transition:
border 0.2s,
background 0.2s;
} }
.messages-group-select-item.selected { .messages-group-select-item.selected {
background: #e6f7ff; background: #e6f7ff;

View File

@@ -1,363 +1,384 @@
import request from '@/api/request'; import request from "@/api/request";
// ==================== 场景相关接口 ==================== // ==================== 场景相关接口 ====================
// 获取场景列表 // 获取场景列表
export function getScenarios(params: any) { export function getScenarios(params: any) {
return request('/v1/plan/scenes', params, 'GET'); return request("/v1/plan/scenes", params, "GET");
} }
// 获取场景详情 // 获取场景详情
export function getScenarioDetail(id: string) { export function getScenarioDetail(id: string) {
return request(`/v1/scenarios/${id}`, {}, 'GET'); return request(`/v1/scenarios/${id}`, {}, "GET");
} }
// 创建场景 // 创建场景
export function createScenario(data: any) { export function createScenario(data: any) {
return request('/v1/scenarios', data, 'POST'); return request("/v1/scenarios", data, "POST");
} }
// 更新场景 // 更新场景
export function updateScenario(id: string, data: any) { export function updateScenario(id: string, data: any) {
return request(`/v1/scenarios/${id}`, data, 'PUT'); return request(`/v1/scenarios/${id}`, data, "PUT");
} }
// 删除场景 // 删除场景
export function deleteScenario(id: string) { export function deleteScenario(id: string) {
return request(`/v1/scenarios/${id}`, {}, 'DELETE'); return request(`/v1/scenarios/${id}`, {}, "DELETE");
} }
// ==================== 计划相关接口 ==================== // ==================== 计划相关接口 ====================
// 获取计划列表 // 获取计划列表
export function getPlanList(scenarioId: string, page: number = 1, limit: number = 20) { export function getPlanList(
return request(`/api/scenarios/${scenarioId}/plans`, { page, limit }, 'GET'); scenarioId: string,
page: number = 1,
limit: number = 20
) {
return request(`/api/scenarios/${scenarioId}/plans`, { page, limit }, "GET");
} }
// 复制计划 // 复制计划
export function copyPlan(planId: string) { export function copyPlan(planId: string) {
return request(`/api/scenarios/plans/${planId}/copy`, undefined, 'POST'); return request(`/api/scenarios/plans/${planId}/copy`, undefined, "POST");
} }
// 删除计划 // 删除计划
export function deletePlan(planId: string) { export function deletePlan(planId: string) {
return request(`/api/scenarios/plans/${planId}`, undefined, 'DELETE'); return request(`/api/scenarios/plans/${planId}`, undefined, "DELETE");
} }
// 获取小程序二维码 // 获取小程序二维码
export function getWxMinAppCode(planId: string) { export function getWxMinAppCode(planId: string) {
return request(`/api/scenarios/plans/${planId}/qrcode`, undefined, 'GET'); return request(`/api/scenarios/plans/${planId}/qrcode`, undefined, "GET");
} }
// ==================== 设备相关接口 ==================== // ==================== 设备相关接口 ====================
// 获取设备列表 // 获取设备列表
export function getDevices() { export function getDevices() {
return request('/api/devices', undefined, 'GET'); return request("/api/devices", undefined, "GET");
} }
// 获取设备详情 // 获取设备详情
export function getDeviceDetail(deviceId: string) { export function getDeviceDetail(deviceId: string) {
return request(`/api/devices/${deviceId}`, undefined, 'GET'); return request(`/api/devices/${deviceId}`, undefined, "GET");
} }
// 创建设备 // 创建设备
export function createDevice(data: any) { export function createDevice(data: any) {
return request('/api/devices', data, 'POST'); return request("/api/devices", data, "POST");
} }
// 更新设备 // 更新设备
export function updateDevice(deviceId: string, data: any) { export function updateDevice(deviceId: string, data: any) {
return request(`/api/devices/${deviceId}`, data, 'PUT'); return request(`/api/devices/${deviceId}`, data, "PUT");
} }
// 删除设备 // 删除设备
export function deleteDevice(deviceId: string) { export function deleteDevice(deviceId: string) {
return request(`/api/devices/${deviceId}`, undefined, 'DELETE'); return request(`/api/devices/${deviceId}`, undefined, "DELETE");
} }
// ==================== 微信号相关接口 ==================== // ==================== 微信号相关接口 ====================
// 获取微信号列表 // 获取微信号列表
export function getWechatAccounts() { export function getWechatAccounts() {
return request('/api/wechat-accounts', undefined, 'GET'); return request("/api/wechat-accounts", undefined, "GET");
} }
// 获取微信号详情 // 获取微信号详情
export function getWechatAccountDetail(accountId: string) { export function getWechatAccountDetail(accountId: string) {
return request(`/api/wechat-accounts/${accountId}`, undefined, 'GET'); return request(`/api/wechat-accounts/${accountId}`, undefined, "GET");
} }
// 创建微信号 // 创建微信号
export function createWechatAccount(data: any) { export function createWechatAccount(data: any) {
return request('/api/wechat-accounts', data, 'POST'); return request("/api/wechat-accounts", data, "POST");
} }
// 更新微信号 // 更新微信号
export function updateWechatAccount(accountId: string, data: any) { export function updateWechatAccount(accountId: string, data: any) {
return request(`/api/wechat-accounts/${accountId}`, data, 'PUT'); return request(`/api/wechat-accounts/${accountId}`, data, "PUT");
} }
// 删除微信号 // 删除微信号
export function deleteWechatAccount(accountId: string) { export function deleteWechatAccount(accountId: string) {
return request(`/api/wechat-accounts/${accountId}`, undefined, 'DELETE'); return request(`/api/wechat-accounts/${accountId}`, undefined, "DELETE");
} }
// ==================== 海报相关接口 ==================== // ==================== 海报相关接口 ====================
// 获取海报列表 // 获取海报列表
export function getPosters() { export function getPosters() {
return request('/api/posters', undefined, 'GET'); return request("/api/posters", undefined, "GET");
} }
// 获取海报详情 // 获取海报详情
export function getPosterDetail(posterId: string) { export function getPosterDetail(posterId: string) {
return request(`/api/posters/${posterId}`, undefined, 'GET'); return request(`/api/posters/${posterId}`, undefined, "GET");
} }
// 创建海报 // 创建海报
export function createPoster(data: any) { export function createPoster(data: any) {
return request('/api/posters', data, 'POST'); return request("/api/posters", data, "POST");
} }
// 更新海报 // 更新海报
export function updatePoster(posterId: string, data: any) { export function updatePoster(posterId: string, data: any) {
return request(`/api/posters/${posterId}`, data, 'PUT'); return request(`/api/posters/${posterId}`, data, "PUT");
} }
// 删除海报 // 删除海报
export function deletePoster(posterId: string) { export function deletePoster(posterId: string) {
return request(`/api/posters/${posterId}`, undefined, 'DELETE'); return request(`/api/posters/${posterId}`, undefined, "DELETE");
} }
// ==================== 内容相关接口 ==================== // ==================== 内容相关接口 ====================
// 获取内容列表 // 获取内容列表
export function getContents(params: any) { export function getContents(params: any) {
return request('/api/contents', params, 'GET'); return request("/api/contents", params, "GET");
} }
// 获取内容详情 // 获取内容详情
export function getContentDetail(contentId: string) { export function getContentDetail(contentId: string) {
return request(`/api/contents/${contentId}`, undefined, 'GET'); return request(`/api/contents/${contentId}`, undefined, "GET");
} }
// 创建内容 // 创建内容
export function createContent(data: any) { export function createContent(data: any) {
return request('/api/contents', data, 'POST'); return request("/api/contents", data, "POST");
} }
// 更新内容 // 更新内容
export function updateContent(contentId: string, data: any) { export function updateContent(contentId: string, data: any) {
return request(`/api/contents/${contentId}`, data, 'PUT'); return request(`/api/contents/${contentId}`, data, "PUT");
} }
// 删除内容 // 删除内容
export function deleteContent(contentId: string) { export function deleteContent(contentId: string) {
return request(`/api/contents/${contentId}`, undefined, 'DELETE'); return request(`/api/contents/${contentId}`, undefined, "DELETE");
} }
// ==================== 流量池相关接口 ==================== // ==================== 流量池相关接口 ====================
// 获取流量池列表 // 获取流量池列表
export function getTrafficPools() { export function getTrafficPools() {
return request('/api/traffic-pools', undefined, 'GET'); return request("/api/traffic-pools", undefined, "GET");
} }
// 获取流量池详情 // 获取流量池详情
export function getTrafficPoolDetail(poolId: string) { export function getTrafficPoolDetail(poolId: string) {
return request(`/api/traffic-pools/${poolId}`, undefined, 'GET'); return request(`/api/traffic-pools/${poolId}`, undefined, "GET");
} }
// 创建流量池 // 创建流量池
export function createTrafficPool(data: any) { export function createTrafficPool(data: any) {
return request('/api/traffic-pools', data, 'POST'); return request("/api/traffic-pools", data, "POST");
} }
// 更新流量池 // 更新流量池
export function updateTrafficPool(poolId: string, data: any) { export function updateTrafficPool(poolId: string, data: any) {
return request(`/api/traffic-pools/${poolId}`, data, 'PUT'); return request(`/api/traffic-pools/${poolId}`, data, "PUT");
} }
// 删除流量池 // 删除流量池
export function deleteTrafficPool(poolId: string) { export function deleteTrafficPool(poolId: string) {
return request(`/api/traffic-pools/${poolId}`, undefined, 'DELETE'); return request(`/api/traffic-pools/${poolId}`, undefined, "DELETE");
} }
// ==================== 工作台相关接口 ==================== // ==================== 工作台相关接口 ====================
// 获取工作台统计数据 // 获取工作台统计数据
export function getWorkspaceStats() { export function getWorkspaceStats() {
return request('/api/workspace/stats', undefined, 'GET'); return request("/api/workspace/stats", undefined, "GET");
} }
// 获取自动点赞任务列表 // 获取自动点赞任务列表
export function getAutoLikeTasks() { export function getAutoLikeTasks() {
return request('/api/workspace/auto-like/tasks', undefined, 'GET'); return request("/api/workspace/auto-like/tasks", undefined, "GET");
} }
// 创建自动点赞任务 // 创建自动点赞任务
export function createAutoLikeTask(data: any) { export function createAutoLikeTask(data: any) {
return request('/api/workspace/auto-like/tasks', data, 'POST'); return request("/api/workspace/auto-like/tasks", data, "POST");
} }
// 更新自动点赞任务 // 更新自动点赞任务
export function updateAutoLikeTask(taskId: string, data: any) { export function updateAutoLikeTask(taskId: string, data: any) {
return request(`/api/workspace/auto-like/tasks/${taskId}`, data, 'PUT'); return request(`/api/workspace/auto-like/tasks/${taskId}`, data, "PUT");
} }
// 删除自动点赞任务 // 删除自动点赞任务
export function deleteAutoLikeTask(taskId: string) { export function deleteAutoLikeTask(taskId: string) {
return request(`/api/workspace/auto-like/tasks/${taskId}`, undefined, 'DELETE'); return request(
`/api/workspace/auto-like/tasks/${taskId}`,
undefined,
"DELETE"
);
} }
// ==================== 群发相关接口 ==================== // ==================== 群发相关接口 ====================
// 获取群发任务列表 // 获取群发任务列表
export function getGroupPushTasks() { export function getGroupPushTasks() {
return request('/api/workspace/group-push/tasks', undefined, 'GET'); return request("/api/workspace/group-push/tasks", undefined, "GET");
} }
// 创建群发任务 // 创建群发任务
export function createGroupPushTask(data: any) { export function createGroupPushTask(data: any) {
return request('/api/workspace/group-push/tasks', data, 'POST'); return request("/api/workspace/group-push/tasks", data, "POST");
} }
// 更新群发任务 // 更新群发任务
export function updateGroupPushTask(taskId: string, data: any) { export function updateGroupPushTask(taskId: string, data: any) {
return request(`/api/workspace/group-push/tasks/${taskId}`, data, 'PUT'); return request(`/api/workspace/group-push/tasks/${taskId}`, data, "PUT");
} }
// 删除群发任务 // 删除群发任务
export function deleteGroupPushTask(taskId: string) { export function deleteGroupPushTask(taskId: string) {
return request(`/api/workspace/group-push/tasks/${taskId}`, undefined, 'DELETE'); return request(
`/api/workspace/group-push/tasks/${taskId}`,
undefined,
"DELETE"
);
} }
// ==================== 自动建群相关接口 ==================== // ==================== 自动建群相关接口 ====================
// 获取自动建群任务列表 // 获取自动建群任务列表
export function getAutoGroupTasks() { export function getAutoGroupTasks() {
return request('/api/workspace/auto-group/tasks', undefined, 'GET'); return request("/api/workspace/auto-group/tasks", undefined, "GET");
} }
// 创建自动建群任务 // 创建自动建群任务
export function createAutoGroupTask(data: any) { export function createAutoGroupTask(data: any) {
return request('/api/workspace/auto-group/tasks', data, 'POST'); return request("/api/workspace/auto-group/tasks", data, "POST");
} }
// 更新自动建群任务 // 更新自动建群任务
export function updateAutoGroupTask(taskId: string, data: any) { export function updateAutoGroupTask(taskId: string, data: any) {
return request(`/api/workspace/auto-group/tasks/${taskId}`, data, 'PUT'); return request(`/api/workspace/auto-group/tasks/${taskId}`, data, "PUT");
} }
// 删除自动建群任务 // 删除自动建群任务
export function deleteAutoGroupTask(taskId: string) { export function deleteAutoGroupTask(taskId: string) {
return request(`/api/workspace/auto-group/tasks/${taskId}`, undefined, 'DELETE'); return request(
`/api/workspace/auto-group/tasks/${taskId}`,
undefined,
"DELETE"
);
} }
// ==================== AI助手相关接口 ==================== // ==================== AI助手相关接口 ====================
// 获取AI对话历史 // 获取AI对话历史
export function getAIChatHistory() { export function getAIChatHistory() {
return request('/api/workspace/ai-assistant/chat-history', undefined, 'GET'); return request("/api/workspace/ai-assistant/chat-history", undefined, "GET");
} }
// 发送AI消息 // 发送AI消息
export function sendAIMessage(data: any) { export function sendAIMessage(data: any) {
return request('/api/workspace/ai-assistant/send-message', data, 'POST'); return request("/api/workspace/ai-assistant/send-message", data, "POST");
} }
// 获取AI分析报告 // 获取AI分析报告
export function getAIAnalysisReport() { export function getAIAnalysisReport() {
return request('/api/workspace/ai-assistant/analysis-report', undefined, 'GET'); return request(
"/api/workspace/ai-assistant/analysis-report",
undefined,
"GET"
);
} }
// ==================== 订单相关接口 ==================== // ==================== 订单相关接口 ====================
// 获取订单列表 // 获取订单列表
export function getOrders(params: any) { export function getOrders(params: any) {
return request('/api/orders', params, 'GET'); return request("/api/orders", params, "GET");
} }
// 获取订单详情 // 获取订单详情
export function getOrderDetail(orderId: string) { export function getOrderDetail(orderId: string) {
return request(`/api/orders/${orderId}`, undefined, 'GET'); return request(`/api/orders/${orderId}`, undefined, "GET");
} }
// 创建订单 // 创建订单
export function createOrder(data: any) { export function createOrder(data: any) {
return request('/api/orders', data, 'POST'); return request("/api/orders", data, "POST");
} }
// 更新订单 // 更新订单
export function updateOrder(orderId: string, data: any) { export function updateOrder(orderId: string, data: any) {
return request(`/api/orders/${orderId}`, data, 'PUT'); return request(`/api/orders/${orderId}`, data, "PUT");
} }
// 删除订单 // 删除订单
export function deleteOrder(orderId: string) { export function deleteOrder(orderId: string) {
return request(`/api/orders/${orderId}`, undefined, 'DELETE'); return request(`/api/orders/${orderId}`, undefined, "DELETE");
} }
// ==================== 用户相关接口 ==================== // ==================== 用户相关接口 ====================
// 获取用户信息 // 获取用户信息
export function getUserInfo() { export function getUserInfo() {
return request('/api/user/info', undefined, 'GET'); return request("/api/user/info", undefined, "GET");
} }
// 更新用户信息 // 更新用户信息
export function updateUserInfo(data: any) { export function updateUserInfo(data: any) {
return request('/api/user/info', data, 'PUT'); return request("/api/user/info", data, "PUT");
} }
// 修改密码 // 修改密码
export function changePassword(data: any) { export function changePassword(data: any) {
return request('/api/user/change-password', data, 'POST'); return request("/api/user/change-password", data, "POST");
} }
// 上传头像 // 上传头像
export function uploadAvatar(data: any) { export function uploadAvatar(data: any) {
return request('/api/user/upload-avatar', data, 'POST'); return request("/api/user/upload-avatar", data, "POST");
} }
// ==================== 文件上传相关接口 ==================== // ==================== 文件上传相关接口 ====================
// 上传文件 // 上传文件
export function uploadFile(data: any) { export function uploadFile(data: any) {
return request('/api/upload/file', data, 'POST'); return request("/api/upload/file", data, "POST");
} }
// 上传图片 // 上传图片
export function uploadImage(data: any) { export function uploadImage(data: any) {
return request('/api/upload/image', data, 'POST'); return request("/api/upload/image", data, "POST");
} }
// 删除文件 // 删除文件
export function deleteFile(fileId: string) { export function deleteFile(fileId: string) {
return request(`/api/upload/files/${fileId}`, undefined, 'DELETE'); return request(`/api/upload/files/${fileId}`, undefined, "DELETE");
} }
// ==================== 系统配置相关接口 ==================== // ==================== 系统配置相关接口 ====================
// 获取系统配置 // 获取系统配置
export function getSystemConfig() { export function getSystemConfig() {
return request('/api/system/config', undefined, 'GET'); return request("/api/system/config", undefined, "GET");
} }
// 更新系统配置 // 更新系统配置
export function updateSystemConfig(data: any) { export function updateSystemConfig(data: any) {
return request('/api/system/config', data, 'PUT'); return request("/api/system/config", data, "PUT");
} }
// 获取系统通知 // 获取系统通知
export function getSystemNotifications() { export function getSystemNotifications() {
return request('/api/system/notifications', undefined, 'GET'); return request("/api/system/notifications", undefined, "GET");
} }
// 标记通知为已读 // 标记通知为已读
export function markNotificationAsRead(notificationId: string) { export function markNotificationAsRead(notificationId: string) {
return request(`/api/system/notifications/${notificationId}/read`, undefined, 'PUT'); return request(
`/api/system/notifications/${notificationId}/read`,
undefined,
"PUT"
);
} }

View File

@@ -1,6 +1,4 @@
.analyzerPage { .analyzerPage {
} }
.tabs { .tabs {
@@ -20,7 +18,7 @@
.planCard { .planCard {
background: #fff; background: #fff;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
padding: 16px 14px 12px 14px; padding: 16px 14px 12px 14px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -46,7 +46,7 @@ const AiAnalyzer: React.FC = () => {
const [tab, setTab] = useState<"all" | "doing" | "done">("all"); const [tab, setTab] = useState<"all" | "doing" | "done">("all");
const filteredPlans = const filteredPlans =
tab === "all" ? mockPlans : mockPlans.filter((p) => p.status === tab); tab === "all" ? mockPlans : mockPlans.filter(p => p.status === tab);
return ( return (
<Layout <Layout
@@ -64,7 +64,7 @@ const AiAnalyzer: React.FC = () => {
<div className={styles.analyzerPage}> <div className={styles.analyzerPage}>
<Tabs <Tabs
activeKey={tab} activeKey={tab}
onChange={(key) => setTab(key as any)} onChange={key => setTab(key as any)}
className={styles.tabs} className={styles.tabs}
> >
<Tabs.Tab title="全部计划" key="all" /> <Tabs.Tab title="全部计划" key="all" />
@@ -72,7 +72,7 @@ const AiAnalyzer: React.FC = () => {
<Tabs.Tab title="已完成" key="done" /> <Tabs.Tab title="已完成" key="done" />
</Tabs> </Tabs>
<div className={styles.planList}> <div className={styles.planList}>
{filteredPlans.map((plan) => ( {filteredPlans.map(plan => (
<div className={styles.planCard} key={plan.id}> <div className={styles.planCard} key={plan.id}>
<div className={styles.cardHeader}> <div className={styles.cardHeader}>
<span className={styles.cardTitle}>{plan.title}</span> <span className={styles.cardTitle}>{plan.title}</span>
@@ -89,7 +89,7 @@ const AiAnalyzer: React.FC = () => {
</div> </div>
<div> <div>
<span className={styles.label}></span> <span className={styles.label}></span>
{plan.keywords.map((k) => ( {plan.keywords.map(k => (
<span className={styles.keyword} key={k}> <span className={styles.keyword} key={k}>
{k} {k}
</span> </span>

View File

@@ -33,7 +33,7 @@
word-break: break-word; word-break: break-word;
background: #fff; background: #fff;
color: #222; color: #222;
box-shadow: 0 2px 8px rgba(0,0,0,0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
} }
.userMessage .bubble { .userMessage .bubble {
@@ -64,7 +64,7 @@
align-items: center; align-items: center;
background: #fff; background: #fff;
padding: 10px 12px 10px 12px; padding: 10px 12px 10px 12px;
box-shadow: 0 -2px 8px rgba(0,0,0,0.04); box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
z-index: 10; z-index: 10;
} }
@@ -80,7 +80,10 @@
} }
.sendButton { .sendButton {
background: var(--primary-gradient, linear-gradient(135deg, #a7e0ff 0%, #5bbcff 100%)); background: var(
--primary-gradient,
linear-gradient(135deg, #a7e0ff 0%, #5bbcff 100%)
);
color: #fff; color: #fff;
border: none; border: none;
border-radius: 18px; border-radius: 18px;
@@ -107,10 +110,13 @@
cursor: pointer; cursor: pointer;
padding: 4px; padding: 4px;
border-radius: 50%; border-radius: 50%;
transition: background 0.2s, color 0.2s; transition:
background 0.2s,
color 0.2s;
} }
.iconBtn:hover, .iconBtn:active { .iconBtn:hover,
.iconBtn:active {
background: #f3f4f6; background: #f3f4f6;
color: #5bbcff; color: #5bbcff;
} }

View File

@@ -52,7 +52,7 @@ const AIAssistant: React.FC = () => {
recognitionRef.current.lang = "zh-CN"; recognitionRef.current.lang = "zh-CN";
recognitionRef.current.onresult = (event: any) => { recognitionRef.current.onresult = (event: any) => {
const transcript = event.results[0][0].transcript; const transcript = event.results[0][0].transcript;
setInput((prev) => prev + transcript); setInput(prev => prev + transcript);
setRecognizing(false); setRecognizing(false);
}; };
recognitionRef.current.onerror = () => setRecognizing(false); recognitionRef.current.onerror = () => setRecognizing(false);
@@ -71,11 +71,11 @@ const AIAssistant: React.FC = () => {
}), }),
type: "text", type: "text",
}; };
setMessages((prev) => [...prev, userMsg]); setMessages(prev => [...prev, userMsg]);
setInput(""); setInput("");
setLoading(true); setLoading(true);
setTimeout(() => { setTimeout(() => {
setMessages((prev) => [ setMessages(prev => [
...prev, ...prev,
{ {
id: Date.now().toString() + "-ai", id: Date.now().toString() + "-ai",
@@ -97,7 +97,7 @@ const AIAssistant: React.FC = () => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
setMessages((prev) => [ setMessages(prev => [
...prev, ...prev,
{ {
id: Date.now().toString(), id: Date.now().toString(),
@@ -121,7 +121,7 @@ const AIAssistant: React.FC = () => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
setMessages((prev) => [ setMessages(prev => [
...prev, ...prev,
{ {
id: Date.now().toString(), id: Date.now().toString(),
@@ -156,7 +156,7 @@ const AIAssistant: React.FC = () => {
<Layout header={<NavCommon title="AI助手" />} loading={false}> <Layout header={<NavCommon title="AI助手" />} loading={false}>
<div className={styles.chatContainer}> <div className={styles.chatContainer}>
<div className={styles.messageList}> <div className={styles.messageList}>
{messages.map((msg) => ( {messages.map(msg => (
<div <div
key={msg.id} key={msg.id}
className={ className={
@@ -242,8 +242,8 @@ const AIAssistant: React.FC = () => {
type="text" type="text"
placeholder="输入消息..." placeholder="输入消息..."
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={e => setInput(e.target.value)}
onKeyDown={(e) => { onKeyDown={e => {
if (e.key === "Enter") handleSend(); if (e.key === "Enter") handleSend();
}} }}
disabled={loading} disabled={loading}

View File

@@ -26,7 +26,7 @@
.infoCard { .infoCard {
margin-bottom: 16px; margin-bottom: 16px;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
border: none; border: none;
background: #fff; background: #fff;
padding: 16px; padding: 16px;
@@ -53,7 +53,7 @@
} }
.progressCard { .progressCard {
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
border: none; border: none;
background: #fff; background: #fff;
padding: 16px; padding: 16px;
@@ -75,7 +75,7 @@
} }
.groupCard { .groupCard {
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
border: none; border: none;
background: #fff; background: #fff;
padding: 12px 16px; padding: 12px 16px;
@@ -134,7 +134,7 @@
padding: 40px 0; padding: 40px 0;
background: #fff; background: #fff;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
margin-top: 32px; margin-top: 32px;
} }
.emptyTitle { .emptyTitle {

View File

@@ -109,7 +109,7 @@ const GroupPreview: React.FC<{
{expanded ? ( {expanded ? (
<> <>
<div className={style.memberGrid}> <div className={style.memberGrid}>
{members.map((member) => ( {members.map(member => (
<div key={member.id} className={style.memberItem}> <div key={member.id} className={style.memberItem}>
<span>{member.nickname}</span> <span>{member.nickname}</span>
{member.tags.length > 0 && ( {member.tags.length > 0 && (
@@ -182,7 +182,7 @@ const GroupCreationProgress: React.FC<{
setStatus("completed"); setStatus("completed");
onComplete(); onComplete();
} else { } else {
setCurrentGroupIndex((prev) => prev + 1); setCurrentGroupIndex(prev => prev + 1);
} }
}, 3000); }, 3000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
@@ -190,7 +190,7 @@ const GroupCreationProgress: React.FC<{
}, [status, currentGroupIndex, groups.length, onComplete]); }, [status, currentGroupIndex, groups.length, onComplete]);
const handleRetryGroup = (groupIndex: number) => { const handleRetryGroup = (groupIndex: number) => {
setGroups((prev) => setGroups(prev =>
prev.map((group, index) => { prev.map((group, index) => {
if (index === groupIndex) { if (index === groupIndex) {
return { return {

View File

@@ -1,5 +1,5 @@
.autoGroupForm { .autoGroupForm {
padding: 10px; padding: 10px;
background: #f7f8fa; background: #f7f8fa;
} }

View File

@@ -117,7 +117,7 @@ const AutoGroupForm: React.FC = () => {
<Form.Item label="任务名称" name="name" required> <Form.Item label="任务名称" name="name" required>
<Input <Input
value={form.name} value={form.name}
onChange={(val) => setForm((f: any) => ({ ...f, name: val }))} onChange={val => setForm((f: any) => ({ ...f, name: val }))}
placeholder="请输入任务名称" placeholder="请输入任务名称"
/> />
</Form.Item> </Form.Item>
@@ -125,7 +125,7 @@ const AutoGroupForm: React.FC = () => {
<Input <Input
type="number" type="number"
value={form.deviceCount} value={form.deviceCount}
onChange={(val) => onChange={val =>
setForm((f: any) => ({ ...f, deviceCount: Number(val) })) setForm((f: any) => ({ ...f, deviceCount: Number(val) }))
} }
placeholder="请输入设备数量" placeholder="请输入设备数量"
@@ -135,7 +135,7 @@ const AutoGroupForm: React.FC = () => {
<Input <Input
type="number" type="number"
value={form.targetFriends} value={form.targetFriends}
onChange={(val) => onChange={val =>
setForm((f: any) => ({ ...f, targetFriends: Number(val) })) setForm((f: any) => ({ ...f, targetFriends: Number(val) }))
} }
placeholder="请输入目标好友数" placeholder="请输入目标好友数"
@@ -145,7 +145,7 @@ const AutoGroupForm: React.FC = () => {
<Input <Input
type="number" type="number"
value={form.createInterval} value={form.createInterval}
onChange={(val) => onChange={val =>
setForm((f: any) => ({ ...f, createInterval: Number(val) })) setForm((f: any) => ({ ...f, createInterval: Number(val) }))
} }
placeholder="请输入建群间隔" placeholder="请输入建群间隔"
@@ -155,7 +155,7 @@ const AutoGroupForm: React.FC = () => {
<Input <Input
type="number" type="number"
value={form.maxGroupsPerDay} value={form.maxGroupsPerDay}
onChange={(val) => onChange={val =>
setForm((f: any) => ({ ...f, maxGroupsPerDay: Number(val) })) setForm((f: any) => ({ ...f, maxGroupsPerDay: Number(val) }))
} }
placeholder="请输入最大建群数" placeholder="请输入最大建群数"
@@ -165,7 +165,7 @@ const AutoGroupForm: React.FC = () => {
<div className={style.timeRangeRow}> <div className={style.timeRangeRow}>
<Input <Input
value={form.timeRange.start} value={form.timeRange.start}
onChange={(val) => onChange={val =>
setForm((f: any) => ({ setForm((f: any) => ({
...f, ...f,
timeRange: { ...f.timeRange, start: val }, timeRange: { ...f.timeRange, start: val },
@@ -176,7 +176,7 @@ const AutoGroupForm: React.FC = () => {
<span style={{ margin: "0 8px" }}>-</span> <span style={{ margin: "0 8px" }}>-</span>
<Input <Input
value={form.timeRange.end} value={form.timeRange.end}
onChange={(val) => onChange={val =>
setForm((f: any) => ({ setForm((f: any) => ({
...f, ...f,
timeRange: { ...f.timeRange, end: val }, timeRange: { ...f.timeRange, end: val },
@@ -191,7 +191,7 @@ const AutoGroupForm: React.FC = () => {
<Input <Input
type="number" type="number"
value={form.groupSize.min} value={form.groupSize.min}
onChange={(val) => onChange={val =>
setForm((f: any) => ({ setForm((f: any) => ({
...f, ...f,
groupSize: { ...f.groupSize, min: Number(val) }, groupSize: { ...f.groupSize, min: Number(val) },
@@ -203,7 +203,7 @@ const AutoGroupForm: React.FC = () => {
<Input <Input
type="number" type="number"
value={form.groupSize.max} value={form.groupSize.max}
onChange={(val) => onChange={val =>
setForm((f: any) => ({ setForm((f: any) => ({
...f, ...f,
groupSize: { ...f.groupSize, max: Number(val) }, groupSize: { ...f.groupSize, max: Number(val) },
@@ -218,15 +218,13 @@ const AutoGroupForm: React.FC = () => {
options={tagOptions} options={tagOptions}
multiple multiple
value={form.targetTags} value={form.targetTags}
onChange={(val) => onChange={val => setForm((f: any) => ({ ...f, targetTags: val }))}
setForm((f: any) => ({ ...f, targetTags: val }))
}
/> />
</Form.Item> </Form.Item>
<Form.Item label="群名称模板" name="groupNameTemplate" required> <Form.Item label="群名称模板" name="groupNameTemplate" required>
<Input <Input
value={form.groupNameTemplate} value={form.groupNameTemplate}
onChange={(val) => onChange={val =>
setForm((f: any) => ({ ...f, groupNameTemplate: val })) setForm((f: any) => ({ ...f, groupNameTemplate: val }))
} }
placeholder="请输入群名称模板" placeholder="请输入群名称模板"
@@ -235,7 +233,7 @@ const AutoGroupForm: React.FC = () => {
<Form.Item label="群描述" name="groupDescription"> <Form.Item label="群描述" name="groupDescription">
<TextArea <TextArea
value={form.groupDescription} value={form.groupDescription}
onChange={(val) => onChange={val =>
setForm((f: any) => ({ ...f, groupDescription: val })) setForm((f: any) => ({ ...f, groupDescription: val }))
} }
placeholder="请输入群描述" placeholder="请输入群描述"

View File

@@ -8,7 +8,7 @@
.taskCard { .taskCard {
margin-bottom: 16px; margin-bottom: 16px;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
border: none; border: none;
background: #fff; background: #fff;
padding: 16px; padding: 16px;
@@ -158,7 +158,7 @@
padding: 40px 0; padding: 40px 0;
background: #fff; background: #fff;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
margin-top: 32px; margin-top: 32px;
} }
.emptyTitle { .emptyTitle {

View File

@@ -112,10 +112,10 @@ const AutoGroupList: React.FC = () => {
const [tasks, setTasks] = useState<GroupTask[]>(mockTasks); const [tasks, setTasks] = useState<GroupTask[]>(mockTasks);
const handleDelete = (taskId: string) => { const handleDelete = (taskId: string) => {
const taskToDelete = tasks.find((task) => task.id === taskId); const taskToDelete = tasks.find(task => task.id === taskId);
if (!taskToDelete) return; if (!taskToDelete) return;
window.confirm(`确定要删除"${taskToDelete.name}"吗?`) && window.confirm(`确定要删除"${taskToDelete.name}"吗?`) &&
setTasks(tasks.filter((task) => task.id !== taskId)); setTasks(tasks.filter(task => task.id !== taskId));
Toast.show({ content: "删除成功" }); Toast.show({ content: "删除成功" });
}; };
@@ -128,7 +128,7 @@ const AutoGroupList: React.FC = () => {
}; };
const handleCopy = (taskId: string) => { const handleCopy = (taskId: string) => {
const taskToCopy = tasks.find((task) => task.id === taskId); const taskToCopy = tasks.find(task => task.id === taskId);
if (taskToCopy) { if (taskToCopy) {
const newTask = { const newTask = {
...taskToCopy, ...taskToCopy,
@@ -142,8 +142,8 @@ const AutoGroupList: React.FC = () => {
}; };
const toggleTaskStatus = (taskId: string) => { const toggleTaskStatus = (taskId: string) => {
setTasks((prev) => setTasks(prev =>
prev.map((task) => prev.map(task =>
task.id === taskId task.id === taskId
? { ? {
...task, ...task,
@@ -159,7 +159,7 @@ const AutoGroupList: React.FC = () => {
navigate("/workspace/auto-group/new"); navigate("/workspace/auto-group/new");
}; };
const filteredTasks = tasks.filter((task) => const filteredTasks = tasks.filter(task =>
task.name.toLowerCase().includes(searchTerm.toLowerCase()) task.name.toLowerCase().includes(searchTerm.toLowerCase())
); );
@@ -192,7 +192,7 @@ const AutoGroupList: React.FC = () => {
<Input <Input
placeholder="搜索计划名称" placeholder="搜索计划名称"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={e => setSearchTerm(e.target.value)}
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
allowClear allowClear
size="large" size="large"
@@ -222,7 +222,7 @@ const AutoGroupList: React.FC = () => {
</Button> </Button>
</Card> </Card>
) : ( ) : (
filteredTasks.map((task) => ( filteredTasks.map(task => (
<Card key={task.id} className={style.taskCard}> <Card key={task.id} className={style.taskCard}>
<div className={style.taskHeader}> <div className={style.taskHeader}>
<div className={style.taskTitle}>{task.name}</div> <div className={style.taskTitle}>{task.name}</div>

View File

@@ -55,7 +55,7 @@ const CardMenu: React.FC<CardMenuProps> = ({
return ( return (
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>
<button onClick={() => setOpen((v) => !v)} className={style["menu-btn"]}> <button onClick={() => setOpen(v => !v)} className={style["menu-btn"]}>
<MoreOutlined /> <MoreOutlined />
</button> </button>
{open && ( {open && (
@@ -223,7 +223,7 @@ const AutoLike: React.FC = () => {
}; };
// 过滤任务 // 过滤任务
const filteredTasks = tasks.filter((task) => const filteredTasks = tasks.filter(task =>
task.name.toLowerCase().includes(searchTerm.toLowerCase()) task.name.toLowerCase().includes(searchTerm.toLowerCase())
); );
@@ -247,7 +247,7 @@ const AutoLike: React.FC = () => {
<Input <Input
placeholder="搜索计划名称" placeholder="搜索计划名称"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={e => setSearchTerm(e.target.value)}
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
allowClear allowClear
size="large" size="large"
@@ -284,7 +284,7 @@ const AutoLike: React.FC = () => {
</div> </div>
</div> </div>
) : ( ) : (
filteredTasks.map((task) => ( filteredTasks.map(task => (
<Card key={task.id} className={style["task-card"]}> <Card key={task.id} className={style["task-card"]}>
<div className={style["task-header"]}> <div className={style["task-header"]}>
<div className={style["task-title-section"]}> <div className={style["task-title-section"]}>

View File

@@ -1,22 +1,27 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from "react-router-dom";
import { ChevronLeft,Plus, Minus, Check, X, Tag as TagIcon } from 'lucide-react'; import {
import { Button } from '@/components/ui/button'; ChevronLeft,
import { Input } from '@/components/ui/input'; Plus,
import { Label } from '@/components/ui/label'; Minus,
import { Switch } from '@/components/ui/switch'; Check,
import { createAutoLikeTask, updateAutoLikeTask, fetchAutoLikeTaskDetail } from '@/api/autoLike'; X,
import { ContentType } from '@/types/auto-like'; Tag as TagIcon,
import { useToast } from '@/components/ui/toast'; } from "lucide-react";
import Layout from '@/components/Layout'; import { Button } from "@/components/ui/button";
import DeviceSelection from '@/components/DeviceSelection'; import { Input } from "@/components/ui/input";
import FriendSelection from '@/components/FriendSelection'; import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
createAutoLikeTask,
updateAutoLikeTask,
fetchAutoLikeTaskDetail,
} from "@/api/autoLike";
import { ContentType } from "@/types/auto-like";
import { useToast } from "@/components/ui/toast";
import Layout from "@/components/Layout";
import DeviceSelection from "@/components/DeviceSelection";
import FriendSelection from "@/components/FriendSelection";
// 修改CreateLikeTaskData接口确保friends字段不是可选的 // 修改CreateLikeTaskData接口确保friends字段不是可选的
interface CreateLikeTaskDataLocal { interface CreateLikeTaskDataLocal {
@@ -43,18 +48,18 @@ export default function NewAutoLike() {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [isLoading, setIsLoading] = useState(isEditMode); const [isLoading, setIsLoading] = useState(isEditMode);
const [formData, setFormData] = useState<CreateLikeTaskDataLocal>({ const [formData, setFormData] = useState<CreateLikeTaskDataLocal>({
name: '', name: "",
interval: 5, interval: 5,
maxLikes: 200, maxLikes: 200,
startTime: '08:00', startTime: "08:00",
endTime: '22:00', endTime: "22:00",
contentTypes: ['text', 'image', 'video'], contentTypes: ["text", "image", "video"],
devices: [], devices: [],
friends: [], // 确保初始化为空数组而不是undefined friends: [], // 确保初始化为空数组而不是undefined
targetTags: [], targetTags: [],
friendMaxLikes: 10, friendMaxLikes: 10,
enableFriendTags: false, enableFriendTags: false,
friendTags: '', friendTags: "",
}); });
// 新增自动开启的独立状态 // 新增自动开启的独立状态
const [autoEnabled, setAutoEnabled] = useState(false); const [autoEnabled, setAutoEnabled] = useState(false);
@@ -70,7 +75,7 @@ export default function NewAutoLike() {
const fetchTaskDetail = async () => { const fetchTaskDetail = async () => {
try { try {
const taskDetail = await fetchAutoLikeTaskDetail(id!); const taskDetail = await fetchAutoLikeTaskDetail(id!);
console.log('Task detail response:', taskDetail); // 添加日志用于调试 console.log("Task detail response:", taskDetail); // 添加日志用于调试
if (taskDetail) { if (taskDetail) {
// 使用类型断言处理可能的字段名称差异 // 使用类型断言处理可能的字段名称差异
@@ -79,65 +84,63 @@ export default function NewAutoLike() {
const config = taskAny.config || taskAny; const config = taskAny.config || taskAny;
setFormData({ setFormData({
name: taskDetail.name || '', name: taskDetail.name || "",
interval: config.likeInterval || config.interval || 5, interval: config.likeInterval || config.interval || 5,
maxLikes: config.maxLikesPerDay || config.maxLikes || 200, maxLikes: config.maxLikesPerDay || config.maxLikes || 200,
startTime: config.timeRange?.start || config.startTime || '08:00', startTime: config.timeRange?.start || config.startTime || "08:00",
endTime: config.timeRange?.end || config.endTime || '22:00', endTime: config.timeRange?.end || config.endTime || "22:00",
contentTypes: config.contentTypes || ['text', 'image', 'video'], contentTypes: config.contentTypes || ["text", "image", "video"],
devices: config.devices || [], devices: config.devices || [],
friends: config.friends || [], friends: config.friends || [],
targetTags: config.targetTags || [], targetTags: config.targetTags || [],
friendMaxLikes: config.friendMaxLikes || 10, friendMaxLikes: config.friendMaxLikes || 10,
enableFriendTags: config.enableFriendTags || false, enableFriendTags: config.enableFriendTags || false,
friendTags: config.friendTags || '', friendTags: config.friendTags || "",
}); });
// 处理状态字段,使用双等号允许类型自动转换 // 处理状态字段,使用双等号允许类型自动转换
const status = taskAny.status; const status = taskAny.status;
setAutoEnabled(status === 1 || status === 'running'); setAutoEnabled(status === 1 || status === "running");
} else { } else {
toast({ toast({
title: '获取任务详情失败', title: "获取任务详情失败",
description: '无法找到该任务', description: "无法找到该任务",
variant: 'destructive', variant: "destructive",
}); });
navigate('/workspace/auto-like'); navigate("/workspace/auto-like");
} }
} catch (error) { } catch (error) {
console.error('获取任务详情出错:', error); // 添加错误日志 console.error("获取任务详情出错:", error); // 添加错误日志
toast({ toast({
title: '获取任务详情失败', title: "获取任务详情失败",
description: '请检查网络连接后重试', description: "请检查网络连接后重试",
variant: 'destructive', variant: "destructive",
}); });
navigate('/workspace/auto-like'); navigate("/workspace/auto-like");
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
const handleUpdateFormData = (data: Partial<CreateLikeTaskDataLocal>) => { const handleUpdateFormData = (data: Partial<CreateLikeTaskDataLocal>) => {
setFormData((prev) => ({ ...prev, ...data })); setFormData(prev => ({ ...prev, ...data }));
}; };
const handleNext = () => { const handleNext = () => {
setCurrentStep((prev) => Math.min(prev + 1, 3)); setCurrentStep(prev => Math.min(prev + 1, 3));
// 滚动到顶部 // 滚动到顶部
const mainElement = document.querySelector('main'); const mainElement = document.querySelector("main");
if (mainElement) { if (mainElement) {
mainElement.scrollTo({ top: 0, behavior: 'smooth' }); mainElement.scrollTo({ top: 0, behavior: "smooth" });
} }
}; };
const handlePrev = () => { const handlePrev = () => {
setCurrentStep((prev) => Math.max(prev - 1, 1)); setCurrentStep(prev => Math.max(prev - 1, 1));
// 滚动到顶部 // 滚动到顶部
const mainElement = document.querySelector('main'); const mainElement = document.querySelector("main");
if (mainElement) { if (mainElement) {
mainElement.scrollTo({ top: 0, behavior: 'smooth' }); mainElement.scrollTo({ top: 0, behavior: "smooth" });
} }
}; };
@@ -156,7 +159,7 @@ export default function NewAutoLike() {
// 编辑模式调用更新API // 编辑模式调用更新API
response = await updateAutoLikeTask({ response = await updateAutoLikeTask({
...apiFormData, ...apiFormData,
id: id! id: id!,
}); });
} else { } else {
// 新建模式调用创建API // 新建模式调用创建API
@@ -165,22 +168,24 @@ export default function NewAutoLike() {
if (response.code === 200) { if (response.code === 200) {
toast({ toast({
title: isEditMode ? '更新成功' : '创建成功', title: isEditMode ? "更新成功" : "创建成功",
description: isEditMode ? '自动点赞任务已更新' : '自动点赞任务已创建并开始执行', description: isEditMode
? "自动点赞任务已更新"
: "自动点赞任务已创建并开始执行",
}); });
navigate('/workspace/auto-like'); navigate("/workspace/auto-like");
} else { } else {
toast({ toast({
title: isEditMode ? '更新失败' : '创建失败', title: isEditMode ? "更新失败" : "创建失败",
description: response.msg || '请稍后重试', description: response.msg || "请稍后重试",
variant: 'destructive', variant: "destructive",
}); });
} }
} catch (error) { } catch (error) {
toast({ toast({
title: isEditMode ? '更新失败' : '创建失败', title: isEditMode ? "更新失败" : "创建失败",
description: '请检查网络连接后重试', description: "请检查网络连接后重试",
variant: 'destructive', variant: "destructive",
}); });
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
@@ -190,10 +195,17 @@ export default function NewAutoLike() {
const header = ( const header = (
<div className="sticky top-0 z-10 bg-white pb-4"> <div className="sticky top-0 z-10 bg-white pb-4">
<div className="flex items-center h-14 px-4"> <div className="flex items-center h-14 px-4">
<Button variant="ghost" size="icon" onClick={() => navigate(-1)} className="hover:bg-gray-50"> <Button
variant="ghost"
size="icon"
onClick={() => navigate(-1)}
className="hover:bg-gray-50"
>
<ChevronLeft className="h-6 w-6" /> <ChevronLeft className="h-6 w-6" />
</Button> </Button>
<h1 className="ml-2 text-lg font-medium">{isEditMode ? '编辑自动点赞' : '新建自动点赞'}</h1> <h1 className="ml-2 text-lg font-medium">
{isEditMode ? "编辑自动点赞" : "新建自动点赞"}
</h1>
</div> </div>
<StepIndicator currentStep={currentStep} /> <StepIndicator currentStep={currentStep} />
</div> </div>
@@ -216,7 +228,6 @@ export default function NewAutoLike() {
<Layout header={header}> <Layout header={header}>
<div className="min-h-screen bg-[#F8F9FA]"> <div className="min-h-screen bg-[#F8F9FA]">
<div className="pt-4"> <div className="pt-4">
<div className="mt-8"> <div className="mt-8">
{currentStep === 1 && ( {currentStep === 1 && (
<BasicSettings <BasicSettings
@@ -232,12 +243,16 @@ export default function NewAutoLike() {
<div className="space-y-6 px-6"> <div className="space-y-6 px-6">
<DeviceSelection <DeviceSelection
selectedDevices={formData.devices} selectedDevices={formData.devices}
onSelect={(devices) => handleUpdateFormData({ devices })} onSelect={devices => handleUpdateFormData({ devices })}
placeholder="选择设备" placeholder="选择设备"
/> />
<div className="flex space-x-4"> <div className="flex space-x-4">
<Button variant="outline" className="flex-1 h-12 rounded-xl text-base" onClick={handlePrev}> <Button
variant="outline"
className="flex-1 h-12 rounded-xl text-base"
onClick={handlePrev}
>
</Button> </Button>
<Button <Button
@@ -255,16 +270,23 @@ export default function NewAutoLike() {
<div className="px-6 space-y-6"> <div className="px-6 space-y-6">
<FriendSelection <FriendSelection
selectedFriends={formData.friends || []} selectedFriends={formData.friends || []}
onSelect={(friends) => handleUpdateFormData({ friends })} onSelect={friends => handleUpdateFormData({ friends })}
deviceIds={formData.devices} deviceIds={formData.devices}
placeholder="选择微信好友" placeholder="选择微信好友"
/> />
<div className="flex space-x-4"> <div className="flex space-x-4">
<Button variant="outline" className="flex-1 h-12 rounded-xl text-base" onClick={handlePrev}> <Button
variant="outline"
className="flex-1 h-12 rounded-xl text-base"
onClick={handlePrev}
>
</Button> </Button>
<Button className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm" onClick={handleComplete}> <Button
className="flex-1 h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
onClick={handleComplete}
>
</Button> </Button>
</div> </div>
@@ -284,9 +306,9 @@ interface StepIndicatorProps {
function StepIndicator({ currentStep }: StepIndicatorProps) { function StepIndicator({ currentStep }: StepIndicatorProps) {
const steps = [ const steps = [
{ title: '基础设置', description: '设置点赞规则' }, { title: "基础设置", description: "设置点赞规则" },
{ title: '设备选择', description: '选择执行设备' }, { title: "设备选择", description: "选择执行设备" },
{ title: '人群选择', description: '选择目标人群' }, { title: "人群选择", description: "选择目标人群" },
]; ];
return ( return (
@@ -294,31 +316,44 @@ function StepIndicator({ currentStep }: StepIndicatorProps) {
<div className="relative"> <div className="relative">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{steps.map((step, index) => ( {steps.map((step, index) => (
<div key={index} className="flex flex-col items-center relative z-10"> <div
key={index}
className="flex flex-col items-center relative z-10"
>
<div <div
className={`flex items-center justify-center w-8 h-8 rounded-full ${ className={`flex items-center justify-center w-8 h-8 rounded-full ${
index < currentStep index < currentStep
? 'bg-blue-600 text-white' ? "bg-blue-600 text-white"
: index === currentStep : index === currentStep
? 'border-2 border-blue-600 text-blue-600' ? "border-2 border-blue-600 text-blue-600"
: 'border-2 border-gray-300 text-gray-300' : "border-2 border-gray-300 text-gray-300"
}`} }`}
> >
{index < currentStep ? <Check className="w-5 h-5" /> : index + 1} {index < currentStep ? (
<Check className="w-5 h-5" />
) : (
index + 1
)}
</div> </div>
<div className="text-center mt-2"> <div className="text-center mt-2">
<div className={`text-sm font-medium ${index <= currentStep ? 'text-gray-900' : 'text-gray-400'}`}> <div
className={`text-sm font-medium ${index <= currentStep ? "text-gray-900" : "text-gray-400"}`}
>
{step.title} {step.title}
</div> </div>
<div className="text-xs text-gray-500 mt-1">{step.description}</div> <div className="text-xs text-gray-500 mt-1">
{step.description}
</div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
<div className="absolute top-4 left-0 w-full h-0.5 bg-gray-200 -translate-y-1/2 z-0"> <div className="absolute top-4 left-0 w-full h-0.5 bg-gray-200 -translate-y-1/2 z-0">
<div <div
className="absolute top-0 left-0 h-full bg-blue-600 transition-all duration-300" className="absolute top-0 left-0 h-full bg-blue-600 transition-all duration-300"
style={{ width: `${((currentStep - 1) / (steps.length - 1)) * 100}%` }} style={{
width: `${((currentStep - 1) / (steps.length - 1)) * 100}%`,
}}
></div> ></div>
</div> </div>
</div> </div>
@@ -335,11 +370,17 @@ interface BasicSettingsProps {
setAutoEnabled: (v: boolean) => void; setAutoEnabled: (v: boolean) => void;
} }
function BasicSettings({ formData, onChange, onNext, autoEnabled, setAutoEnabled }: BasicSettingsProps) { function BasicSettings({
formData,
onChange,
onNext,
autoEnabled,
setAutoEnabled,
}: BasicSettingsProps) {
const handleContentTypeChange = (type: ContentType) => { const handleContentTypeChange = (type: ContentType) => {
const currentTypes = [...formData.contentTypes]; const currentTypes = [...formData.contentTypes];
if (currentTypes.includes(type)) { if (currentTypes.includes(type)) {
onChange({ contentTypes: currentTypes.filter((t) => t !== type) }); onChange({ contentTypes: currentTypes.filter(t => t !== type) });
} else { } else {
onChange({ contentTypes: [...currentTypes, type] }); onChange({ contentTypes: [...currentTypes, type] });
} }
@@ -365,11 +406,11 @@ function BasicSettings({ formData, onChange, onNext, autoEnabled, setAutoEnabled
<div className="space-y-6 px-6"> <div className="space-y-6 px-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="task-name"></Label> <Label htmlFor="task-name"></Label>
<Input <Input
id="task-name" id="task-name"
placeholder="请输入任务名称" placeholder="请输入任务名称"
value={formData.name} value={formData.name}
onChange={(e) => onChange({ name: e.target.value })} onChange={e => onChange({ name: e.target.value })}
className="h-12 rounded-xl border-gray-200" className="h-12 rounded-xl border-gray-200"
/> />
</div> </div>
@@ -393,7 +434,9 @@ function BasicSettings({ formData, onChange, onNext, autoEnabled, setAutoEnabled
min={5} min={5}
max={60} max={60}
value={formData.interval.toString()} value={formData.interval.toString()}
onChange={(e) => onChange({ interval: Number.parseInt(e.target.value) || 5 })} onChange={e =>
onChange({ interval: Number.parseInt(e.target.value) || 5 })
}
className="h-12 rounded-none border-x-0 border-gray-200 text-center" className="h-12 rounded-none border-x-0 border-gray-200 text-center"
/> />
<div className="absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none text-gray-500"> <div className="absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none text-gray-500">
@@ -432,7 +475,9 @@ function BasicSettings({ formData, onChange, onNext, autoEnabled, setAutoEnabled
min={10} min={10}
max={500} max={500}
value={formData.maxLikes.toString()} value={formData.maxLikes.toString()}
onChange={(e) => onChange({ maxLikes: Number.parseInt(e.target.value) || 10 })} onChange={e =>
onChange({ maxLikes: Number.parseInt(e.target.value) || 10 })
}
className="h-12 rounded-none border-x-0 border-gray-200 text-center" className="h-12 rounded-none border-x-0 border-gray-200 text-center"
/> />
<div className="absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none text-gray-500"> <div className="absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none text-gray-500">
@@ -459,7 +504,7 @@ function BasicSettings({ formData, onChange, onNext, autoEnabled, setAutoEnabled
<Input <Input
type="time" type="time"
value={formData.startTime} value={formData.startTime}
onChange={(e) => onChange({ startTime: e.target.value })} onChange={e => onChange({ startTime: e.target.value })}
className="h-12 rounded-xl border-gray-200" className="h-12 rounded-xl border-gray-200"
/> />
</div> </div>
@@ -467,7 +512,7 @@ function BasicSettings({ formData, onChange, onNext, autoEnabled, setAutoEnabled
<Input <Input
type="time" type="time"
value={formData.endTime} value={formData.endTime}
onChange={(e) => onChange({ endTime: e.target.value })} onChange={e => onChange({ endTime: e.target.value })}
className="h-12 rounded-xl border-gray-200" className="h-12 rounded-xl border-gray-200"
/> />
</div> </div>
@@ -479,16 +524,16 @@ function BasicSettings({ formData, onChange, onNext, autoEnabled, setAutoEnabled
<Label></Label> <Label></Label>
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
{[ {[
{ id: 'text' as ContentType, label: '文字' }, { id: "text" as ContentType, label: "文字" },
{ id: 'image' as ContentType, label: '图片' }, { id: "image" as ContentType, label: "图片" },
{ id: 'video' as ContentType, label: '视频' }, { id: "video" as ContentType, label: "视频" },
].map((type) => ( ].map(type => (
<div <div
key={type.id} key={type.id}
className={`flex items-center justify-center h-12 rounded-xl border cursor-pointer ${ className={`flex items-center justify-center h-12 rounded-xl border cursor-pointer ${
formData.contentTypes.includes(type.id) formData.contentTypes.includes(type.id)
? 'border-blue-500 bg-blue-50 text-blue-600' ? "border-blue-500 bg-blue-50 text-blue-600"
: 'border-gray-200 text-gray-600' : "border-gray-200 text-gray-600"
}`} }`}
onClick={() => handleContentTypeChange(type.id)} onClick={() => handleContentTypeChange(type.id)}
> >
@@ -507,7 +552,7 @@ function BasicSettings({ formData, onChange, onNext, autoEnabled, setAutoEnabled
<Switch <Switch
id="enable-friend-tags" id="enable-friend-tags"
checked={formData.enableFriendTags} checked={formData.enableFriendTags}
onCheckedChange={(checked) => onChange({ enableFriendTags: checked })} onCheckedChange={checked => onChange({ enableFriendTags: checked })}
/> />
</div> </div>
{formData.enableFriendTags && ( {formData.enableFriendTags && (
@@ -517,7 +562,7 @@ function BasicSettings({ formData, onChange, onNext, autoEnabled, setAutoEnabled
<Input <Input
id="friend-tags" id="friend-tags"
placeholder="请输入标签" placeholder="请输入标签"
value={formData.friendTags || ''} value={formData.friendTags || ""}
onChange={e => onChange({ friendTags: e.target.value })} onChange={e => onChange({ friendTags: e.target.value })}
className="h-12 rounded-xl border-gray-200" className="h-12 rounded-xl border-gray-200"
/> />
@@ -531,22 +576,19 @@ function BasicSettings({ formData, onChange, onNext, autoEnabled, setAutoEnabled
<Label htmlFor="auto-enabled" className="cursor-pointer"> <Label htmlFor="auto-enabled" className="cursor-pointer">
</Label> </Label>
<Switch <Switch
id="auto-enabled" id="auto-enabled"
checked={autoEnabled} checked={autoEnabled}
onCheckedChange={setAutoEnabled} onCheckedChange={setAutoEnabled}
/> />
</div> </div>
<Button onClick={onNext} className="w-full h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"> <Button
onClick={onNext}
className="w-full h-12 bg-blue-600 hover:bg-blue-700 rounded-xl text-base shadow-sm"
>
</Button> </Button>
</div> </div>
); );
} }

View File

@@ -94,11 +94,11 @@ const NewAutoLike: React.FC = () => {
}; };
const handleUpdateFormData = (data: Partial<CreateLikeTaskData>) => { const handleUpdateFormData = (data: Partial<CreateLikeTaskData>) => {
setFormData((prev) => ({ ...prev, ...data })); setFormData(prev => ({ ...prev, ...data }));
}; };
const handleNext = () => { const handleNext = () => {
setCurrentStep((prev) => Math.min(prev + 1, 3)); setCurrentStep(prev => Math.min(prev + 1, 3));
// 滚动到顶部 // 滚动到顶部
const mainElement = document.querySelector("main"); const mainElement = document.querySelector("main");
if (mainElement) { if (mainElement) {
@@ -107,7 +107,7 @@ const NewAutoLike: React.FC = () => {
}; };
const handlePrev = () => { const handlePrev = () => {
setCurrentStep((prev) => Math.max(prev - 1, 1)); setCurrentStep(prev => Math.max(prev - 1, 1));
// 滚动到顶部 // 滚动到顶部
const mainElement = document.querySelector("main"); const mainElement = document.querySelector("main");
if (mainElement) { if (mainElement) {
@@ -154,7 +154,7 @@ const NewAutoLike: React.FC = () => {
<Input <Input
placeholder="请输入任务名称" placeholder="请输入任务名称"
value={formData.name} value={formData.name}
onChange={(e) => handleUpdateFormData({ name: e.target.value })} onChange={e => handleUpdateFormData({ name: e.target.value })}
className={style.input} className={style.input}
/> />
</div> </div>
@@ -176,7 +176,7 @@ const NewAutoLike: React.FC = () => {
min={1} min={1}
max={60} max={60}
value={formData.interval} value={formData.interval}
onChange={(e) => onChange={e =>
handleUpdateFormData({ handleUpdateFormData({
interval: Number.parseInt(e.target.value) || 1, interval: Number.parseInt(e.target.value) || 1,
}) })
@@ -213,7 +213,7 @@ const NewAutoLike: React.FC = () => {
min={1} min={1}
max={500} max={500}
value={formData.maxLikes} value={formData.maxLikes}
onChange={(e) => onChange={e =>
handleUpdateFormData({ handleUpdateFormData({
maxLikes: Number.parseInt(e.target.value) || 1, maxLikes: Number.parseInt(e.target.value) || 1,
}) })
@@ -238,16 +238,14 @@ const NewAutoLike: React.FC = () => {
<Input <Input
type="time" type="time"
value={formData.startTime} value={formData.startTime}
onChange={(e) => onChange={e => handleUpdateFormData({ startTime: e.target.value })}
handleUpdateFormData({ startTime: e.target.value })
}
className={style.inputTime} className={style.inputTime}
/> />
<span className={style.timeSeparator}></span> <span className={style.timeSeparator}></span>
<Input <Input
type="time" type="time"
value={formData.endTime} value={formData.endTime}
onChange={(e) => handleUpdateFormData({ endTime: e.target.value })} onChange={e => handleUpdateFormData({ endTime: e.target.value })}
className={style.inputTime} className={style.inputTime}
/> />
</div> </div>
@@ -256,7 +254,7 @@ const NewAutoLike: React.FC = () => {
<div className={style.formItem}> <div className={style.formItem}>
<div className={style.formLabel}></div> <div className={style.formLabel}></div>
<div className={style.contentTypes}> <div className={style.contentTypes}>
{(["text", "image", "video"] as ContentType[]).map((type) => ( {(["text", "image", "video"] as ContentType[]).map(type => (
<Button <Button
key={type} key={type}
type={ type={
@@ -266,7 +264,7 @@ const NewAutoLike: React.FC = () => {
className={style.contentTypeBtn} className={style.contentTypeBtn}
onClick={() => { onClick={() => {
const newTypes = formData.contentTypes.includes(type) const newTypes = formData.contentTypes.includes(type)
? formData.contentTypes.filter((t) => t !== type) ? formData.contentTypes.filter(t => t !== type)
: [...formData.contentTypes, type]; : [...formData.contentTypes, type];
handleUpdateFormData({ contentTypes: newTypes }); handleUpdateFormData({ contentTypes: newTypes });
}} }}
@@ -282,7 +280,7 @@ const NewAutoLike: React.FC = () => {
<span className={style.switchLabel}></span> <span className={style.switchLabel}></span>
<Switch <Switch
checked={formData.enableFriendTags} checked={formData.enableFriendTags}
onChange={(checked) => onChange={checked =>
handleUpdateFormData({ enableFriendTags: checked }) handleUpdateFormData({ enableFriendTags: checked })
} }
className={style.switch} className={style.switch}
@@ -293,7 +291,7 @@ const NewAutoLike: React.FC = () => {
<Input <Input
placeholder="请输入标签" placeholder="请输入标签"
value={formData.friendTags} value={formData.friendTags}
onChange={(e) => onChange={e =>
handleUpdateFormData({ friendTags: e.target.value }) handleUpdateFormData({ friendTags: e.target.value })
} }
className={style.input} className={style.input}
@@ -331,7 +329,7 @@ const NewAutoLike: React.FC = () => {
<div className={style.formItem}> <div className={style.formItem}>
<DeviceSelection <DeviceSelection
selectedDevices={formData.devices} selectedDevices={formData.devices}
onSelect={(devices) => handleUpdateFormData({ devices })} onSelect={devices => handleUpdateFormData({ devices })}
showInput={true} showInput={true}
showSelectedList={true} showSelectedList={true}
/> />
@@ -362,7 +360,7 @@ const NewAutoLike: React.FC = () => {
<div className={style.formItem}> <div className={style.formItem}>
<FriendSelection <FriendSelection
selectedFriends={formData.friends || []} selectedFriends={formData.friends || []}
onSelect={(friends) => handleUpdateFormData({ friends })} onSelect={friends => handleUpdateFormData({ friends })}
deviceIds={formData.devices} deviceIds={formData.devices}
/> />
</div> </div>

View File

@@ -215,14 +215,16 @@
bottom: 0; bottom: 0;
z-index: 30; z-index: 30;
background: #fff; background: #fff;
box-shadow: 0 -2px 8px rgba(0,0,0,0.04); box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
padding: 16px 24px 24px 24px; padding: 16px 24px 24px 24px;
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 16px; gap: 16px;
} }
.prevBtn, .nextBtn, .completeBtn { .prevBtn,
.nextBtn,
.completeBtn {
height: 44px; height: 44px;
border-radius: 8px; border-radius: 8px;
font-size: 15px; font-size: 15px;

View File

@@ -1,36 +1,29 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import { useParams } from 'react-router-dom'; import { useParams } from "react-router-dom";
import { import { ThumbsUp, RefreshCw, Search } from "lucide-react";
ThumbsUp, import { Card } from "@/components/ui/card";
RefreshCw, import { Button } from "@/components/ui/button";
Search, import { Badge } from "@/components/ui/badge";
} from 'lucide-react'; import { Input } from "@/components/ui/input";
import { Card, } from '@/components/ui/card'; import { Avatar } from "@/components/ui/avatar";
import { Button } from '@/components/ui/button'; import { Skeleton } from "@/components/ui/skeleton";
import { Badge } from '@/components/ui/badge'; import { Separator } from "@/components/ui/separator";
import { Input } from '@/components/ui/input'; import Layout from "@/components/Layout";
import { Avatar } from '@/components/ui/avatar'; import PageHeader from "@/components/PageHeader";
import { Skeleton } from '@/components/ui/skeleton'; import { useToast } from "@/components/ui/toast";
import { Separator } from '@/components/ui/separator'; import "@/components/Layout.css";
import Layout from '@/components/Layout'; import { fetchLikeRecords, LikeRecord } from "@/api/autoLike";
import PageHeader from '@/components/PageHeader';
import { useToast } from '@/components/ui/toast';
import '@/components/Layout.css';
import {
fetchLikeRecords,
LikeRecord,
} from '@/api/autoLike';
// 格式化日期 // 格式化日期
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
try { try {
const date = new Date(dateString); const date = new Date(dateString);
return date.toLocaleString('zh-CN', { return date.toLocaleString("zh-CN", {
year: 'numeric', year: "numeric",
month: '2-digit', month: "2-digit",
day: '2-digit', day: "2-digit",
hour: '2-digit', hour: "2-digit",
minute: '2-digit' minute: "2-digit",
}); });
} catch (error) { } catch (error) {
return dateString; return dateString;
@@ -42,7 +35,7 @@ export default function AutoLikeDetail() {
const { toast } = useToast(); const { toast } = useToast();
const [records, setRecords] = useState<LikeRecord[]>([]); const [records, setRecords] = useState<LikeRecord[]>([]);
const [recordsLoading, setRecordsLoading] = useState(false); const [recordsLoading, setRecordsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState("");
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const pageSize = 10; const pageSize = 10;
@@ -58,9 +51,9 @@ export default function AutoLikeDetail() {
}) })
.catch(() => { .catch(() => {
toast({ toast({
title: '获取点赞记录失败', title: "获取点赞记录失败",
description: '请稍后重试', description: "请稍后重试",
variant: 'destructive', variant: "destructive",
}); });
}) })
.finally(() => setRecordsLoading(false)); .finally(() => setRecordsLoading(false));
@@ -77,9 +70,9 @@ export default function AutoLikeDetail() {
}) })
.catch(() => { .catch(() => {
toast({ toast({
title: '获取点赞记录失败', title: "获取点赞记录失败",
description: '请稍后重试', description: "请稍后重试",
variant: 'destructive', variant: "destructive",
}); });
}); });
}; };
@@ -92,9 +85,9 @@ export default function AutoLikeDetail() {
}) })
.catch(() => { .catch(() => {
toast({ toast({
title: '获取点赞记录失败', title: "获取点赞记录失败",
description: '请稍后重试', description: "请稍后重试",
variant: 'destructive', variant: "destructive",
}); });
}); });
}; };
@@ -108,9 +101,9 @@ export default function AutoLikeDetail() {
}) })
.catch(() => { .catch(() => {
toast({ toast({
title: '获取点赞记录失败', title: "获取点赞记录失败",
description: '请稍后重试', description: "请稍后重试",
variant: 'destructive', variant: "destructive",
}); });
}); });
}; };
@@ -119,23 +112,27 @@ export default function AutoLikeDetail() {
<Layout <Layout
header={ header={
<> <>
<PageHeader <PageHeader title="点赞记录" defaultBackPath="/workspace/auto-like" />
title="点赞记录" <div className="flex items-center space-x-2 px-4 py-4">
defaultBackPath="/workspace/auto-like"
/>
<div className="flex items-center space-x-2 px-4 py-4">
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" /> <Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input <Input
placeholder="搜索好友昵称或内容" placeholder="搜索好友昵称或内容"
className="pl-9" className="pl-9"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={e => setSearchTerm(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()} onKeyDown={e => e.key === "Enter" && handleSearch()}
/> />
</div> </div>
<Button variant="outline" size="icon" onClick={handleRefresh} disabled={recordsLoading}> <Button
<RefreshCw className={`h-4 w-4 ${recordsLoading ? 'animate-spin' : ''}`} /> variant="outline"
size="icon"
onClick={handleRefresh}
disabled={recordsLoading}
>
<RefreshCw
className={`h-4 w-4 ${recordsLoading ? "animate-spin" : ""}`}
/>
</Button> </Button>
</div> </div>
</> </>
@@ -143,37 +140,35 @@ export default function AutoLikeDetail() {
footer={ footer={
<> <>
{records.length > 0 && total > pageSize && ( {records.length > 0 && total > pageSize && (
<div className="flex justify-center py-4"> <div className="flex justify-center py-4">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
disabled={currentPage === 1} disabled={currentPage === 1}
onClick={() => handlePageChange(currentPage - 1)} onClick={() => handlePageChange(currentPage - 1)}
className="mx-1" className="mx-1"
> >
</Button> </Button>
<span className="mx-4 py-2 text-sm text-gray-500"> <span className="mx-4 py-2 text-sm text-gray-500">
{currentPage} {Math.ceil(total / pageSize)} {currentPage} {Math.ceil(total / pageSize)}
</span> </span>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
disabled={currentPage >= Math.ceil(total / pageSize)} disabled={currentPage >= Math.ceil(total / pageSize)}
onClick={() => handlePageChange(currentPage + 1)} onClick={() => handlePageChange(currentPage + 1)}
className="mx-1" className="mx-1"
> >
</Button> </Button>
</div> </div>
)} )}
</> </>
} }
> >
<div className="bg-gray-50 min-h-screen pb-20"> <div className="bg-gray-50 min-h-screen pb-20">
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
{recordsLoading ? ( {recordsLoading ? (
<div className="space-y-4"> <div className="space-y-4">
{Array.from({ length: 3 }).map((_, index) => ( {Array.from({ length: 3 }).map((_, index) => (
@@ -204,25 +199,37 @@ export default function AutoLikeDetail() {
</div> </div>
) : ( ) : (
<> <>
{records.map((record) => ( {records.map(record => (
<div key={record.id} className="p-4 mb-4 bg-white rounded-2xl shadow-sm"> <div
key={record.id}
className="p-4 mb-4 bg-white rounded-2xl shadow-sm"
>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-center space-x-3 max-w-[65%]"> <div className="flex items-center space-x-3 max-w-[65%]">
<Avatar> <Avatar>
<img <img
src={record.friendAvatar || "https://api.dicebear.com/7.x/avataaars/svg?seed=fallback"} src={
record.friendAvatar ||
"https://api.dicebear.com/7.x/avataaars/svg?seed=fallback"
}
alt={record.friendName} alt={record.friendName}
className="w-10 h-10 rounded-full" className="w-10 h-10 rounded-full"
/> />
</Avatar> </Avatar>
<div className="min-w-0"> <div className="min-w-0">
<div className="font-medium truncate" title={record.friendName}> <div
className="font-medium truncate"
title={record.friendName}
>
{record.friendName} {record.friendName}
</div> </div>
<div className="text-sm text-gray-500"></div> <div className="text-sm text-gray-500"></div>
</div> </div>
</div> </div>
<Badge variant="outline" className="bg-blue-50 whitespace-nowrap shrink-0"> <Badge
variant="outline"
className="bg-blue-50 whitespace-nowrap shrink-0"
>
{formatDate(record.momentTime || record.likeTime)} {formatDate(record.momentTime || record.likeTime)}
</Badge> </Badge>
</div> </div>
@@ -233,36 +240,54 @@ export default function AutoLikeDetail() {
{record.content} {record.content}
</p> </p>
)} )}
{Array.isArray(record.resUrls) && record.resUrls.length > 0 && ( {Array.isArray(record.resUrls) &&
<div className={`grid gap-2 ${ record.resUrls.length > 0 && (
record.resUrls.length === 1 ? "grid-cols-1" : <div
record.resUrls.length === 2 ? "grid-cols-2" : className={`grid gap-2 ${
record.resUrls.length <= 3 ? "grid-cols-3" : record.resUrls.length === 1
record.resUrls.length <= 6 ? "grid-cols-3 grid-rows-2" : ? "grid-cols-1"
"grid-cols-3 grid-rows-3" : record.resUrls.length === 2
}`}> ? "grid-cols-2"
{record.resUrls.slice(0, 9).map((image: string, idx: number) => ( : record.resUrls.length <= 3
<div key={idx} className="relative aspect-square rounded-md overflow-hidden"> ? "grid-cols-3"
<img : record.resUrls.length <= 6
src={image} ? "grid-cols-3 grid-rows-2"
alt={`内容图片 ${idx + 1}`} : "grid-cols-3 grid-rows-3"
className="object-cover w-full h-full" }`}
/> >
</div> {record.resUrls
))} .slice(0, 9)
</div> .map((image: string, idx: number) => (
)} <div
key={idx}
className="relative aspect-square rounded-md overflow-hidden"
>
<img
src={image}
alt={`内容图片 ${idx + 1}`}
className="object-cover w-full h-full"
/>
</div>
))}
</div>
)}
</div> </div>
<div className="flex items-center mt-4 p-2 bg-gray-50 rounded-md"> <div className="flex items-center mt-4 p-2 bg-gray-50 rounded-md">
<Avatar className="h-8 w-8 mr-2 shrink-0"> <Avatar className="h-8 w-8 mr-2 shrink-0">
<img <img
src={record.operatorAvatar || "https://api.dicebear.com/7.x/avataaars/svg?seed=operator"} src={
record.operatorAvatar ||
"https://api.dicebear.com/7.x/avataaars/svg?seed=operator"
}
alt={record.operatorName} alt={record.operatorName}
className="w-8 h-8 rounded-full" className="w-8 h-8 rounded-full"
/> />
</Avatar> </Avatar>
<div className="text-sm min-w-0"> <div className="text-sm min-w-0">
<span className="font-medium truncate inline-block max-w-full" title={record.operatorName}> <span
className="font-medium truncate inline-block max-w-full"
title={record.operatorName}
>
{record.operatorName} {record.operatorName}
</span> </span>
<span className="text-gray-500 ml-2"></span> <span className="text-gray-500 ml-2"></span>
@@ -272,8 +297,6 @@ export default function AutoLikeDetail() {
))} ))}
</> </>
)} )}
</div> </div>
</div> </div>
</Layout> </Layout>

View File

@@ -112,7 +112,7 @@ export default function AutoLikeRecord() {
placeholder="搜索好友昵称或内容" placeholder="搜索好友昵称或内容"
className={styles.headerSearchInput} className={styles.headerSearchInput}
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={e => setSearchTerm(e.target.value)}
onPressEnter={handleSearch} onPressEnter={handleSearch}
allowClear allowClear
/> />
@@ -211,7 +211,7 @@ export default function AutoLikeRecord() {
</div> </div>
) : ( ) : (
<> <>
{records.map((record) => ( {records.map(record => (
<div key={record.id} className={styles.recordCard}> <div key={record.id} className={styles.recordCard}>
<div className={styles.recordCardHeader}> <div className={styles.recordCardHeader}>
<div className={styles.recordCardHeaderLeft}> <div className={styles.recordCardHeaderLeft}>

View File

@@ -24,7 +24,9 @@
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
@keyframes spin { @keyframes spin {
100% { transform: rotate(360deg); } 100% {
transform: rotate(360deg);
}
} }
// 分页 // 分页
@@ -146,7 +148,7 @@
.recordCard { .recordCard {
background: #fff; background: #fff;
border-radius: 16px; border-radius: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 16px; padding: 16px;
} }
.recordCardHeader { .recordCardHeader {

View File

@@ -1,5 +1,3 @@
.searchBar { .searchBar {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -18,13 +16,13 @@
padding: 48px 0; padding: 48px 0;
background: #fff; background: #fff;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
} }
.taskCard { .taskCard {
background: #fff; background: #fff;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 20px 16px 12px 16px; padding: 20px 16px 12px 16px;
} }

View File

@@ -23,7 +23,7 @@ const Detail: React.FC = () => {
if (!id) return; if (!id) return;
setLoading(true); setLoading(true);
getGroupPushTaskDetail(id) getGroupPushTaskDetail(id)
.then((res) => { .then(res => {
setTask(res.data || res); // 兼容两种返回格式 setTask(res.data || res); // 兼容两种返回格式
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -195,7 +195,7 @@ const Detail: React.FC = () => {
<div> <div>
<TeamOutlined /> <b></b> <TeamOutlined /> <b></b>
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}> <div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
{task.targetGroups.map((group) => ( {task.targetGroups.map(group => (
<Badge <Badge
key={group} key={group}
color="blue" color="blue"
@@ -234,7 +234,7 @@ const Detail: React.FC = () => {
<div style={{ marginTop: 8 }}> <div style={{ marginTop: 8 }}>
<div></div> <div></div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}> <div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
{task.targetTags.map((tag) => ( {task.targetTags.map(tag => (
<Badge <Badge
key={tag} key={tag}
color="purple" color="purple"

View File

@@ -38,11 +38,11 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
const [values, setValues] = useState(defaultValues); const [values, setValues] = useState(defaultValues);
const handleChange = (field: string, value: any) => { const handleChange = (field: string, value: any) => {
setValues((prev) => ({ ...prev, [field]: value })); setValues(prev => ({ ...prev, [field]: value }));
}; };
const handleCountChange = (increment: boolean) => { const handleCountChange = (increment: boolean) => {
setValues((prev) => ({ setValues(prev => ({
...prev, ...prev,
dailyPushCount: increment dailyPushCount: increment
? prev.dailyPushCount + 1 ? prev.dailyPushCount + 1
@@ -59,7 +59,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
<span style={{ color: "red", marginRight: 4 }}>*</span>: <span style={{ color: "red", marginRight: 4 }}>*</span>:
<Input <Input
value={values.name} value={values.name}
onChange={(e) => handleChange("name", e.target.value)} onChange={e => handleChange("name", e.target.value)}
placeholder="请输入任务名称" placeholder="请输入任务名称"
style={{ marginTop: 4 }} style={{ marginTop: 4 }}
/> />
@@ -71,14 +71,14 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
<Input <Input
type="time" type="time"
value={values.pushTimeStart} value={values.pushTimeStart}
onChange={(e) => handleChange("pushTimeStart", e.target.value)} onChange={e => handleChange("pushTimeStart", e.target.value)}
style={{ width: 120 }} style={{ width: 120 }}
/> />
<span style={{ color: "#888" }}></span> <span style={{ color: "#888" }}></span>
<Input <Input
type="time" type="time"
value={values.pushTimeEnd} value={values.pushTimeEnd}
onChange={(e) => handleChange("pushTimeEnd", e.target.value)} onChange={e => handleChange("pushTimeEnd", e.target.value)}
style={{ width: 120 }} style={{ width: 120 }}
/> />
</div> </div>
@@ -102,7 +102,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
<Input <Input
type="number" type="number"
value={values.dailyPushCount} value={values.dailyPushCount}
onChange={(e) => onChange={e =>
handleChange( handleChange(
"dailyPushCount", "dailyPushCount",
Number.parseInt(e.target.value) || 1 Number.parseInt(e.target.value) || 1
@@ -155,7 +155,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
</span> </span>
<Switch <Switch
checked={values.isLoopPush} checked={values.isLoopPush}
onChange={(checked) => handleChange("isLoopPush", checked)} onChange={checked => handleChange("isLoopPush", checked)}
disabled={loading} disabled={loading}
/> />
</div> </div>
@@ -174,7 +174,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
</span> </span>
<Switch <Switch
checked={values.isImmediatePush} checked={values.isImmediatePush}
onChange={(checked) => handleChange("isImmediatePush", checked)} onChange={checked => handleChange("isImmediatePush", checked)}
disabled={loading} disabled={loading}
/> />
</div> </div>
@@ -206,7 +206,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
</span> </span>
<Switch <Switch
checked={values.isEnabled} checked={values.isEnabled}
onChange={(checked) => handleChange("isEnabled", checked)} onChange={checked => handleChange("isEnabled", checked)}
disabled={loading} disabled={loading}
/> />
</div> </div>

View File

@@ -80,7 +80,7 @@ const ContentSelector: React.FC<ContentSelectorProps> = ({
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [libraries] = useState<ContentLibrary[]>(mockLibraries); const [libraries] = useState<ContentLibrary[]>(mockLibraries);
const filteredLibraries = libraries.filter((library) => const filteredLibraries = libraries.filter(library =>
library.name.toLowerCase().includes(searchTerm.toLowerCase()) library.name.toLowerCase().includes(searchTerm.toLowerCase())
); );
@@ -88,7 +88,7 @@ const ContentSelector: React.FC<ContentSelectorProps> = ({
if (checked) { if (checked) {
onLibrariesChange([...selectedLibraries, library]); onLibrariesChange([...selectedLibraries, library]);
} else { } else {
onLibrariesChange(selectedLibraries.filter((l) => l.id !== library.id)); onLibrariesChange(selectedLibraries.filter(l => l.id !== library.id));
} }
}; };
@@ -101,7 +101,7 @@ const ContentSelector: React.FC<ContentSelectorProps> = ({
}; };
const isLibrarySelected = (libraryId: string) => { const isLibrarySelected = (libraryId: string) => {
return selectedLibraries.some((library) => library.id === libraryId); return selectedLibraries.some(library => library.id === libraryId);
}; };
return ( return (
@@ -114,7 +114,7 @@ const ContentSelector: React.FC<ContentSelectorProps> = ({
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
placeholder="搜索内容库名称" placeholder="搜索内容库名称"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={e => setSearchTerm(e.target.value)}
disabled={loading} disabled={loading}
style={{ marginTop: 4 }} style={{ marginTop: 4 }}
/> />
@@ -139,7 +139,7 @@ const ContentSelector: React.FC<ContentSelectorProps> = ({
</Checkbox> </Checkbox>
</div> </div>
<div style={{ maxHeight: 320, overflowY: "auto" }}> <div style={{ maxHeight: 320, overflowY: "auto" }}>
{filteredLibraries.map((library) => ( {filteredLibraries.map(library => (
<div <div
key={library.id} key={library.id}
style={{ style={{
@@ -156,9 +156,7 @@ const ContentSelector: React.FC<ContentSelectorProps> = ({
> >
<Checkbox <Checkbox
checked={isLibrarySelected(library.id)} checked={isLibrarySelected(library.id)}
onChange={(e) => onChange={e => handleLibraryToggle(library, e.target.checked)}
handleLibraryToggle(library, e.target.checked)
}
disabled={loading} disabled={loading}
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
/> />
@@ -178,7 +176,7 @@ const ContentSelector: React.FC<ContentSelectorProps> = ({
</div> </div>
</div> </div>
<div style={{ display: "flex", gap: 2 }}> <div style={{ display: "flex", gap: 2 }}>
{library.targets.slice(0, 3).map((target) => ( {library.targets.slice(0, 3).map(target => (
<Avatar <Avatar
key={target.id} key={target.id}
src={target.avatar} src={target.avatar}

View File

@@ -99,7 +99,7 @@ const GroupSelector: React.FC<GroupSelectorProps> = ({
const [groups] = useState<WechatGroup[]>(mockGroups); const [groups] = useState<WechatGroup[]>(mockGroups);
const filteredGroups = groups.filter( const filteredGroups = groups.filter(
(group) => group =>
group.name.toLowerCase().includes(searchTerm.toLowerCase()) || group.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
group.serviceAccount.name.toLowerCase().includes(searchTerm.toLowerCase()) group.serviceAccount.name.toLowerCase().includes(searchTerm.toLowerCase())
); );
@@ -108,7 +108,7 @@ const GroupSelector: React.FC<GroupSelectorProps> = ({
if (checked) { if (checked) {
onGroupsChange([...selectedGroups, group]); onGroupsChange([...selectedGroups, group]);
} else { } else {
onGroupsChange(selectedGroups.filter((g) => g.id !== group.id)); onGroupsChange(selectedGroups.filter(g => g.id !== group.id));
} }
}; };
@@ -121,7 +121,7 @@ const GroupSelector: React.FC<GroupSelectorProps> = ({
}; };
const isGroupSelected = (groupId: string) => { const isGroupSelected = (groupId: string) => {
return selectedGroups.some((group) => group.id === groupId); return selectedGroups.some(group => group.id === groupId);
}; };
return ( return (
@@ -134,7 +134,7 @@ const GroupSelector: React.FC<GroupSelectorProps> = ({
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
placeholder="搜索群组名称或客服名称" placeholder="搜索群组名称或客服名称"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={e => setSearchTerm(e.target.value)}
disabled={loading} disabled={loading}
style={{ marginTop: 4 }} style={{ marginTop: 4 }}
/> />
@@ -159,7 +159,7 @@ const GroupSelector: React.FC<GroupSelectorProps> = ({
</Checkbox> </Checkbox>
</div> </div>
<div style={{ maxHeight: 320, overflowY: "auto" }}> <div style={{ maxHeight: 320, overflowY: "auto" }}>
{filteredGroups.map((group) => ( {filteredGroups.map(group => (
<div <div
key={group.id} key={group.id}
style={{ style={{
@@ -174,7 +174,7 @@ const GroupSelector: React.FC<GroupSelectorProps> = ({
> >
<Checkbox <Checkbox
checked={isGroupSelected(group.id)} checked={isGroupSelected(group.id)}
onChange={(e) => handleGroupToggle(group, e.target.checked)} onChange={e => handleGroupToggle(group, e.target.checked)}
disabled={loading} disabled={loading}
style={{ marginRight: 8 }} style={{ marginRight: 8 }}
/> />

View File

@@ -37,16 +37,16 @@ const NewGroupPush: React.FC = () => {
const [isEditMode, setIsEditMode] = useState(false); const [isEditMode, setIsEditMode] = useState(false);
const handleBasicSettingsNext = (values: Partial<FormData>) => { const handleBasicSettingsNext = (values: Partial<FormData>) => {
setFormData((prev) => ({ ...prev, ...values })); setFormData(prev => ({ ...prev, ...values }));
setCurrentStep(2); setCurrentStep(2);
}; };
const handleGroupsChange = (groups: WechatGroup[]) => { const handleGroupsChange = (groups: WechatGroup[]) => {
setFormData((prev) => ({ ...prev, groups })); setFormData(prev => ({ ...prev, groups }));
}; };
const handleLibrariesChange = (contentLibraries: ContentLibrary[]) => { const handleLibrariesChange = (contentLibraries: ContentLibrary[]) => {
setFormData((prev) => ({ ...prev, contentLibraries })); setFormData(prev => ({ ...prev, contentLibraries }));
}; };
const handleSave = async () => { const handleSave = async () => {
@@ -75,8 +75,8 @@ const NewGroupPush: React.FC = () => {
isLoopPush: formData.isLoopPush, isLoopPush: formData.isLoopPush,
isImmediatePush: formData.isImmediatePush, isImmediatePush: formData.isImmediatePush,
isEnabled: formData.isEnabled, isEnabled: formData.isEnabled,
targetGroups: formData.groups.map((g) => g.name), targetGroups: formData.groups.map(g => g.name),
contentLibraries: formData.contentLibraries.map((c) => c.name), contentLibraries: formData.contentLibraries.map(c => c.name),
pushMode: formData.isImmediatePush pushMode: formData.isImmediatePush
? ("immediate" as const) ? ("immediate" as const)
: ("scheduled" as const), : ("scheduled" as const),

View File

@@ -1,4 +1,3 @@
.nav-title { .nav-title {
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
@@ -29,13 +28,13 @@
padding: 48px 0; padding: 48px 0;
background: #fff; background: #fff;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
} }
.taskCard { .taskCard {
background: #fff; background: #fff;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 20px 16px 12px 16px; padding: 20px 16px 12px 16px;
} }

View File

@@ -85,7 +85,7 @@ const GroupPush: React.FC = () => {
}; };
const toggleTaskStatus = async (taskId: string) => { const toggleTaskStatus = async (taskId: string) => {
const task = tasks.find((t) => t.id === taskId); const task = tasks.find(t => t.id === taskId);
if (!task) return; if (!task) return;
const newStatus = task.status === 1 ? 2 : 1; const newStatus = task.status === 1 ? 2 : 1;
await toggleGroupPushTask(taskId, String(newStatus)); await toggleGroupPushTask(taskId, String(newStatus));
@@ -96,7 +96,7 @@ const GroupPush: React.FC = () => {
navigate("/workspace/group-push/new"); navigate("/workspace/group-push/new");
}; };
const filteredTasks = tasks.filter((task) => const filteredTasks = tasks.filter(task =>
task.name.toLowerCase().includes(searchTerm.toLowerCase()) task.name.toLowerCase().includes(searchTerm.toLowerCase())
); );
@@ -175,7 +175,7 @@ const GroupPush: React.FC = () => {
<Input <Input
placeholder="搜索计划名称" placeholder="搜索计划名称"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={e => setSearchTerm(e.target.value)}
prefix={<SearchOutlined />} prefix={<SearchOutlined />}
allowClear allowClear
size="large" size="large"
@@ -211,7 +211,7 @@ const GroupPush: React.FC = () => {
</Button> </Button>
</Card> </Card>
) : ( ) : (
filteredTasks.map((task) => ( filteredTasks.map(task => (
<Card key={task.id} className={styles.taskCard}> <Card key={task.id} className={styles.taskCard}>
<div className={styles.taskHeader}> <div className={styles.taskHeader}>
<div className={styles.taskTitle}> <div className={styles.taskTitle}>
@@ -328,7 +328,7 @@ const GroupPush: React.FC = () => {
<div <div
style={{ display: "flex", flexWrap: "wrap", gap: 4 }} style={{ display: "flex", flexWrap: "wrap", gap: 4 }}
> >
{task.targetGroups.map((group) => ( {task.targetGroups.map(group => (
<Badge <Badge
key={group} key={group}
color="blue" color="blue"
@@ -375,7 +375,7 @@ const GroupPush: React.FC = () => {
gap: 4, gap: 4,
}} }}
> >
{task.targetTags.map((tag) => ( {task.targetTags.map(tag => (
<Badge <Badge
key={tag} key={tag}
color="purple" color="purple"

View File

@@ -1,13 +1,13 @@
import request from '@/api/request'; import request from "@/api/request";
// 设备统计 // 设备统计
export function getDeviceStats() { export function getDeviceStats() {
return request('/v1/dashboard/device-stats', {}, 'GET'); return request("/v1/dashboard/device-stats", {}, "GET");
} }
// 微信号统计 // 微信号统计
export function getWechatStats() { export function getWechatStats() {
return request('/v1/dashboard/wechat-stats', {}, 'GET'); return request("/v1/dashboard/wechat-stats", {}, "GET");
} }
// 你可以根据需要继续添加其他接口 // 你可以根据需要继续添加其他接口

Some files were not shown because too many files have changed in this diff Show More