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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
27
nkebao/.gitattributes
vendored
Normal 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
13
nkebao/.prettierrc
Normal 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
11
nkebao/.vscode/extensions.json
vendored
Normal 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
45
nkebao/.vscode/settings.json
vendored
Normal 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
6459
nkebao/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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}\""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.popupHeader {
|
.popupHeader {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.refreshBtn {
|
.refreshBtn {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
112
nkebao/src/components/Upload/README.md
Normal file
112
nkebao/src/components/Upload/README.md
Normal 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 默认样式
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
- 文件类型不匹配时会显示错误提示
|
||||||
|
- 文件大小超限时会显示错误提示
|
||||||
|
- 上传失败时会显示错误提示
|
||||||
|
- 网络错误时会显示错误提示
|
||||||
145
nkebao/src/components/Upload/VideoUpload.tsx
Normal file
145
nkebao/src/components/Upload/VideoUpload.tsx
Normal 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;
|
||||||
108
nkebao/src/components/Upload/index.module.scss
Normal file
108
nkebao/src/components/Upload/index.module.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
nkebao/src/components/Upload/index.tsx
Normal file
134
nkebao/src/components/Upload/index.tsx
Normal 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;
|
||||||
@@ -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");
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"]}>
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 内容库类型
|
// 内容库类型
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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%;
|
||||||
|
|||||||
@@ -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"]}
|
||||||
|
|||||||
@@ -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={{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
< 返回
|
||||||
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"]}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"] : ""}`}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"]} ${
|
||||||
|
|||||||
@@ -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));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染当前步骤内容
|
// 渲染当前步骤内容
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.autoGroupForm {
|
.autoGroupForm {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: #f7f8fa;
|
background: #f7f8fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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="请输入群描述"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"]}>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user