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
@@ -1,43 +1,49 @@
|
|||||||
{
|
{
|
||||||
"name": "cunkebao",
|
"name": "cunkebao",
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.6.1",
|
"@ant-design/icons": "^5.6.1",
|
||||||
"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",
|
||||||
"echarts": "^5.6.0",
|
"dayjs": "^1.11.13",
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts": "^5.6.0",
|
||||||
"react": "^18.2.0",
|
"echarts-for-react": "^3.0.2",
|
||||||
"react-dom": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-router-dom": "^6.20.0",
|
"react-dom": "^18.2.0",
|
||||||
"vconsole": "^3.15.1",
|
"react-router-dom": "^6.20.0",
|
||||||
"zustand": "^5.0.6"
|
"vconsole": "^3.15.1",
|
||||||
},
|
"zustand": "^5.0.6"
|
||||||
"devDependencies": {
|
},
|
||||||
"@types/node": "^24.0.14",
|
"devDependencies": {
|
||||||
"@types/react": "^19.1.8",
|
"@types/node": "^24.0.14",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react": "^19.1.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.7.0",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@typescript-eslint/parser": "^7.7.0",
|
"@typescript-eslint/eslint-plugin": "^7.7.0",
|
||||||
"@vitejs/plugin-react": "^4.6.0",
|
"@typescript-eslint/parser": "^7.7.0",
|
||||||
"eslint": "^8.57.0",
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-react": "^7.34.1",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"postcss": "^8.4.38",
|
"eslint-plugin-react": "^7.34.1",
|
||||||
"postcss-pxtorem": "^6.0.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"prettier": "^3.2.5",
|
"postcss": "^8.4.38",
|
||||||
"sass": "^1.75.0",
|
"postcss-pxtorem": "^6.0.0",
|
||||||
"typescript": "^5.4.5",
|
"prettier": "^3.2.5",
|
||||||
"vite": "^7.0.5"
|
"sass": "^1.75.0",
|
||||||
},
|
"typescript": "^5.4.5",
|
||||||
"scripts": {
|
"vite": "^7.0.5"
|
||||||
"dev": "vite",
|
},
|
||||||
"build": "vite build",
|
"scripts": {
|
||||||
"preview": "vite preview",
|
"dev": "vite",
|
||||||
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix"
|
"build": "vite build",
|
||||||
}
|
"build:check": "tsc && vite build",
|
||||||
}
|
"preview": "vite preview",
|
||||||
|
"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}\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import AppRouter from "@/router";
|
import AppRouter from "@/router";
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppRouter />
|
<AppRouter />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
import request from "./request";
|
import request from "./request";
|
||||||
|
|
||||||
// 获取设备列表
|
// 获取设备列表
|
||||||
export const fetchDeviceList = (params: {
|
export const fetchDeviceList = (params: {
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
}) => request("/v1/devices", params, "GET");
|
}) => request("/v1/devices", params, "GET");
|
||||||
|
|
||||||
// 获取设备详情
|
// 获取设备详情
|
||||||
export const fetchDeviceDetail = (id: string | number) =>
|
export const fetchDeviceDetail = (id: string | number) =>
|
||||||
request(`/v1/devices/${id}`);
|
request(`/v1/devices/${id}`);
|
||||||
|
|
||||||
// 获取设备关联微信账号
|
// 获取设备关联微信账号
|
||||||
export const fetchDeviceRelatedAccounts = (id: string | number) =>
|
export const fetchDeviceRelatedAccounts = (id: string | number) =>
|
||||||
request(`/v1/wechats/related-device/${id}`);
|
request(`/v1/wechats/related-device/${id}`);
|
||||||
|
|
||||||
// 获取设备操作日志
|
// 获取设备操作日志
|
||||||
export const fetchDeviceHandleLogs = (
|
export const fetchDeviceHandleLogs = (
|
||||||
id: string | number,
|
id: string | number,
|
||||||
page = 1,
|
page = 1,
|
||||||
limit = 10
|
limit = 10
|
||||||
) => request(`/v1/devices/${id}/handle-logs`, { page, limit }, "GET");
|
) => request(`/v1/devices/${id}/handle-logs`, { page, limit }, "GET");
|
||||||
|
|
||||||
// 更新设备任务配置
|
// 更新设备任务配置
|
||||||
export const updateDeviceTaskConfig = (config: {
|
export const updateDeviceTaskConfig = (config: {
|
||||||
deviceId: string | number;
|
deviceId: string | number;
|
||||||
autoAddFriend?: boolean;
|
autoAddFriend?: boolean;
|
||||||
autoReply?: boolean;
|
autoReply?: boolean;
|
||||||
momentsSync?: boolean;
|
momentsSync?: boolean;
|
||||||
aiChat?: boolean;
|
aiChat?: boolean;
|
||||||
}) => request("/v1/devices/task-config", config, "POST");
|
}) => request("/v1/devices/task-config", config, "POST");
|
||||||
|
|
||||||
// 删除设备
|
// 删除设备
|
||||||
export const deleteDevice = (id: number) =>
|
export const deleteDevice = (id: number) =>
|
||||||
request(`/v1/devices/${id}`, undefined, "DELETE");
|
request(`/v1/devices/${id}`, undefined, "DELETE");
|
||||||
|
|
||||||
// 获取设备二维码
|
// 获取设备二维码
|
||||||
export const fetchDeviceQRCode = (accountId: string) =>
|
export const fetchDeviceQRCode = (accountId: string) =>
|
||||||
request("/v1/api/device/add", { accountId }, "POST");
|
request("/v1/api/device/add", { accountId }, "POST");
|
||||||
|
|
||||||
// 通过IMEI添加设备
|
// 通过IMEI添加设备
|
||||||
export const addDeviceByImei = (imei: string, name: string) =>
|
export const addDeviceByImei = (imei: string, name: string) =>
|
||||||
request("/v1/api/device/add-by-imei", { imei, name }, "POST");
|
request("/v1/api/device/add-by-imei", { imei, name }, "POST");
|
||||||
|
|||||||
@@ -1,84 +1,90 @@
|
|||||||
import axios, {
|
import axios, {
|
||||||
AxiosInstance,
|
AxiosInstance,
|
||||||
AxiosRequestConfig,
|
AxiosRequestConfig,
|
||||||
Method,
|
Method,
|
||||||
AxiosResponse,
|
AxiosResponse,
|
||||||
} from "axios";
|
} from "axios";
|
||||||
import { Toast } from "antd-mobile";
|
import { Toast } from "antd-mobile";
|
||||||
|
|
||||||
const DEFAULT_DEBOUNCE_GAP = 1000;
|
const DEFAULT_DEBOUNCE_GAP = 1000;
|
||||||
const debounceMap = new Map<string, number>();
|
const debounceMap = new Map<string, number>();
|
||||||
|
|
||||||
const instance: AxiosInstance = axios.create({
|
const instance: AxiosInstance = axios.create({
|
||||||
baseURL: (import.meta as any).env?.VITE_API_BASE_URL || "/api",
|
baseURL: (import.meta as any).env?.VITE_API_BASE_URL || "/api",
|
||||||
timeout: 20000,
|
timeout: 20000,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
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 || {};
|
||||||
config.headers["Authorization"] = `Bearer ${token}`;
|
config.headers["Authorization"] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
instance.interceptors.response.use(
|
instance.interceptors.response.use(
|
||||||
(res: AxiosResponse) => {
|
(res: AxiosResponse) => {
|
||||||
const { code, success, msg } = res.data || {};
|
const { code, success, msg } = res.data || {};
|
||||||
if (code === 200 || success) {
|
if (code === 200 || success) {
|
||||||
return res.data.data ?? res.data;
|
return res.data.data ?? res.data;
|
||||||
}
|
}
|
||||||
Toast.show({ content: msg || "接口错误", position: "top" });
|
Toast.show({ content: msg || "接口错误", position: "top" });
|
||||||
if (code === 401) {
|
if (code === 401) {
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
const currentPath = window.location.pathname + window.location.search;
|
const currentPath = window.location.pathname + window.location.search;
|
||||||
if (currentPath === "/login") {
|
if (currentPath === "/login") {
|
||||||
window.location.href = "/login";
|
window.location.href = "/login";
|
||||||
} else {
|
} else {
|
||||||
window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`;
|
window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export function request(
|
export function request(
|
||||||
url: string,
|
url: string,
|
||||||
data?: any,
|
data?: any,
|
||||||
method: Method = "GET",
|
method: Method = "GET",
|
||||||
config?: AxiosRequestConfig,
|
config?: AxiosRequestConfig,
|
||||||
debounceGap?: number
|
debounceGap?: number
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const gap =
|
const gap =
|
||||||
typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP;
|
typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP;
|
||||||
const key = `${method}_${url}_${JSON.stringify(data)}`;
|
const key = `${method}_${url}_${JSON.stringify(data)}`;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const last = debounceMap.get(key) || 0;
|
const last = debounceMap.get(key) || 0;
|
||||||
if (gap > 0 && now - last < gap) {
|
if (gap > 0 && now - last < gap) {
|
||||||
// Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' });
|
// Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' });
|
||||||
return Promise.reject("请求过于频繁,请稍后再试");
|
return Promise.reject("请求过于频繁,请稍后再试");
|
||||||
}
|
}
|
||||||
debounceMap.set(key, now);
|
debounceMap.set(key, now);
|
||||||
|
|
||||||
const axiosConfig: AxiosRequestConfig = {
|
const axiosConfig: AxiosRequestConfig = {
|
||||||
url,
|
url,
|
||||||
method,
|
method,
|
||||||
...config,
|
...config,
|
||||||
};
|
};
|
||||||
if (method.toUpperCase() === "GET") {
|
|
||||||
axiosConfig.params = data;
|
// 如果是FormData,不设置Content-Type,让浏览器自动设置
|
||||||
} else {
|
if (data instanceof FormData) {
|
||||||
axiosConfig.data = data;
|
delete axiosConfig.headers?.["Content-Type"];
|
||||||
}
|
}
|
||||||
return instance(axiosConfig);
|
|
||||||
}
|
if (method.toUpperCase() === "GET") {
|
||||||
|
axiosConfig.params = data;
|
||||||
export default request;
|
} else {
|
||||||
|
axiosConfig.data = data;
|
||||||
|
}
|
||||||
|
return instance(axiosConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default request;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import request from "@/api/request";
|
import request from "@/api/request";
|
||||||
|
|
||||||
// 获取好友列表
|
// 获取好友列表
|
||||||
export function getAccountList(params: {
|
export function getAccountList(params: {
|
||||||
page: number;
|
page: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
}) {
|
}) {
|
||||||
return request("/v1/workbench/account-list", params, "GET");
|
return request("/v1/workbench/account-list", params, "GET");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import request from "@/api/request";
|
import request from "@/api/request";
|
||||||
|
|
||||||
export function getContentLibraryList(params: any) {
|
export function getContentLibraryList(params: any) {
|
||||||
return request("/v1/content/library/list", params, "GET");
|
return request("/v1/content/library/list", params, "GET");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +1,117 @@
|
|||||||
.inputWrapper {
|
.inputWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.selectedListWindow {
|
.selectedListWindow {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
border: 1px solid #e5e6eb;
|
border: 1px solid #e5e6eb;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.selectedListRow {
|
.selectedListRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.libraryList {
|
.libraryList {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.libraryListInner {
|
.libraryListInner {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.libraryItem {
|
.libraryItem {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid #f0f0f0;
|
border: 1px solid #f0f0f0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #f5f6fa;
|
background: #f5f6fa;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.checkboxWrapper {
|
.checkboxWrapper {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
.checkboxSelected {
|
.checkboxSelected {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #1677ff;
|
background: #1677ff;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.checkboxUnselected {
|
.checkboxUnselected {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #e5e6eb;
|
border: 1px solid #e5e6eb;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.checkboxDot {
|
.checkboxDot {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.libraryInfo {
|
.libraryInfo {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.libraryHeader {
|
.libraryHeader {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
.libraryName {
|
.libraryName {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #222;
|
color: #222;
|
||||||
}
|
}
|
||||||
.typeTag {
|
.typeTag {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #1677ff;
|
color: #1677ff;
|
||||||
border: 1px solid #1677ff;
|
border: 1px solid #1677ff;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
background: #f4f8ff;
|
background: #f4f8ff;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.libraryMeta {
|
.libraryMeta {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
.libraryDesc {
|
.libraryDesc {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #888;
|
color: #888;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
.loadingBox {
|
.loadingBox {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.loadingText {
|
.loadingText {
|
||||||
color: #888;
|
color: #888;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
.emptyBox {
|
.emptyBox {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
.emptyText {
|
.emptyText {
|
||||||
color: #888;
|
color: #888;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,343 +1,343 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { SearchOutlined, DeleteOutlined } from "@ant-design/icons";
|
import { SearchOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||||
import { Button, Input } from "antd";
|
import { Button, Input } from "antd";
|
||||||
import { Popup, Checkbox } from "antd-mobile";
|
import { Popup, Checkbox } from "antd-mobile";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
import PopupHeader from "@/components/PopuLayout/header";
|
import PopupHeader from "@/components/PopuLayout/header";
|
||||||
import PopupFooter from "@/components/PopuLayout/footer";
|
import PopupFooter from "@/components/PopuLayout/footer";
|
||||||
import { getContentLibraryList } from "./api";
|
import { getContentLibraryList } from "./api";
|
||||||
|
|
||||||
// 内容库接口类型
|
// 内容库接口类型
|
||||||
interface ContentLibraryItem {
|
interface ContentLibraryItem {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
sourceType?: number; // 1=文本 2=图片 3=视频
|
sourceType?: number; // 1=文本 2=图片 3=视频
|
||||||
creatorName?: string;
|
creatorName?: string;
|
||||||
updateTime?: string;
|
updateTime?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 类型标签文本
|
// 类型标签文本
|
||||||
const getTypeText = (type?: number) => {
|
const getTypeText = (type?: number) => {
|
||||||
if (type === 1) return "文本";
|
if (type === 1) return "文本";
|
||||||
if (type === 2) return "图片";
|
if (type === 2) return "图片";
|
||||||
if (type === 3) return "视频";
|
if (type === 3) return "视频";
|
||||||
return "未知";
|
return "未知";
|
||||||
};
|
};
|
||||||
|
|
||||||
// 时间格式化
|
// 时间格式化
|
||||||
const formatDate = (dateStr?: string) => {
|
const formatDate = (dateStr?: string) => {
|
||||||
if (!dateStr) return "-";
|
if (!dateStr) return "-";
|
||||||
const d = new Date(dateStr);
|
const d = new Date(dateStr);
|
||||||
if (isNaN(d.getTime())) return "-";
|
if (isNaN(d.getTime())) return "-";
|
||||||
return `${d.getFullYear()}/${(d.getMonth() + 1)
|
return `${d.getFullYear()}/${(d.getMonth() + 1)
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(2, "0")}/${d.getDate().toString().padStart(2, "0")} ${d
|
.padStart(2, "0")}/${d.getDate().toString().padStart(2, "0")} ${d
|
||||||
.getHours()
|
.getHours()
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}:${d
|
.padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}:${d
|
||||||
.getSeconds()
|
.getSeconds()
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(2, "0")}`;
|
.padStart(2, "0")}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 组件属性接口
|
// 组件属性接口
|
||||||
interface ContentLibrarySelectionProps {
|
interface ContentLibrarySelectionProps {
|
||||||
selectedLibraries: (string | number)[];
|
selectedLibraries: (string | number)[];
|
||||||
onSelect: (libraries: string[]) => void;
|
onSelect: (libraries: string[]) => void;
|
||||||
onSelectDetail?: (libraries: ContentLibraryItem[]) => void;
|
onSelectDetail?: (libraries: ContentLibraryItem[]) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
onVisibleChange?: (visible: boolean) => void;
|
onVisibleChange?: (visible: boolean) => void;
|
||||||
selectedListMaxHeight?: number;
|
selectedListMaxHeight?: number;
|
||||||
showInput?: boolean;
|
showInput?: boolean;
|
||||||
showSelectedList?: boolean;
|
showSelectedList?: boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
onConfirm?: (
|
onConfirm?: (
|
||||||
selectedIds: string[],
|
selectedIds: string[],
|
||||||
selectedItems: ContentLibraryItem[]
|
selectedItems: ContentLibraryItem[]
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ContentLibrarySelection({
|
export default function ContentLibrarySelection({
|
||||||
selectedLibraries,
|
selectedLibraries,
|
||||||
onSelect,
|
onSelect,
|
||||||
onSelectDetail,
|
onSelectDetail,
|
||||||
placeholder = "选择内容库",
|
placeholder = "选择内容库",
|
||||||
className = "",
|
className = "",
|
||||||
visible,
|
visible,
|
||||||
onVisibleChange,
|
onVisibleChange,
|
||||||
selectedListMaxHeight = 300,
|
selectedListMaxHeight = 300,
|
||||||
showInput = true,
|
showInput = true,
|
||||||
showSelectedList = true,
|
showSelectedList = true,
|
||||||
readonly = false,
|
readonly = false,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}: ContentLibrarySelectionProps) {
|
}: ContentLibrarySelectionProps) {
|
||||||
const [popupVisible, setPopupVisible] = useState(false);
|
const [popupVisible, setPopupVisible] = useState(false);
|
||||||
const [libraries, setLibraries] = useState<ContentLibraryItem[]>([]);
|
const [libraries, setLibraries] = useState<ContentLibraryItem[]>([]);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const [totalLibraries, setTotalLibraries] = useState(0);
|
const [totalLibraries, setTotalLibraries] = useState(0);
|
||||||
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));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 受控弹窗逻辑
|
// 受控弹窗逻辑
|
||||||
const realVisible = visible !== undefined ? visible : popupVisible;
|
const realVisible = visible !== undefined ? visible : popupVisible;
|
||||||
const setRealVisible = (v: boolean) => {
|
const setRealVisible = (v: boolean) => {
|
||||||
if (onVisibleChange) onVisibleChange(v);
|
if (onVisibleChange) onVisibleChange(v);
|
||||||
if (visible === undefined) setPopupVisible(v);
|
if (visible === undefined) setPopupVisible(v);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开弹窗
|
// 打开弹窗
|
||||||
const openPopup = () => {
|
const openPopup = () => {
|
||||||
if (readonly) return;
|
if (readonly) return;
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
setRealVisible(true);
|
setRealVisible(true);
|
||||||
fetchLibraries(1, "");
|
fetchLibraries(1, "");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 当页码变化时,拉取对应页数据(弹窗已打开时)
|
// 当页码变化时,拉取对应页数据(弹窗已打开时)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (realVisible && currentPage !== 1) {
|
if (realVisible && currentPage !== 1) {
|
||||||
fetchLibraries(currentPage, searchQuery);
|
fetchLibraries(currentPage, searchQuery);
|
||||||
}
|
}
|
||||||
}, [currentPage, realVisible, searchQuery]);
|
}, [currentPage, realVisible, searchQuery]);
|
||||||
|
|
||||||
// 搜索防抖
|
// 搜索防抖
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!realVisible) return;
|
if (!realVisible) return;
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
fetchLibraries(1, searchQuery);
|
fetchLibraries(1, searchQuery);
|
||||||
}, 500);
|
}, 500);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [searchQuery, realVisible]);
|
}, [searchQuery, realVisible]);
|
||||||
|
|
||||||
// 获取内容库列表API
|
// 获取内容库列表API
|
||||||
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,
|
||||||
};
|
};
|
||||||
if (keyword.trim()) {
|
if (keyword.trim()) {
|
||||||
params.keyword = keyword.trim();
|
params.keyword = keyword.trim();
|
||||||
}
|
}
|
||||||
const response = await getContentLibraryList(params);
|
const response = await getContentLibraryList(params);
|
||||||
if (response && response.list) {
|
if (response && response.list) {
|
||||||
setLibraries(response.list);
|
setLibraries(response.list);
|
||||||
setTotalLibraries(response.total || 0);
|
setTotalLibraries(response.total || 0);
|
||||||
setTotalPages(Math.ceil((response.total || 0) / 20));
|
setTotalPages(Math.ceil((response.total || 0) / 20));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取内容库列表失败:", error);
|
console.error("获取内容库列表失败:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理内容库选择
|
// 处理内容库选择
|
||||||
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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取显示文本
|
// 获取显示文本
|
||||||
const getDisplayText = () => {
|
const getDisplayText = () => {
|
||||||
if (selectedLibraries.length === 0) return "";
|
if (selectedLibraries.length === 0) return "";
|
||||||
return `已选择 ${selectedLibraries.length} 个内容库`;
|
return `已选择 ${selectedLibraries.length} 个内容库`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 确认选择
|
// 确认选择
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
if (onConfirm) {
|
if (onConfirm) {
|
||||||
onConfirm(selectedLibraries, selectedLibraryObjs);
|
onConfirm(selectedLibraries, selectedLibraryObjs);
|
||||||
}
|
}
|
||||||
setRealVisible(false);
|
setRealVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 输入框 */}
|
{/* 输入框 */}
|
||||||
{showInput && (
|
{showInput && (
|
||||||
<div className={`${style.inputWrapper} ${className}`}>
|
<div className={`${style.inputWrapper} ${className}`}>
|
||||||
<Input
|
<Input
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={getDisplayText()}
|
value={getDisplayText()}
|
||||||
onClick={openPopup}
|
onClick={openPopup}
|
||||||
prefix={<SearchOutlined />}
|
prefix={<SearchOutlined />}
|
||||||
allowClear={!readonly}
|
allowClear={!readonly}
|
||||||
size="large"
|
size="large"
|
||||||
readOnly={readonly}
|
readOnly={readonly}
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
style={
|
style={
|
||||||
readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {}
|
readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 已选内容库列表窗口 */}
|
{/* 已选内容库列表窗口 */}
|
||||||
{showSelectedList && selectedLibraryObjs.length > 0 && (
|
{showSelectedList && selectedLibraryObjs.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className={style.selectedListWindow}
|
className={style.selectedListWindow}
|
||||||
style={{
|
style={{
|
||||||
maxHeight: selectedListMaxHeight,
|
maxHeight: selectedListMaxHeight,
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
border: "1px solid #e5e6eb",
|
border: "1px solid #e5e6eb",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
background: "#fff",
|
background: "#fff",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedLibraryObjs.map((item) => (
|
{selectedLibraryObjs.map(item => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className={style.selectedListRow}
|
className={style.selectedListRow}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: "4px 8px",
|
padding: "4px 8px",
|
||||||
borderBottom: "1px solid #f0f0f0",
|
borderBottom: "1px solid #f0f0f0",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.name || item.id}
|
{item.name || item.id}
|
||||||
</div>
|
</div>
|
||||||
{!readonly && (
|
{!readonly && (
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
color: "#ff4d4f",
|
color: "#ff4d4f",
|
||||||
border: "none",
|
border: "none",
|
||||||
background: "none",
|
background: "none",
|
||||||
minWidth: 24,
|
minWidth: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
onClick={() => handleRemoveLibrary(item.id)}
|
onClick={() => handleRemoveLibrary(item.id)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 弹窗 */}
|
{/* 弹窗 */}
|
||||||
<Popup
|
<Popup
|
||||||
visible={realVisible && !readonly}
|
visible={realVisible && !readonly}
|
||||||
onMaskClick={() => setRealVisible(false)}
|
onMaskClick={() => setRealVisible(false)}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
bodyStyle={{ height: "100vh" }}
|
bodyStyle={{ height: "100vh" }}
|
||||||
>
|
>
|
||||||
<Layout
|
<Layout
|
||||||
header={
|
header={
|
||||||
<PopupHeader
|
<PopupHeader
|
||||||
title="选择内容库"
|
title="选择内容库"
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
setSearchQuery={setSearchQuery}
|
setSearchQuery={setSearchQuery}
|
||||||
searchPlaceholder="搜索内容库"
|
searchPlaceholder="搜索内容库"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onRefresh={() => fetchLibraries(currentPage, searchQuery)}
|
onRefresh={() => fetchLibraries(currentPage, searchQuery)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
<PopupFooter
|
<PopupFooter
|
||||||
total={totalLibraries}
|
total={totalLibraries}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
selectedCount={selectedLibraries.length}
|
selectedCount={selectedLibraries.length}
|
||||||
onPageChange={setCurrentPage}
|
onPageChange={setCurrentPage}
|
||||||
onCancel={() => setRealVisible(false)}
|
onCancel={() => setRealVisible(false)}
|
||||||
onConfirm={handleConfirm}
|
onConfirm={handleConfirm}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={style.libraryList}>
|
<div className={style.libraryList}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className={style.loadingBox}>
|
<div className={style.loadingBox}>
|
||||||
<div className={style.loadingText}>加载中...</div>
|
<div className={style.loadingText}>加载中...</div>
|
||||||
</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)}
|
||||||
onChange={() => !readonly && handleLibraryToggle(item.id)}
|
onChange={() => !readonly && handleLibraryToggle(item.id)}
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
className={style.checkboxWrapper}
|
className={style.checkboxWrapper}
|
||||||
/>
|
/>
|
||||||
<div className={style.libraryInfo}>
|
<div className={style.libraryInfo}>
|
||||||
<div className={style.libraryHeader}>
|
<div className={style.libraryHeader}>
|
||||||
<span className={style.libraryName}>{item.name}</span>
|
<span className={style.libraryName}>{item.name}</span>
|
||||||
<span className={style.typeTag}>
|
<span className={style.typeTag}>
|
||||||
{getTypeText(item.sourceType)}
|
{getTypeText(item.sourceType)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.libraryMeta}>
|
<div className={style.libraryMeta}>
|
||||||
<div>创建人: {item.creatorName || "-"}</div>
|
<div>创建人: {item.creatorName || "-"}</div>
|
||||||
<div>更新时间: {formatDate(item.updateTime)}</div>
|
<div>更新时间: {formatDate(item.updateTime)}</div>
|
||||||
</div>
|
</div>
|
||||||
{item.description && (
|
{item.description && (
|
||||||
<div className={style.libraryDesc}>
|
<div className={style.libraryDesc}>
|
||||||
{item.description}
|
{item.description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={style.emptyBox}>
|
<div className={style.emptyBox}>
|
||||||
<div className={style.emptyText}>
|
<div className={style.emptyText}>
|
||||||
{searchQuery
|
{searchQuery
|
||||||
? `没有找到包含"${searchQuery}"的内容库`
|
? `没有找到包含"${searchQuery}"的内容库`
|
||||||
: "没有找到内容库"}
|
: "没有找到内容库"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Popup>
|
</Popup>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import request from "@/api/request";
|
import request from "@/api/request";
|
||||||
|
|
||||||
// 获取设备列表
|
// 获取设备列表
|
||||||
export function getDeviceList(params: {
|
export function getDeviceList(params: {
|
||||||
page: number;
|
page: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
}) {
|
}) {
|
||||||
return request("/v1/devices", params, "GET");
|
return request("/v1/devices", params, "GET");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
// 设备选择项接口
|
// 设备选择项接口
|
||||||
export interface DeviceSelectionItem {
|
export interface DeviceSelectionItem {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
imei: string;
|
imei: string;
|
||||||
wechatId: string;
|
wechatId: string;
|
||||||
status: "online" | "offline";
|
status: "online" | "offline";
|
||||||
wxid?: string;
|
wxid?: string;
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
usedInPlans?: number;
|
usedInPlans?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件属性接口
|
// 组件属性接口
|
||||||
export interface DeviceSelectionProps {
|
export interface DeviceSelectionProps {
|
||||||
selectedDevices: string[];
|
selectedDevices: string[];
|
||||||
onSelect: (devices: string[]) => void;
|
onSelect: (devices: string[]) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
mode?: "input" | "dialog"; // 新增,默认input
|
mode?: "input" | "dialog"; // 新增,默认input
|
||||||
open?: boolean; // 仅mode=dialog时生效
|
open?: boolean; // 仅mode=dialog时生效
|
||||||
onOpenChange?: (open: boolean) => void; // 仅mode=dialog时生效
|
onOpenChange?: (open: boolean) => void; // 仅mode=dialog时生效
|
||||||
selectedListMaxHeight?: number; // 新增,已选列表最大高度,默认500
|
selectedListMaxHeight?: number; // 新增,已选列表最大高度,默认500
|
||||||
showInput?: boolean; // 新增
|
showInput?: boolean; // 新增
|
||||||
showSelectedList?: boolean; // 新增
|
showSelectedList?: boolean; // 新增
|
||||||
readonly?: boolean; // 新增
|
readonly?: boolean; // 新增
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,183 +1,182 @@
|
|||||||
.inputWrapper {
|
.inputWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.inputIcon {
|
.inputIcon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
color: #bdbdbd;
|
color: #bdbdbd;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
.input {
|
.input {
|
||||||
padding-left: 38px !important;
|
padding-left: 38px !important;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
border-radius: 16px !important;
|
border-radius: 16px !important;
|
||||||
border: 1px solid #e5e6eb !important;
|
border: 1px solid #e5e6eb !important;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popupHeader {
|
||||||
.popupHeader {
|
padding: 16px;
|
||||||
padding: 16px;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
}
|
||||||
}
|
.popupTitle {
|
||||||
.popupTitle {
|
font-size: 20px;
|
||||||
font-size: 20px;
|
font-weight: 600;
|
||||||
font-weight: 600;
|
text-align: center;
|
||||||
text-align: center;
|
}
|
||||||
}
|
.popupSearchRow {
|
||||||
.popupSearchRow {
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
gap: 16px;
|
||||||
gap: 16px;
|
padding: 16px;
|
||||||
padding: 16px;
|
}
|
||||||
}
|
.popupSearchInputWrap {
|
||||||
.popupSearchInputWrap {
|
position: relative;
|
||||||
position: relative;
|
flex: 1;
|
||||||
flex: 1;
|
}
|
||||||
}
|
.popupSearchInput {
|
||||||
.popupSearchInput {
|
padding-left: 36px !important;
|
||||||
padding-left: 36px !important;
|
border-radius: 12px !important;
|
||||||
border-radius: 12px !important;
|
height: 44px;
|
||||||
height: 44px;
|
font-size: 15px;
|
||||||
font-size: 15px;
|
border: 1px solid #e5e6eb !important;
|
||||||
border: 1px solid #e5e6eb !important;
|
background: #f8f9fa;
|
||||||
background: #f8f9fa;
|
}
|
||||||
}
|
.statusSelect {
|
||||||
.statusSelect {
|
width: 120px;
|
||||||
width: 120px;
|
height: 40px;
|
||||||
height: 40px;
|
border-radius: 8px;
|
||||||
border-radius: 8px;
|
border: 1px solid #e5e6eb;
|
||||||
border: 1px solid #e5e6eb;
|
font-size: 15px;
|
||||||
font-size: 15px;
|
padding: 0 10px;
|
||||||
padding: 0 10px;
|
background: #fff;
|
||||||
background: #fff;
|
}
|
||||||
}
|
.deviceList {
|
||||||
.deviceList {
|
flex: 1;
|
||||||
flex: 1;
|
overflow-y: auto;
|
||||||
overflow-y: auto;
|
}
|
||||||
}
|
.deviceListInner {
|
||||||
.deviceListInner {
|
display: flex;
|
||||||
display: flex;
|
flex-direction: column;
|
||||||
flex-direction: column;
|
gap: 12px;
|
||||||
gap: 12px;
|
padding: 16px;
|
||||||
padding: 16px;
|
}
|
||||||
}
|
.deviceItem {
|
||||||
.deviceItem {
|
display: flex;
|
||||||
display: flex;
|
align-items: flex-start;
|
||||||
align-items: flex-start;
|
gap: 12px;
|
||||||
gap: 12px;
|
padding: 16px;
|
||||||
padding: 16px;
|
border-radius: 12px;
|
||||||
border-radius: 12px;
|
border: 1px solid #f0f0f0;
|
||||||
border: 1px solid #f0f0f0;
|
background: #fff;
|
||||||
background: #fff;
|
cursor: pointer;
|
||||||
cursor: pointer;
|
transition: background 0.2s;
|
||||||
transition: background 0.2s;
|
&:hover {
|
||||||
&:hover {
|
background: #f5f6fa;
|
||||||
background: #f5f6fa;
|
}
|
||||||
}
|
}
|
||||||
}
|
.deviceCheckbox {
|
||||||
.deviceCheckbox {
|
margin-top: 4px;
|
||||||
margin-top: 4px;
|
}
|
||||||
}
|
.deviceInfo {
|
||||||
.deviceInfo {
|
flex: 1;
|
||||||
flex: 1;
|
}
|
||||||
}
|
.deviceInfoRow {
|
||||||
.deviceInfoRow {
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
justify-content: space-between;
|
||||||
justify-content: space-between;
|
}
|
||||||
}
|
.deviceName {
|
||||||
.deviceName {
|
font-weight: 500;
|
||||||
font-weight: 500;
|
font-size: 16px;
|
||||||
font-size: 16px;
|
color: #222;
|
||||||
color: #222;
|
}
|
||||||
}
|
.statusOnline {
|
||||||
.statusOnline {
|
width: 56px;
|
||||||
width: 56px;
|
height: 24px;
|
||||||
height: 24px;
|
border-radius: 12px;
|
||||||
border-radius: 12px;
|
background: #52c41a;
|
||||||
background: #52c41a;
|
color: #fff;
|
||||||
color: #fff;
|
font-size: 13px;
|
||||||
font-size: 13px;
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
justify-content: center;
|
}
|
||||||
}
|
.statusOffline {
|
||||||
.statusOffline {
|
width: 56px;
|
||||||
width: 56px;
|
height: 24px;
|
||||||
height: 24px;
|
border-radius: 12px;
|
||||||
border-radius: 12px;
|
background: #e5e6eb;
|
||||||
background: #e5e6eb;
|
color: #888;
|
||||||
color: #888;
|
font-size: 13px;
|
||||||
font-size: 13px;
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
justify-content: center;
|
}
|
||||||
}
|
.deviceInfoDetail {
|
||||||
.deviceInfoDetail {
|
font-size: 13px;
|
||||||
font-size: 13px;
|
color: #888;
|
||||||
color: #888;
|
margin-top: 4px;
|
||||||
margin-top: 4px;
|
}
|
||||||
}
|
.loadingBox {
|
||||||
.loadingBox {
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
justify-content: center;
|
height: 100%;
|
||||||
height: 100%;
|
}
|
||||||
}
|
.loadingText {
|
||||||
.loadingText {
|
color: #888;
|
||||||
color: #888;
|
font-size: 15px;
|
||||||
font-size: 15px;
|
}
|
||||||
}
|
.popupFooter {
|
||||||
.popupFooter {
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
justify-content: space-between;
|
||||||
justify-content: space-between;
|
padding: 16px;
|
||||||
padding: 16px;
|
border-top: 1px solid #f0f0f0;
|
||||||
border-top: 1px solid #f0f0f0;
|
background: #fff;
|
||||||
background: #fff;
|
}
|
||||||
}
|
.selectedCount {
|
||||||
.selectedCount {
|
font-size: 14px;
|
||||||
font-size: 14px;
|
color: #888;
|
||||||
color: #888;
|
}
|
||||||
}
|
.footerBtnGroup {
|
||||||
.footerBtnGroup {
|
display: flex;
|
||||||
display: flex;
|
gap: 12px;
|
||||||
gap: 12px;
|
}
|
||||||
}
|
.refreshBtn {
|
||||||
.refreshBtn {
|
width: 36px;
|
||||||
width: 36px;
|
height: 36px;
|
||||||
height: 36px;
|
}
|
||||||
}
|
.paginationRow {
|
||||||
.paginationRow {
|
border-top: 1px solid #f0f0f0;
|
||||||
border-top: 1px solid #f0f0f0;
|
padding: 16px;
|
||||||
padding: 16px;
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
justify-content: space-between;
|
||||||
justify-content: space-between;
|
background: #fff;
|
||||||
background: #fff;
|
}
|
||||||
}
|
.totalCount {
|
||||||
.totalCount {
|
font-size: 14px;
|
||||||
font-size: 14px;
|
color: #888;
|
||||||
color: #888;
|
}
|
||||||
}
|
.paginationControls {
|
||||||
.paginationControls {
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
gap: 8px;
|
||||||
gap: 8px;
|
}
|
||||||
}
|
.pageBtn {
|
||||||
.pageBtn {
|
padding: 0 8px;
|
||||||
padding: 0 8px;
|
height: 32px;
|
||||||
height: 32px;
|
min-width: 32px;
|
||||||
min-width: 32px;
|
border-radius: 16px;
|
||||||
border-radius: 16px;
|
}
|
||||||
}
|
.pageInfo {
|
||||||
.pageInfo {
|
font-size: 14px;
|
||||||
font-size: 14px;
|
color: #222;
|
||||||
color: #222;
|
margin: 0 8px;
|
||||||
margin: 0 8px;
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -1,207 +1,207 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { Checkbox, Popup } from "antd-mobile";
|
import { Checkbox, Popup } from "antd-mobile";
|
||||||
import { getDeviceList } from "./api";
|
import { getDeviceList } from "./api";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
import PopupHeader from "@/components/PopuLayout/header";
|
import PopupHeader from "@/components/PopuLayout/header";
|
||||||
import PopupFooter from "@/components/PopuLayout/footer";
|
import PopupFooter from "@/components/PopuLayout/footer";
|
||||||
|
|
||||||
interface DeviceSelectionItem {
|
interface DeviceSelectionItem {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
imei: string;
|
imei: string;
|
||||||
wechatId: string;
|
wechatId: string;
|
||||||
status: "online" | "offline";
|
status: "online" | "offline";
|
||||||
wxid?: string;
|
wxid?: string;
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
usedInPlans?: number;
|
usedInPlans?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectionPopupProps {
|
interface SelectionPopupProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
selectedDevices: string[];
|
selectedDevices: string[];
|
||||||
onSelect: (devices: string[]) => void;
|
onSelect: (devices: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE = 20;
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
const SelectionPopup: React.FC<SelectionPopupProps> = ({
|
const SelectionPopup: React.FC<SelectionPopupProps> = ({
|
||||||
visible,
|
visible,
|
||||||
onClose,
|
onClose,
|
||||||
selectedDevices,
|
selectedDevices,
|
||||||
onSelect,
|
onSelect,
|
||||||
}) => {
|
}) => {
|
||||||
// 设备数据
|
// 设备数据
|
||||||
const [devices, setDevices] = useState<DeviceSelectionItem[]>([]);
|
const [devices, setDevices] = useState<DeviceSelectionItem[]>([]);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [statusFilter, setStatusFilter] = useState("all");
|
const [statusFilter, setStatusFilter] = useState("all");
|
||||||
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);
|
||||||
|
|
||||||
// 获取设备列表,支持keyword和分页
|
// 获取设备列表,支持keyword和分页
|
||||||
const fetchDevices = useCallback(
|
const fetchDevices = useCallback(
|
||||||
async (keyword: string = "", page: number = 1) => {
|
async (keyword: string = "", page: number = 1) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await getDeviceList({
|
const res = await getDeviceList({
|
||||||
page,
|
page,
|
||||||
limit: PAGE_SIZE,
|
limit: PAGE_SIZE,
|
||||||
keyword: keyword.trim() || undefined,
|
keyword: keyword.trim() || undefined,
|
||||||
});
|
});
|
||||||
if (res && Array.isArray(res.list)) {
|
if (res && Array.isArray(res.list)) {
|
||||||
setDevices(
|
setDevices(
|
||||||
res.list.map((d: any) => ({
|
res.list.map((d: any) => ({
|
||||||
id: d.id?.toString() || "",
|
id: d.id?.toString() || "",
|
||||||
name: d.memo || d.imei || "",
|
name: d.memo || d.imei || "",
|
||||||
imei: d.imei || "",
|
imei: d.imei || "",
|
||||||
wechatId: d.wechatId || "",
|
wechatId: d.wechatId || "",
|
||||||
status: d.alive === 1 ? "online" : "offline",
|
status: d.alive === 1 ? "online" : "offline",
|
||||||
wxid: d.wechatId || "",
|
wxid: d.wechatId || "",
|
||||||
nickname: d.nickname || "",
|
nickname: d.nickname || "",
|
||||||
usedInPlans: d.usedInPlans || 0,
|
usedInPlans: d.usedInPlans || 0,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
setTotal(res.total || 0);
|
setTotal(res.total || 0);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取设备列表失败:", error);
|
console.error("获取设备列表失败:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 打开弹窗时获取第一页
|
// 打开弹窗时获取第一页
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
fetchDevices("", 1);
|
fetchDevices("", 1);
|
||||||
}
|
}
|
||||||
}, [visible, fetchDevices]);
|
}, [visible, fetchDevices]);
|
||||||
|
|
||||||
// 搜索防抖
|
// 搜索防抖
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visible) return;
|
if (!visible) return;
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
fetchDevices(searchQuery, 1);
|
fetchDevices(searchQuery, 1);
|
||||||
}, 500);
|
}, 500);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [searchQuery, visible, fetchDevices]);
|
}, [searchQuery, visible, fetchDevices]);
|
||||||
|
|
||||||
// 翻页时重新请求
|
// 翻页时重新请求
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visible) return;
|
if (!visible) return;
|
||||||
fetchDevices(searchQuery, currentPage);
|
fetchDevices(searchQuery, currentPage);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [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") ||
|
||||||
(statusFilter === "offline" && device.status === "offline");
|
(statusFilter === "offline" && device.status === "offline");
|
||||||
return matchesStatus;
|
return matchesStatus;
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
||||||
|
|
||||||
// 处理设备选择
|
// 处理设备选择
|
||||||
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]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup
|
<Popup
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onMaskClick={onClose}
|
onMaskClick={onClose}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
bodyStyle={{ height: "100vh" }}
|
bodyStyle={{ height: "100vh" }}
|
||||||
closeOnMaskClick={false}
|
closeOnMaskClick={false}
|
||||||
>
|
>
|
||||||
<Layout
|
<Layout
|
||||||
header={
|
header={
|
||||||
<PopupHeader
|
<PopupHeader
|
||||||
title="选择设备"
|
title="选择设备"
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
setSearchQuery={setSearchQuery}
|
setSearchQuery={setSearchQuery}
|
||||||
searchPlaceholder="搜索设备IMEI/备注/微信号"
|
searchPlaceholder="搜索设备IMEI/备注/微信号"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onRefresh={() => fetchDevices(searchQuery, currentPage)}
|
onRefresh={() => fetchDevices(searchQuery, currentPage)}
|
||||||
showTabs={true}
|
showTabs={true}
|
||||||
tabsConfig={{
|
tabsConfig={{
|
||||||
activeKey: statusFilter,
|
activeKey: statusFilter,
|
||||||
onChange: setStatusFilter,
|
onChange: setStatusFilter,
|
||||||
tabs: [
|
tabs: [
|
||||||
{ title: "全部", key: "all" },
|
{ title: "全部", key: "all" },
|
||||||
{ title: "在线", key: "online" },
|
{ title: "在线", key: "online" },
|
||||||
{ title: "离线", key: "offline" },
|
{ title: "离线", key: "offline" },
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
<PopupFooter
|
<PopupFooter
|
||||||
total={total}
|
total={total}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
selectedCount={selectedDevices.length}
|
selectedCount={selectedDevices.length}
|
||||||
onPageChange={setCurrentPage}
|
onPageChange={setCurrentPage}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
onConfirm={onClose}
|
onConfirm={onClose}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={style.deviceList}>
|
<div className={style.deviceList}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className={style.loadingBox}>
|
<div className={style.loadingBox}>
|
||||||
<div className={style.loadingText}>加载中...</div>
|
<div className={style.loadingText}>加载中...</div>
|
||||||
</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)}
|
||||||
onChange={() => handleDeviceToggle(device.id)}
|
onChange={() => handleDeviceToggle(device.id)}
|
||||||
className={style.deviceCheckbox}
|
className={style.deviceCheckbox}
|
||||||
/>
|
/>
|
||||||
<div className={style.deviceInfo}>
|
<div className={style.deviceInfo}>
|
||||||
<div className={style.deviceInfoRow}>
|
<div className={style.deviceInfoRow}>
|
||||||
<span className={style.deviceName}>{device.name}</span>
|
<span className={style.deviceName}>{device.name}</span>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
device.status === "online"
|
device.status === "online"
|
||||||
? style.statusOnline
|
? style.statusOnline
|
||||||
: style.statusOffline
|
: style.statusOffline
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{device.status === "online" ? "在线" : "离线"}
|
{device.status === "online" ? "在线" : "离线"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.deviceInfoDetail}>
|
<div className={style.deviceInfoDetail}>
|
||||||
<div>IMEI: {device.imei}</div>
|
<div>IMEI: {device.imei}</div>
|
||||||
<div>微信号: {device.wechatId}</div>
|
<div>微信号: {device.wechatId}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Popup>
|
</Popup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SelectionPopup;
|
export default SelectionPopup;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import request from "@/api/request";
|
import request from "@/api/request";
|
||||||
|
|
||||||
// 获取好友列表
|
// 获取好友列表
|
||||||
export function getFriendList(params: {
|
export function getFriendList(params: {
|
||||||
page: number;
|
page: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
deviceIds?: string; // 逗号分隔
|
deviceIds?: string; // 逗号分隔
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
}) {
|
}) {
|
||||||
return request("/v1/friend", params, "GET");
|
return request("/v1/friend", params, "GET");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,231 +1,231 @@
|
|||||||
.inputWrapper {
|
.inputWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.inputIcon {
|
.inputIcon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
color: #bdbdbd;
|
color: #bdbdbd;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
.input {
|
.input {
|
||||||
padding-left: 38px !important;
|
padding-left: 38px !important;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
border-radius: 16px !important;
|
border-radius: 16px !important;
|
||||||
border: 1px solid #e5e6eb !important;
|
border: 1px solid #e5e6eb !important;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupContainer {
|
.popupContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.popupHeader {
|
.popupHeader {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
.popupTitle {
|
.popupTitle {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
.searchWrapper {
|
.searchWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
.searchInput {
|
.searchInput {
|
||||||
padding-left: 40px !important;
|
padding-left: 40px !important;
|
||||||
padding-top: 8px !important;
|
padding-top: 8px !important;
|
||||||
padding-bottom: 8px !important;
|
padding-bottom: 8px !important;
|
||||||
border-radius: 24px !important;
|
border-radius: 24px !important;
|
||||||
border: 1px solid #e5e6eb !important;
|
border: 1px solid #e5e6eb !important;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
.searchIcon {
|
.searchIcon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
color: #bdbdbd;
|
color: #bdbdbd;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.clearBtn {
|
.clearBtn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
min-width: 24px;
|
min-width: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.friendList {
|
.friendList {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.friendListInner {
|
.friendListInner {
|
||||||
border-top: 1px solid #f0f0f0;
|
border-top: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
.friendItem {
|
.friendItem {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 24px;
|
padding: 16px 24px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #f5f6fa;
|
background: #f5f6fa;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.radioWrapper {
|
.radioWrapper {
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.radioSelected {
|
.radioSelected {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 2px solid #1890ff;
|
border: 2px solid #1890ff;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.radioUnselected {
|
.radioUnselected {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 2px solid #e5e6eb;
|
border: 2px solid #e5e6eb;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.radioDot {
|
.radioDot {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #1890ff;
|
background: #1890ff;
|
||||||
}
|
}
|
||||||
.friendInfo {
|
.friendInfo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.friendAvatar {
|
.friendAvatar {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.avatarImg {
|
.avatarImg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
.friendDetail {
|
.friendDetail {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.friendName {
|
.friendName {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #222;
|
color: #222;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
.friendId {
|
.friendId {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #888;
|
color: #888;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
.friendCustomer {
|
.friendCustomer {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #bdbdbd;
|
color: #bdbdbd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loadingBox {
|
.loadingBox {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.loadingText {
|
.loadingText {
|
||||||
color: #888;
|
color: #888;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
.emptyBox {
|
.emptyBox {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.emptyText {
|
.emptyText {
|
||||||
color: #888;
|
color: #888;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paginationRow {
|
.paginationRow {
|
||||||
border-top: 1px solid #f0f0f0;
|
border-top: 1px solid #f0f0f0;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.totalCount {
|
.totalCount {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
.paginationControls {
|
.paginationControls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.pageBtn {
|
.pageBtn {
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
min-width: 32px;
|
min-width: 32px;
|
||||||
}
|
}
|
||||||
.pageInfo {
|
.pageInfo {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #222;
|
color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupFooter {
|
.popupFooter {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-top: 1px solid #f0f0f0;
|
border-top: 1px solid #f0f0f0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.selectedCount {
|
.selectedCount {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
.footerBtnGroup {
|
.footerBtnGroup {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
.cancelBtn {
|
.cancelBtn {
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
border: 1px solid #e5e6eb;
|
border: 1px solid #e5e6eb;
|
||||||
}
|
}
|
||||||
.confirmBtn {
|
.confirmBtn {
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import request from "@/api/request";
|
import request from "@/api/request";
|
||||||
|
|
||||||
// 获取群组列表
|
// 获取群组列表
|
||||||
export function getGroupList(params: {
|
export function getGroupList(params: {
|
||||||
page: number;
|
page: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
}) {
|
}) {
|
||||||
return request("/v1/chatroom", params, "GET");
|
return request("/v1/chatroom", params, "GET");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,222 +1,222 @@
|
|||||||
.inputWrapper {
|
.inputWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.inputIcon {
|
.inputIcon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
color: #bdbdbd;
|
color: #bdbdbd;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
.input {
|
.input {
|
||||||
padding-left: 38px !important;
|
padding-left: 38px !important;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
border-radius: 16px !important;
|
border-radius: 16px !important;
|
||||||
border: 1px solid #e5e6eb !important;
|
border: 1px solid #e5e6eb !important;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupContainer {
|
.popupContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.popupHeader {
|
.popupHeader {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
.popupTitle {
|
.popupTitle {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
.searchWrapper {
|
.searchWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
.searchInput {
|
.searchInput {
|
||||||
padding-left: 40px !important;
|
padding-left: 40px !important;
|
||||||
padding-top: 8px !important;
|
padding-top: 8px !important;
|
||||||
padding-bottom: 8px !important;
|
padding-bottom: 8px !important;
|
||||||
border-radius: 24px !important;
|
border-radius: 24px !important;
|
||||||
border: 1px solid #e5e6eb !important;
|
border: 1px solid #e5e6eb !important;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
.searchIcon {
|
.searchIcon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
color: #bdbdbd;
|
color: #bdbdbd;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.clearBtn {
|
.clearBtn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
min-width: 24px;
|
min-width: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.groupList {
|
.groupList {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.groupListInner {
|
.groupListInner {
|
||||||
border-top: 1px solid #f0f0f0;
|
border-top: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
.groupItem {
|
.groupItem {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 24px;
|
padding: 16px 24px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #f5f6fa;
|
background: #f5f6fa;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.radioWrapper {
|
.radioWrapper {
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.radioSelected {
|
.radioSelected {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 2px solid #1890ff;
|
border: 2px solid #1890ff;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.radioUnselected {
|
.radioUnselected {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 2px solid #e5e6eb;
|
border: 2px solid #e5e6eb;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.radioDot {
|
.radioDot {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #1890ff;
|
background: #1890ff;
|
||||||
}
|
}
|
||||||
.groupInfo {
|
.groupInfo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.groupAvatar {
|
.groupAvatar {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.avatarImg {
|
.avatarImg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
.groupDetail {
|
.groupDetail {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.groupName {
|
.groupName {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #222;
|
color: #222;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
.groupId {
|
.groupId {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #888;
|
color: #888;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
.groupOwner {
|
.groupOwner {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #bdbdbd;
|
color: #bdbdbd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loadingBox {
|
.loadingBox {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.loadingText {
|
.loadingText {
|
||||||
color: #888;
|
color: #888;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
.emptyBox {
|
.emptyBox {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.emptyText {
|
.emptyText {
|
||||||
color: #888;
|
color: #888;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paginationRow {
|
.paginationRow {
|
||||||
border-top: 1px solid #f0f0f0;
|
border-top: 1px solid #f0f0f0;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.totalCount {
|
.totalCount {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
.paginationControls {
|
.paginationControls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.pageBtn {
|
.pageBtn {
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
min-width: 32px;
|
min-width: 32px;
|
||||||
}
|
}
|
||||||
.pageInfo {
|
.pageInfo {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #222;
|
color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupFooter {
|
.popupFooter {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-top: 1px solid #f0f0f0;
|
border-top: 1px solid #f0f0f0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.selectedCount {
|
.selectedCount {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
.footerBtnGroup {
|
.footerBtnGroup {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,341 +1,341 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { SearchOutlined, DeleteOutlined } from "@ant-design/icons";
|
import { SearchOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||||
import { Button, Input } from "antd";
|
import { Button, Input } from "antd";
|
||||||
import { Popup } from "antd-mobile";
|
import { Popup } from "antd-mobile";
|
||||||
import { getGroupList } from "./api";
|
import { getGroupList } from "./api";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
import PopupHeader from "@/components/PopuLayout/header";
|
import PopupHeader from "@/components/PopuLayout/header";
|
||||||
import PopupFooter from "@/components/PopuLayout/footer";
|
import PopupFooter from "@/components/PopuLayout/footer";
|
||||||
|
|
||||||
// 群组接口类型
|
// 群组接口类型
|
||||||
interface WechatGroup {
|
interface WechatGroup {
|
||||||
id: string;
|
id: string;
|
||||||
chatroomId: string;
|
chatroomId: string;
|
||||||
name: string;
|
name: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
ownerWechatId: string;
|
ownerWechatId: string;
|
||||||
ownerNickname: string;
|
ownerNickname: string;
|
||||||
ownerAvatar: string;
|
ownerAvatar: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件属性接口
|
// 组件属性接口
|
||||||
interface GroupSelectionProps {
|
interface GroupSelectionProps {
|
||||||
selectedGroups: string[];
|
selectedGroups: string[];
|
||||||
onSelect: (groups: string[]) => void;
|
onSelect: (groups: string[]) => void;
|
||||||
onSelectDetail?: (groups: WechatGroup[]) => void;
|
onSelectDetail?: (groups: WechatGroup[]) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
onVisibleChange?: (visible: boolean) => void;
|
onVisibleChange?: (visible: boolean) => void;
|
||||||
selectedListMaxHeight?: number;
|
selectedListMaxHeight?: number;
|
||||||
showInput?: boolean;
|
showInput?: boolean;
|
||||||
showSelectedList?: boolean;
|
showSelectedList?: boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
onConfirm?: (selectedIds: string[], selectedItems: WechatGroup[]) => void; // 新增
|
onConfirm?: (selectedIds: string[], selectedItems: WechatGroup[]) => void; // 新增
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GroupSelection({
|
export default function GroupSelection({
|
||||||
selectedGroups,
|
selectedGroups,
|
||||||
onSelect,
|
onSelect,
|
||||||
onSelectDetail,
|
onSelectDetail,
|
||||||
placeholder = "选择群聊",
|
placeholder = "选择群聊",
|
||||||
className = "",
|
className = "",
|
||||||
visible,
|
visible,
|
||||||
onVisibleChange,
|
onVisibleChange,
|
||||||
selectedListMaxHeight = 300,
|
selectedListMaxHeight = 300,
|
||||||
showInput = true,
|
showInput = true,
|
||||||
showSelectedList = true,
|
showSelectedList = true,
|
||||||
readonly = false,
|
readonly = false,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}: GroupSelectionProps) {
|
}: GroupSelectionProps) {
|
||||||
const [popupVisible, setPopupVisible] = useState(false);
|
const [popupVisible, setPopupVisible] = useState(false);
|
||||||
const [groups, setGroups] = useState<WechatGroup[]>([]);
|
const [groups, setGroups] = useState<WechatGroup[]>([]);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const [totalGroups, setTotalGroups] = useState(0);
|
const [totalGroups, setTotalGroups] = useState(0);
|
||||||
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));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 受控弹窗逻辑
|
// 受控弹窗逻辑
|
||||||
const realVisible = visible !== undefined ? visible : popupVisible;
|
const realVisible = visible !== undefined ? visible : popupVisible;
|
||||||
const setRealVisible = (v: boolean) => {
|
const setRealVisible = (v: boolean) => {
|
||||||
if (onVisibleChange) onVisibleChange(v);
|
if (onVisibleChange) onVisibleChange(v);
|
||||||
if (visible === undefined) setPopupVisible(v);
|
if (visible === undefined) setPopupVisible(v);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开弹窗
|
// 打开弹窗
|
||||||
const openPopup = () => {
|
const openPopup = () => {
|
||||||
if (readonly) return;
|
if (readonly) return;
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
setRealVisible(true);
|
setRealVisible(true);
|
||||||
fetchGroups(1, "");
|
fetchGroups(1, "");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 当页码变化时,拉取对应页数据(弹窗已打开时)
|
// 当页码变化时,拉取对应页数据(弹窗已打开时)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (realVisible && currentPage !== 1) {
|
if (realVisible && currentPage !== 1) {
|
||||||
fetchGroups(currentPage, searchQuery);
|
fetchGroups(currentPage, searchQuery);
|
||||||
}
|
}
|
||||||
}, [currentPage, realVisible, searchQuery]);
|
}, [currentPage, realVisible, searchQuery]);
|
||||||
|
|
||||||
// 搜索防抖
|
// 搜索防抖
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!realVisible) return;
|
if (!realVisible) return;
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
fetchGroups(1, searchQuery);
|
fetchGroups(1, searchQuery);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [searchQuery, realVisible]);
|
}, [searchQuery, realVisible]);
|
||||||
|
|
||||||
// 获取群聊列表API
|
// 获取群聊列表API
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (keyword.trim()) {
|
if (keyword.trim()) {
|
||||||
params.keyword = keyword.trim();
|
params.keyword = keyword.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await getGroupList(params);
|
const response = await getGroupList(params);
|
||||||
if (response && response.list) {
|
if (response && response.list) {
|
||||||
setGroups(response.list);
|
setGroups(response.list);
|
||||||
setTotalGroups(response.total || 0);
|
setTotalGroups(response.total || 0);
|
||||||
setTotalPages(Math.ceil((response.total || 0) / 20));
|
setTotalPages(Math.ceil((response.total || 0) / 20));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取群聊列表失败:", error);
|
console.error("获取群聊列表失败:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理群聊选择
|
// 处理群聊选择
|
||||||
const handleGroupToggle = (groupId: string) => {
|
const handleGroupToggle = (groupId: string) => {
|
||||||
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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取显示文本
|
// 获取显示文本
|
||||||
const getDisplayText = () => {
|
const getDisplayText = () => {
|
||||||
if (selectedGroups.length === 0) return "";
|
if (selectedGroups.length === 0) return "";
|
||||||
return `已选择 ${selectedGroups.length} 个群聊`;
|
return `已选择 ${selectedGroups.length} 个群聊`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 确认选择
|
// 确认选择
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
if (onConfirm) {
|
if (onConfirm) {
|
||||||
onConfirm(selectedGroups, selectedGroupObjs);
|
onConfirm(selectedGroups, selectedGroupObjs);
|
||||||
}
|
}
|
||||||
setRealVisible(false);
|
setRealVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 输入框 */}
|
{/* 输入框 */}
|
||||||
{showInput && (
|
{showInput && (
|
||||||
<div className={`${style.inputWrapper} ${className}`}>
|
<div className={`${style.inputWrapper} ${className}`}>
|
||||||
<Input
|
<Input
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={getDisplayText()}
|
value={getDisplayText()}
|
||||||
onClick={openPopup}
|
onClick={openPopup}
|
||||||
prefix={<SearchOutlined />}
|
prefix={<SearchOutlined />}
|
||||||
allowClear={!readonly}
|
allowClear={!readonly}
|
||||||
size="large"
|
size="large"
|
||||||
readOnly={readonly}
|
readOnly={readonly}
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
style={
|
style={
|
||||||
readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {}
|
readonly ? { background: "#f5f5f5", cursor: "not-allowed" } : {}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 已选群聊列表窗口 */}
|
{/* 已选群聊列表窗口 */}
|
||||||
{showSelectedList && selectedGroupObjs.length > 0 && (
|
{showSelectedList && selectedGroupObjs.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className={style.selectedListWindow}
|
className={style.selectedListWindow}
|
||||||
style={{
|
style={{
|
||||||
maxHeight: selectedListMaxHeight,
|
maxHeight: selectedListMaxHeight,
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
border: "1px solid #e5e6eb",
|
border: "1px solid #e5e6eb",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
background: "#fff",
|
background: "#fff",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedGroupObjs.map((group) => (
|
{selectedGroupObjs.map(group => (
|
||||||
<div
|
<div
|
||||||
key={group.id}
|
key={group.id}
|
||||||
className={style.selectedListRow}
|
className={style.selectedListRow}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: "4px 8px",
|
padding: "4px 8px",
|
||||||
borderBottom: "1px solid #f0f0f0",
|
borderBottom: "1px solid #f0f0f0",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{group.name || group.chatroomId || group.id}
|
{group.name || group.chatroomId || group.id}
|
||||||
</div>
|
</div>
|
||||||
{!readonly && (
|
{!readonly && (
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
color: "#ff4d4f",
|
color: "#ff4d4f",
|
||||||
border: "none",
|
border: "none",
|
||||||
background: "none",
|
background: "none",
|
||||||
minWidth: 24,
|
minWidth: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
onClick={() => handleRemoveGroup(group.id)}
|
onClick={() => handleRemoveGroup(group.id)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 弹窗 */}
|
{/* 弹窗 */}
|
||||||
<Popup
|
<Popup
|
||||||
visible={realVisible && !readonly}
|
visible={realVisible && !readonly}
|
||||||
onMaskClick={() => setRealVisible(false)}
|
onMaskClick={() => setRealVisible(false)}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
bodyStyle={{ height: "100vh" }}
|
bodyStyle={{ height: "100vh" }}
|
||||||
>
|
>
|
||||||
<Layout
|
<Layout
|
||||||
header={
|
header={
|
||||||
<PopupHeader
|
<PopupHeader
|
||||||
title="选择群聊"
|
title="选择群聊"
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
setSearchQuery={setSearchQuery}
|
setSearchQuery={setSearchQuery}
|
||||||
searchPlaceholder="搜索群聊"
|
searchPlaceholder="搜索群聊"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onRefresh={() => fetchGroups(currentPage, searchQuery)}
|
onRefresh={() => fetchGroups(currentPage, searchQuery)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
<PopupFooter
|
<PopupFooter
|
||||||
total={totalGroups}
|
total={totalGroups}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
selectedCount={selectedGroups.length}
|
selectedCount={selectedGroups.length}
|
||||||
onPageChange={setCurrentPage}
|
onPageChange={setCurrentPage}
|
||||||
onCancel={() => setRealVisible(false)}
|
onCancel={() => setRealVisible(false)}
|
||||||
onConfirm={handleConfirm}
|
onConfirm={handleConfirm}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={style.groupList}>
|
<div className={style.groupList}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className={style.loadingBox}>
|
<div className={style.loadingBox}>
|
||||||
<div className={style.loadingText}>加载中...</div>
|
<div className={style.loadingText}>加载中...</div>
|
||||||
</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}
|
||||||
onClick={() => !readonly && handleGroupToggle(group.id)}
|
onClick={() => !readonly && handleGroupToggle(group.id)}
|
||||||
>
|
>
|
||||||
<div className={style.radioWrapper}>
|
<div className={style.radioWrapper}>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
selectedGroups.includes(group.id)
|
selectedGroups.includes(group.id)
|
||||||
? style.radioSelected
|
? style.radioSelected
|
||||||
: style.radioUnselected
|
: style.radioUnselected
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{selectedGroups.includes(group.id) && (
|
{selectedGroups.includes(group.id) && (
|
||||||
<div className={style.radioDot}></div>
|
<div className={style.radioDot}></div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.groupInfo}>
|
<div className={style.groupInfo}>
|
||||||
<div className={style.groupAvatar}>
|
<div className={style.groupAvatar}>
|
||||||
{group.avatar ? (
|
{group.avatar ? (
|
||||||
<img
|
<img
|
||||||
src={group.avatar}
|
src={group.avatar}
|
||||||
alt={group.name}
|
alt={group.name}
|
||||||
className={style.avatarImg}
|
className={style.avatarImg}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
group.name.charAt(0)
|
group.name.charAt(0)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={style.groupDetail}>
|
<div className={style.groupDetail}>
|
||||||
<div className={style.groupName}>{group.name}</div>
|
<div className={style.groupName}>{group.name}</div>
|
||||||
<div className={style.groupId}>
|
<div className={style.groupId}>
|
||||||
群ID: {group.chatroomId}
|
群ID: {group.chatroomId}
|
||||||
</div>
|
</div>
|
||||||
{group.ownerNickname && (
|
{group.ownerNickname && (
|
||||||
<div className={style.groupOwner}>
|
<div className={style.groupOwner}>
|
||||||
群主: {group.ownerNickname}
|
群主: {group.ownerNickname}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={style.emptyBox}>
|
<div className={style.emptyBox}>
|
||||||
<div className={style.emptyText}>
|
<div className={style.emptyText}>
|
||||||
{searchQuery
|
{searchQuery
|
||||||
? `没有找到包含"${searchQuery}"的群聊`
|
? `没有找到包含"${searchQuery}"的群聊`
|
||||||
: "没有找到群聊"}
|
: "没有找到群聊"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Popup>
|
</Popup>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +1,87 @@
|
|||||||
.listContainer {
|
.listContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.listItem {
|
.listItem {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loadMoreButtonContainer {
|
.loadMoreButtonContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.noMoreText {
|
.noMoreText {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #999;
|
color: #999;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emptyState {
|
.emptyState {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 60px 20px;
|
padding: 60px 20px;
|
||||||
color: #999;
|
color: #999;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emptyIcon {
|
.emptyIcon {
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emptyText {
|
.emptyText {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pullToRefresh {
|
.pullToRefresh {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自定义滚动条样式
|
// 自定义滚动条样式
|
||||||
.listContainer::-webkit-scrollbar {
|
.listContainer::-webkit-scrollbar {
|
||||||
width: 4px;
|
width: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.listContainer::-webkit-scrollbar-track {
|
.listContainer::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.listContainer::-webkit-scrollbar-thumb {
|
.listContainer::-webkit-scrollbar-thumb {
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.listContainer::-webkit-scrollbar-thumb:hover {
|
.listContainer::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式设计
|
// 响应式设计
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.listContainer {
|
.listContainer {
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loadMoreButtonContainer {
|
.loadMoreButtonContainer {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.noMoreText {
|
.noMoreText {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,195 +1,195 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import {
|
import {
|
||||||
PullToRefresh,
|
PullToRefresh,
|
||||||
InfiniteScroll,
|
InfiniteScroll,
|
||||||
Button,
|
Button,
|
||||||
SpinLoading,
|
SpinLoading,
|
||||||
} from "antd-mobile";
|
} from "antd-mobile";
|
||||||
import styles from "./InfiniteList.module.scss";
|
import styles from "./InfiniteList.module.scss";
|
||||||
|
|
||||||
interface InfiniteListProps<T> {
|
interface InfiniteListProps<T> {
|
||||||
// 数据相关
|
// 数据相关
|
||||||
data: T[];
|
data: T[];
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
hasMore?: boolean;
|
hasMore?: boolean;
|
||||||
loadingText?: string;
|
loadingText?: string;
|
||||||
noMoreText?: string;
|
noMoreText?: string;
|
||||||
|
|
||||||
// 渲染相关
|
// 渲染相关
|
||||||
renderItem: (item: T, index: number) => React.ReactNode;
|
renderItem: (item: T, index: number) => React.ReactNode;
|
||||||
keyExtractor?: (item: T, index: number) => string | number;
|
keyExtractor?: (item: T, index: number) => string | number;
|
||||||
|
|
||||||
// 事件回调
|
// 事件回调
|
||||||
onLoadMore?: () => Promise<void> | void;
|
onLoadMore?: () => Promise<void> | void;
|
||||||
onRefresh?: () => Promise<void> | void;
|
onRefresh?: () => Promise<void> | void;
|
||||||
|
|
||||||
// 样式相关
|
// 样式相关
|
||||||
className?: string;
|
className?: string;
|
||||||
itemClassName?: string;
|
itemClassName?: string;
|
||||||
containerStyle?: React.CSSProperties;
|
containerStyle?: React.CSSProperties;
|
||||||
|
|
||||||
// 功能开关
|
// 功能开关
|
||||||
enablePullToRefresh?: boolean;
|
enablePullToRefresh?: boolean;
|
||||||
enableInfiniteScroll?: boolean;
|
enableInfiniteScroll?: boolean;
|
||||||
enableLoadMoreButton?: boolean;
|
enableLoadMoreButton?: boolean;
|
||||||
|
|
||||||
// 自定义高度
|
// 自定义高度
|
||||||
height?: string | number;
|
height?: string | number;
|
||||||
minHeight?: string | number;
|
minHeight?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InfiniteList = <T extends any>({
|
const InfiniteList = <T extends any>({
|
||||||
data,
|
data,
|
||||||
loading = false,
|
loading = false,
|
||||||
hasMore = true,
|
hasMore = true,
|
||||||
loadingText = "加载中...",
|
loadingText = "加载中...",
|
||||||
noMoreText = "没有更多了",
|
noMoreText = "没有更多了",
|
||||||
|
|
||||||
renderItem,
|
renderItem,
|
||||||
keyExtractor = (_, index) => index,
|
keyExtractor = (_, index) => index,
|
||||||
|
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
|
|
||||||
className = "",
|
className = "",
|
||||||
itemClassName = "",
|
itemClassName = "",
|
||||||
containerStyle = {},
|
containerStyle = {},
|
||||||
|
|
||||||
enablePullToRefresh = true,
|
enablePullToRefresh = true,
|
||||||
enableInfiniteScroll = true,
|
enableInfiniteScroll = true,
|
||||||
enableLoadMoreButton = false,
|
enableLoadMoreButton = false,
|
||||||
|
|
||||||
height = "100%",
|
height = "100%",
|
||||||
minHeight = "200px",
|
minHeight = "200px",
|
||||||
}: InfiniteListProps<T>) => {
|
}: InfiniteListProps<T>) => {
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [loadingMore, setLoadingMore] = useState(false);
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// 处理下拉刷新
|
// 处理下拉刷新
|
||||||
const handleRefresh = useCallback(async () => {
|
const handleRefresh = useCallback(async () => {
|
||||||
if (!onRefresh) return;
|
if (!onRefresh) return;
|
||||||
|
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
try {
|
try {
|
||||||
await onRefresh();
|
await onRefresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Refresh failed:", error);
|
console.error("Refresh failed:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
}
|
}
|
||||||
}, [onRefresh]);
|
}, [onRefresh]);
|
||||||
|
|
||||||
// 处理加载更多
|
// 处理加载更多
|
||||||
const handleLoadMore = useCallback(async () => {
|
const handleLoadMore = useCallback(async () => {
|
||||||
if (!onLoadMore || loadingMore || !hasMore) return;
|
if (!onLoadMore || loadingMore || !hasMore) return;
|
||||||
|
|
||||||
setLoadingMore(true);
|
setLoadingMore(true);
|
||||||
try {
|
try {
|
||||||
await onLoadMore();
|
await onLoadMore();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Load more failed:", error);
|
console.error("Load more failed:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingMore(false);
|
setLoadingMore(false);
|
||||||
}
|
}
|
||||||
}, [onLoadMore, loadingMore, hasMore]);
|
}, [onLoadMore, loadingMore, hasMore]);
|
||||||
|
|
||||||
// 点击加载更多按钮
|
// 点击加载更多按钮
|
||||||
const handleLoadMoreClick = useCallback(() => {
|
const handleLoadMoreClick = useCallback(() => {
|
||||||
handleLoadMore();
|
handleLoadMore();
|
||||||
}, [handleLoadMore]);
|
}, [handleLoadMore]);
|
||||||
|
|
||||||
// 容器样式
|
// 容器样式
|
||||||
const containerStyles: React.CSSProperties = {
|
const containerStyles: React.CSSProperties = {
|
||||||
height,
|
height,
|
||||||
minHeight,
|
minHeight,
|
||||||
...containerStyle,
|
...containerStyle,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染列表项
|
// 渲染列表项
|
||||||
const renderListItems = () => {
|
const renderListItems = () => {
|
||||||
return data.map((item, index) => (
|
return data.map((item, index) => (
|
||||||
<div
|
<div
|
||||||
key={keyExtractor(item, index)}
|
key={keyExtractor(item, index)}
|
||||||
className={`${styles.listItem} ${itemClassName}`}
|
className={`${styles.listItem} ${itemClassName}`}
|
||||||
>
|
>
|
||||||
{renderItem(item, index)}
|
{renderItem(item, index)}
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染加载更多按钮
|
// 渲染加载更多按钮
|
||||||
const renderLoadMoreButton = () => {
|
const renderLoadMoreButton = () => {
|
||||||
if (!enableLoadMoreButton || !hasMore) return null;
|
if (!enableLoadMoreButton || !hasMore) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.loadMoreButtonContainer}>
|
<div className={styles.loadMoreButtonContainer}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
loading={loadingMore}
|
loading={loadingMore}
|
||||||
onClick={handleLoadMoreClick}
|
onClick={handleLoadMoreClick}
|
||||||
disabled={loading || !hasMore}
|
disabled={loading || !hasMore}
|
||||||
>
|
>
|
||||||
{loadingMore ? loadingText : "点击加载更多"}
|
{loadingMore ? loadingText : "点击加载更多"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染无更多数据提示
|
// 渲染无更多数据提示
|
||||||
const renderNoMoreText = () => {
|
const renderNoMoreText = () => {
|
||||||
if (hasMore || data.length === 0) return null;
|
if (hasMore || data.length === 0) return null;
|
||||||
|
|
||||||
return <div className={styles.noMoreText}>{noMoreText}</div>;
|
return <div className={styles.noMoreText}>{noMoreText}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染空状态
|
// 渲染空状态
|
||||||
const renderEmptyState = () => {
|
const renderEmptyState = () => {
|
||||||
if (data.length > 0 || loading) return null;
|
if (data.length > 0 || loading) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.emptyState}>
|
<div className={styles.emptyState}>
|
||||||
<div className={styles.emptyIcon}>📝</div>
|
<div className={styles.emptyIcon}>📝</div>
|
||||||
<div className={styles.emptyText}>暂无数据</div>
|
<div className={styles.emptyText}>暂无数据</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div
|
<div
|
||||||
className={`${styles.listContainer} ${className}`}
|
className={`${styles.listContainer} ${className}`}
|
||||||
style={containerStyles}
|
style={containerStyles}
|
||||||
>
|
>
|
||||||
{renderListItems()}
|
{renderListItems()}
|
||||||
{renderLoadMoreButton()}
|
{renderLoadMoreButton()}
|
||||||
{renderNoMoreText()}
|
{renderNoMoreText()}
|
||||||
{renderEmptyState()}
|
{renderEmptyState()}
|
||||||
|
|
||||||
{/* 无限滚动组件 */}
|
{/* 无限滚动组件 */}
|
||||||
{enableInfiniteScroll && (
|
{enableInfiniteScroll && (
|
||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
loadMore={handleLoadMore}
|
loadMore={handleLoadMore}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
threshold={100}
|
threshold={100}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 如果启用下拉刷新,包装PullToRefresh
|
// 如果启用下拉刷新,包装PullToRefresh
|
||||||
if (enablePullToRefresh && onRefresh) {
|
if (enablePullToRefresh && onRefresh) {
|
||||||
return (
|
return (
|
||||||
<PullToRefresh
|
<PullToRefresh
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
className={styles.pullToRefresh}
|
className={styles.pullToRefresh}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</PullToRefresh>
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InfiniteList;
|
export default InfiniteList;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container main {
|
.container main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -25,4 +25,4 @@
|
|||||||
color: #666;
|
color: #666;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,53 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactECharts from "echarts-for-react";
|
import ReactECharts from "echarts-for-react";
|
||||||
|
|
||||||
interface LineChartProps {
|
interface LineChartProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
xData: string[];
|
xData: string[];
|
||||||
yData: number[];
|
yData: number[];
|
||||||
height?: number | string;
|
height?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LineChart: React.FC<LineChartProps> = ({
|
const LineChart: React.FC<LineChartProps> = ({
|
||||||
title = "",
|
title = "",
|
||||||
xData,
|
xData,
|
||||||
yData,
|
yData,
|
||||||
height = 200,
|
height = 200,
|
||||||
}) => {
|
}) => {
|
||||||
const option = {
|
const option = {
|
||||||
title: {
|
title: {
|
||||||
text: title,
|
text: title,
|
||||||
left: "center",
|
left: "center",
|
||||||
textStyle: { fontSize: 16 },
|
textStyle: { fontSize: 16 },
|
||||||
},
|
},
|
||||||
tooltip: { trigger: "axis" },
|
tooltip: { trigger: "axis" },
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: "category",
|
type: "category",
|
||||||
data: xData,
|
data: xData,
|
||||||
boundaryGap: false,
|
boundaryGap: false,
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: "value",
|
type: "value",
|
||||||
boundaryGap: ["10%", "10%"], // 上下留白
|
boundaryGap: ["10%", "10%"], // 上下留白
|
||||||
min: (value: any) => value.min - 10, // 下方多留一点空间
|
min: (value: any) => value.min - 10, // 下方多留一点空间
|
||||||
max: (value: any) => value.max + 10, // 上方多留一点空间
|
max: (value: any) => value.max + 10, // 上方多留一点空间
|
||||||
minInterval: 1,
|
minInterval: 1,
|
||||||
axisLabel: { margin: 12 },
|
axisLabel: { margin: 12 },
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
data: yData,
|
data: yData,
|
||||||
type: "line",
|
type: "line",
|
||||||
smooth: true,
|
smooth: true,
|
||||||
symbol: "circle",
|
symbol: "circle",
|
||||||
lineStyle: { color: "#1677ff" },
|
lineStyle: { color: "#1677ff" },
|
||||||
itemStyle: { color: "#1677ff" },
|
itemStyle: { color: "#1677ff" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
grid: { left: 40, right: 24, top: 40, bottom: 32 },
|
grid: { left: 40, right: 24, top: 40, bottom: 32 },
|
||||||
};
|
};
|
||||||
|
|
||||||
return <ReactECharts option={option} style={{ height, width: "100%" }} />;
|
return <ReactECharts option={option} style={{ height, width: "100%" }} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LineChart;
|
export default LineChart;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { NavBar } from "antd-mobile";
|
import { NavBar } from "antd-mobile";
|
||||||
import { ArrowLeftOutlined } from "@ant-design/icons";
|
import { ArrowLeftOutlined } from "@ant-design/icons";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
interface NavCommonProps {
|
interface NavCommonProps {
|
||||||
title: string;
|
title: string;
|
||||||
backFn?: () => void;
|
backFn?: () => void;
|
||||||
right?: React.ReactNode;
|
right?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NavCommon: React.FC<NavCommonProps> = ({ title, backFn, right }) => {
|
const NavCommon: React.FC<NavCommonProps> = ({ title, backFn, right }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<NavBar
|
<NavBar
|
||||||
back={null}
|
back={null}
|
||||||
style={{ background: "#fff" }}
|
style={{ background: "#fff" }}
|
||||||
left={
|
left={
|
||||||
<div className="nav-title">
|
<div className="nav-title">
|
||||||
<ArrowLeftOutlined
|
<ArrowLeftOutlined
|
||||||
twoToneColor="#1677ff"
|
twoToneColor="#1677ff"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (backFn) {
|
if (backFn) {
|
||||||
backFn();
|
backFn();
|
||||||
} else {
|
} else {
|
||||||
navigate(-1);
|
navigate(-1);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
right={right}
|
right={right}
|
||||||
>
|
>
|
||||||
<span style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
<span style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
||||||
{title}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
</NavBar>
|
</NavBar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NavCommon;
|
export default NavCommon;
|
||||||
|
|||||||
@@ -1,71 +1,71 @@
|
|||||||
.popupFooter {
|
.popupFooter {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-top: 1px solid #f0f0f0;
|
border-top: 1px solid #f0f0f0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectedCount {
|
.selectedCount {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footerBtnGroup {
|
.footerBtnGroup {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paginationRow {
|
.paginationRow {
|
||||||
border-top: 1px solid #f0f0f0;
|
border-top: 1px solid #f0f0f0;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.totalCount {
|
.totalCount {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paginationControls {
|
.paginationControls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pageBtn {
|
.pageBtn {
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
min-width: 32px;
|
min-width: 32px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid #d9d9d9;
|
||||||
color: #333;
|
color: #333;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
&:hover:not(:disabled) {
|
||||||
border-color: #1677ff;
|
border-color: #1677ff;
|
||||||
color: #1677ff;
|
color: #1677ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pageInfo {
|
.pageInfo {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #222;
|
color: #222;
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,67 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button } from "antd";
|
import { Button } from "antd";
|
||||||
import style from "./footer.module.scss";
|
import style from "./footer.module.scss";
|
||||||
import { ArrowLeftOutlined, ArrowRightOutlined } from "@ant-design/icons";
|
import { ArrowLeftOutlined, ArrowRightOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
interface PopupFooterProps {
|
interface PopupFooterProps {
|
||||||
total: number;
|
total: number;
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
selectedCount: number;
|
selectedCount: number;
|
||||||
onPageChange: (page: number) => void;
|
onPageChange: (page: number) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PopupFooter: React.FC<PopupFooterProps> = ({
|
const PopupFooter: React.FC<PopupFooterProps> = ({
|
||||||
total,
|
total,
|
||||||
currentPage,
|
currentPage,
|
||||||
totalPages,
|
totalPages,
|
||||||
loading,
|
loading,
|
||||||
selectedCount,
|
selectedCount,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
onCancel,
|
onCancel,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 分页栏 */}
|
{/* 分页栏 */}
|
||||||
<div className={style.paginationRow}>
|
<div className={style.paginationRow}>
|
||||||
<div className={style.totalCount}>总计 {total} 个设备</div>
|
<div className={style.totalCount}>总计 {total} 个设备</div>
|
||||||
<div className={style.paginationControls}>
|
<div className={style.paginationControls}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
|
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
|
||||||
disabled={currentPage === 1 || loading}
|
disabled={currentPage === 1 || loading}
|
||||||
className={style.pageBtn}
|
className={style.pageBtn}
|
||||||
>
|
>
|
||||||
<ArrowLeftOutlined />
|
<ArrowLeftOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
<span className={style.pageInfo}>
|
<span className={style.pageInfo}>
|
||||||
{currentPage} / {totalPages}
|
{currentPage} / {totalPages}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
|
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
|
||||||
disabled={currentPage === totalPages || loading}
|
disabled={currentPage === totalPages || loading}
|
||||||
className={style.pageBtn}
|
className={style.pageBtn}
|
||||||
>
|
>
|
||||||
<ArrowRightOutlined />
|
<ArrowRightOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.popupFooter}>
|
<div className={style.popupFooter}>
|
||||||
<div className={style.selectedCount}>已选择 {selectedCount} 个设备</div>
|
<div className={style.selectedCount}>已选择 {selectedCount} 个设备</div>
|
||||||
<div className={style.footerBtnGroup}>
|
<div className={style.footerBtnGroup}>
|
||||||
<Button color="primary" variant="filled" onClick={onCancel}>
|
<Button color="primary" variant="filled" onClick={onCancel}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="primary" onClick={onConfirm}>
|
<Button type="primary" onClick={onConfirm}>
|
||||||
确定
|
确定
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PopupFooter;
|
export default PopupFooter;
|
||||||
|
|||||||
@@ -1,52 +1,51 @@
|
|||||||
.popupHeader {
|
.popupHeader {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupTitle {
|
.popupTitle {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupSearchRow {
|
.popupSearchRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupSearchInputWrap {
|
.popupSearchInputWrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputIcon {
|
.inputIcon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
color: #bdbdbd;
|
color: #bdbdbd;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.refreshBtn {
|
||||||
.refreshBtn {
|
width: 36px;
|
||||||
width: 36px;
|
height: 36px;
|
||||||
height: 36px;
|
}
|
||||||
}
|
|
||||||
|
.loadingIcon {
|
||||||
.loadingIcon {
|
animation: spin 1s linear infinite;
|
||||||
animation: spin 1s linear infinite;
|
font-size: 16px;
|
||||||
font-size: 16px;
|
}
|
||||||
}
|
|
||||||
|
@keyframes spin {
|
||||||
@keyframes spin {
|
from {
|
||||||
from {
|
transform: rotate(0deg);
|
||||||
transform: rotate(0deg);
|
}
|
||||||
}
|
to {
|
||||||
to {
|
transform: rotate(360deg);
|
||||||
transform: rotate(360deg);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,86 +1,86 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons";
|
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||||
import { Input, Button } from "antd";
|
import { Input, Button } from "antd";
|
||||||
import { Tabs } from "antd-mobile";
|
import { Tabs } from "antd-mobile";
|
||||||
import style from "./header.module.scss";
|
import style from "./header.module.scss";
|
||||||
|
|
||||||
interface PopupHeaderProps {
|
interface PopupHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
setSearchQuery: (value: string) => void;
|
setSearchQuery: (value: string) => void;
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
showRefresh?: boolean;
|
showRefresh?: boolean;
|
||||||
showSearch?: boolean;
|
showSearch?: boolean;
|
||||||
showTabs?: boolean;
|
showTabs?: boolean;
|
||||||
tabsConfig?: {
|
tabsConfig?: {
|
||||||
activeKey: string;
|
activeKey: string;
|
||||||
onChange: (key: string) => void;
|
onChange: (key: string) => void;
|
||||||
tabs: Array<{ title: string; key: string }>;
|
tabs: Array<{ title: string; key: string }>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const PopupHeader: React.FC<PopupHeaderProps> = ({
|
const PopupHeader: React.FC<PopupHeaderProps> = ({
|
||||||
title,
|
title,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
searchPlaceholder = "搜索...",
|
searchPlaceholder = "搜索...",
|
||||||
loading = false,
|
loading = false,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
showRefresh = true,
|
showRefresh = true,
|
||||||
showSearch = true,
|
showSearch = true,
|
||||||
showTabs = false,
|
showTabs = false,
|
||||||
tabsConfig,
|
tabsConfig,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={style.popupHeader}>
|
<div className={style.popupHeader}>
|
||||||
<div className={style.popupTitle}>{title}</div>
|
<div className={style.popupTitle}>{title}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showSearch && (
|
{showSearch && (
|
||||||
<div className={style.popupSearchRow}>
|
<div className={style.popupSearchRow}>
|
||||||
<div className={style.popupSearchInputWrap}>
|
<div className={style.popupSearchInputWrap}>
|
||||||
<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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showRefresh && onRefresh && (
|
{showRefresh && onRefresh && (
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
onClick={onRefresh}
|
onClick={onRefresh}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className={style.refreshBtn}
|
className={style.refreshBtn}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className={style.loadingIcon}>⟳</div>
|
<div className={style.loadingIcon}>⟳</div>
|
||||||
) : (
|
) : (
|
||||||
<ReloadOutlined />
|
<ReloadOutlined />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showTabs && tabsConfig && (
|
{showTabs && tabsConfig && (
|
||||||
<Tabs
|
<Tabs
|
||||||
activeKey={tabsConfig.activeKey}
|
activeKey={tabsConfig.activeKey}
|
||||||
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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PopupHeader;
|
export default PopupHeader;
|
||||||
|
|||||||
@@ -1,67 +1,67 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import DeviceSelection from "./DeviceSelection";
|
import DeviceSelection from "./DeviceSelection";
|
||||||
import FriendSelection from "./FriendSelection";
|
import FriendSelection from "./FriendSelection";
|
||||||
import GroupSelection from "./GroupSelection";
|
import GroupSelection from "./GroupSelection";
|
||||||
import { Button, Space } from "antd-mobile";
|
import { Button, Space } from "antd-mobile";
|
||||||
|
|
||||||
export default function SelectionTest() {
|
export default function SelectionTest() {
|
||||||
// 设备选择
|
// 设备选择
|
||||||
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
|
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
|
||||||
const [deviceDialogOpen, setDeviceDialogOpen] = useState(false);
|
const [deviceDialogOpen, setDeviceDialogOpen] = useState(false);
|
||||||
|
|
||||||
// 好友选择
|
// 好友选择
|
||||||
const [selectedFriends, setSelectedFriends] = useState<string[]>([]);
|
const [selectedFriends, setSelectedFriends] = useState<string[]>([]);
|
||||||
const [friendDialogOpen, setFriendDialogOpen] = useState(false);
|
const [friendDialogOpen, setFriendDialogOpen] = useState(false);
|
||||||
|
|
||||||
// 群组选择
|
// 群组选择
|
||||||
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
|
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
|
||||||
const [groupDialogOpen, setGroupDialogOpen] = useState(false);
|
const [groupDialogOpen, setGroupDialogOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 24 }}>
|
<div style={{ padding: 24 }}>
|
||||||
<h2>选择弹窗测试</h2>
|
<h2>选择弹窗测试</h2>
|
||||||
<Space direction="vertical" block>
|
<Space direction="vertical" block>
|
||||||
<div>
|
<div>
|
||||||
<b>DeviceSelection(内嵌输入框+弹窗)</b>
|
<b>DeviceSelection(内嵌输入框+弹窗)</b>
|
||||||
<DeviceSelection
|
<DeviceSelection
|
||||||
selectedDevices={selectedDevices}
|
selectedDevices={selectedDevices}
|
||||||
onSelect={setSelectedDevices}
|
onSelect={setSelectedDevices}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<b>FriendSelection</b>
|
<b>FriendSelection</b>
|
||||||
<Button color="primary" onClick={() => setFriendDialogOpen(true)}>
|
<Button color="primary" onClick={() => setFriendDialogOpen(true)}>
|
||||||
打开好友选择弹窗
|
打开好友选择弹窗
|
||||||
</Button>
|
</Button>
|
||||||
<FriendSelection
|
<FriendSelection
|
||||||
selectedFriends={selectedFriends}
|
selectedFriends={selectedFriends}
|
||||||
onSelect={setSelectedFriends}
|
onSelect={setSelectedFriends}
|
||||||
placeholder="请选择微信好友"
|
placeholder="请选择微信好友"
|
||||||
className=""
|
className=""
|
||||||
visible={friendDialogOpen}
|
visible={friendDialogOpen}
|
||||||
onVisibleChange={setFriendDialogOpen}
|
onVisibleChange={setFriendDialogOpen}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<b>GroupSelection</b>
|
<b>GroupSelection</b>
|
||||||
<Button color="primary" onClick={() => setGroupDialogOpen(true)}>
|
<Button color="primary" onClick={() => setGroupDialogOpen(true)}>
|
||||||
打开群组选择弹窗
|
打开群组选择弹窗
|
||||||
</Button>
|
</Button>
|
||||||
<GroupSelection
|
<GroupSelection
|
||||||
selectedGroups={selectedGroups}
|
selectedGroups={selectedGroups}
|
||||||
onSelect={setSelectedGroups}
|
onSelect={setSelectedGroups}
|
||||||
placeholder="请选择群聊"
|
placeholder="请选择群聊"
|
||||||
className=""
|
className=""
|
||||||
visible={groupDialogOpen}
|
visible={groupDialogOpen}
|
||||||
onVisibleChange={setGroupDialogOpen}
|
onVisibleChange={setGroupDialogOpen}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
<div style={{ marginTop: 32 }}>
|
<div style={{ marginTop: 32 }}>
|
||||||
<div>已选设备ID: {selectedDevices.join(", ")}</div>
|
<div>已选设备ID: {selectedDevices.join(", ")}</div>
|
||||||
<div>已选好友ID: {selectedFriends.join(", ")}</div>
|
<div>已选好友ID: {selectedFriends.join(", ")}</div>
|
||||||
<div>已选群组ID: {selectedGroups.join(", ")}</div>
|
<div>已选群组ID: {selectedGroups.join(", ")}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,43 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Steps } from "antd-mobile";
|
import { Steps } from "antd-mobile";
|
||||||
|
|
||||||
interface StepIndicatorProps {
|
interface StepIndicatorProps {
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
steps: { id: number; title: string; subtitle: string }[];
|
steps: { id: number; title: string; subtitle: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepIndicator: React.FC<StepIndicatorProps> = ({
|
const StepIndicator: React.FC<StepIndicatorProps> = ({
|
||||||
currentStep,
|
currentStep,
|
||||||
steps,
|
steps,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div style={{ overflowX: "auto", padding: "30px 0px", background: "#fff" }}>
|
<div style={{ overflowX: "auto", padding: "30px 0px", background: "#fff" }}>
|
||||||
<Steps current={currentStep - 1}>
|
<Steps current={currentStep - 1}>
|
||||||
{steps.map((step, idx) => (
|
{steps.map((step, idx) => (
|
||||||
<Steps.Step
|
<Steps.Step
|
||||||
key={step.id}
|
key={step.id}
|
||||||
title={step.subtitle}
|
title={step.subtitle}
|
||||||
icon={
|
icon={
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
backgroundColor: idx < currentStep ? "#1677ff" : "#cccccc",
|
backgroundColor: idx < currentStep ? "#1677ff" : "#cccccc",
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{step.id}
|
{step.id}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Steps>
|
</Steps>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StepIndicator;
|
export default StepIndicator;
|
||||||
|
|||||||
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,8 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import "./styles/global.scss";
|
import "./styles/global.scss";
|
||||||
// import VConsole from "vconsole";
|
// import VConsole from "vconsole";
|
||||||
// new VConsole();
|
// new VConsole();
|
||||||
const root = createRoot(document.getElementById("root")!);
|
const root = createRoot(document.getElementById("root")!);
|
||||||
root.render(<App />);
|
root.render(<App />);
|
||||||
|
|||||||
@@ -1,53 +1,53 @@
|
|||||||
import request from '@/api/request';
|
import request from "@/api/request";
|
||||||
export interface LoginParams {
|
export interface LoginParams {
|
||||||
phone: string;
|
phone: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
verificationCode?: string;
|
verificationCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
code: number;
|
code: number;
|
||||||
msg: string;
|
msg: string;
|
||||||
data: {
|
data: {
|
||||||
token: string;
|
token: string;
|
||||||
token_expired: string;
|
token_expired: string;
|
||||||
member: {
|
member: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
s2_accountId: string;
|
s2_accountId: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendCodeResponse {
|
export interface SendCodeResponse {
|
||||||
code: number;
|
code: number;
|
||||||
msg: string;
|
msg: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 密码登录
|
// 密码登录
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,439 +1,440 @@
|
|||||||
.login-page {
|
.login-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--primary-gradient);
|
background: var(--primary-gradient);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 背景装饰
|
// 背景装饰
|
||||||
.bg-decoration {
|
.bg-decoration {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-circle {
|
.bg-circle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
animation: float 6s ease-in-out infinite;
|
animation: float 6s ease-in-out infinite;
|
||||||
|
|
||||||
&:nth-child(1) {
|
&:nth-child(1) {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
top: -100px;
|
top: -100px;
|
||||||
right: -100px;
|
right: -100px;
|
||||||
animation-delay: 0s;
|
animation-delay: 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(2) {
|
&:nth-child(2) {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
bottom: -75px;
|
bottom: -75px;
|
||||||
left: -75px;
|
left: -75px;
|
||||||
animation-delay: 2s;
|
animation-delay: 2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(3) {
|
&:nth-child(3) {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
right: 10%;
|
right: 10%;
|
||||||
animation-delay: 4s;
|
animation-delay: 4s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
0%, 100% {
|
0%,
|
||||||
transform: translateY(0px) rotate(0deg);
|
100% {
|
||||||
}
|
transform: translateY(0px) rotate(0deg);
|
||||||
50% {
|
}
|
||||||
transform: translateY(-20px) rotate(180deg);
|
50% {
|
||||||
}
|
transform: translateY(-20px) rotate(180deg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.login-container {
|
|
||||||
width: 100%;
|
.login-container {
|
||||||
max-width: 420px;
|
width: 100%;
|
||||||
background: #ffffff;
|
max-width: 420px;
|
||||||
backdrop-filter: blur(20px);
|
background: #ffffff;
|
||||||
border-radius: 24px;
|
backdrop-filter: blur(20px);
|
||||||
padding: 24px 20px;
|
border-radius: 24px;
|
||||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
padding: 24px 20px;
|
||||||
position: relative;
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||||
z-index: 1;
|
position: relative;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
z-index: 1;
|
||||||
}
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
.login-header {
|
|
||||||
text-align: center;
|
.login-header {
|
||||||
margin-bottom: 24px;
|
text-align: center;
|
||||||
}
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
.logo-section {
|
|
||||||
display: flex;
|
.logo-section {
|
||||||
align-items: center;
|
display: flex;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
gap: 12px;
|
justify-content: center;
|
||||||
margin-bottom: 16px;
|
gap: 12px;
|
||||||
}
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
.logo-icon {
|
|
||||||
width: 40px;
|
.logo-icon {
|
||||||
height: 40px;
|
width: 40px;
|
||||||
background: var(--primary-gradient);
|
height: 40px;
|
||||||
border-radius: 10px;
|
background: var(--primary-gradient);
|
||||||
display: flex;
|
border-radius: 10px;
|
||||||
align-items: center;
|
display: flex;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
color: white;
|
justify-content: center;
|
||||||
font-size: 20px;
|
color: white;
|
||||||
box-shadow: 0 6px 12px var(--primary-shadow);
|
font-size: 20px;
|
||||||
}
|
box-shadow: 0 6px 12px var(--primary-shadow);
|
||||||
|
}
|
||||||
.app-name {
|
|
||||||
font-size: 24px;
|
.app-name {
|
||||||
font-weight: 800;
|
font-size: 24px;
|
||||||
background: var(--primary-gradient);
|
font-weight: 800;
|
||||||
-webkit-background-clip: text;
|
background: var(--primary-gradient);
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-background-clip: text;
|
||||||
background-clip: text;
|
-webkit-text-fill-color: transparent;
|
||||||
margin: 0;
|
background-clip: text;
|
||||||
}
|
margin: 0;
|
||||||
|
}
|
||||||
.subtitle {
|
|
||||||
font-size: 13px;
|
.subtitle {
|
||||||
color: #666;
|
font-size: 13px;
|
||||||
margin: 0;
|
color: #666;
|
||||||
}
|
margin: 0;
|
||||||
|
}
|
||||||
.form-container {
|
|
||||||
margin-bottom: 20px;
|
.form-container {
|
||||||
}
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
// 标签页样式
|
|
||||||
.tab-container {
|
// 标签页样式
|
||||||
display: flex;
|
.tab-container {
|
||||||
background: #f8f9fa;
|
display: flex;
|
||||||
border-radius: 10px;
|
background: #f8f9fa;
|
||||||
padding: 3px;
|
border-radius: 10px;
|
||||||
margin-bottom: 24px;
|
padding: 3px;
|
||||||
position: relative;
|
margin-bottom: 24px;
|
||||||
}
|
position: relative;
|
||||||
|
}
|
||||||
.tab-item {
|
|
||||||
flex: 1;
|
.tab-item {
|
||||||
text-align: center;
|
flex: 1;
|
||||||
padding: 10px 12px;
|
text-align: center;
|
||||||
font-size: 13px;
|
padding: 10px 12px;
|
||||||
font-weight: 500;
|
font-size: 13px;
|
||||||
color: #666;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
color: #666;
|
||||||
border-radius: 7px;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
border-radius: 7px;
|
||||||
position: relative;
|
transition: all 0.3s ease;
|
||||||
z-index: 2;
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
&.active {
|
|
||||||
color: var(--primary-color);
|
&.active {
|
||||||
font-weight: 600;
|
color: var(--primary-color);
|
||||||
background: white;
|
font-weight: 600;
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
background: white;
|
||||||
}
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.tab-indicator {
|
|
||||||
display: none; // 隐藏分割线指示器
|
.tab-indicator {
|
||||||
}
|
display: none; // 隐藏分割线指示器
|
||||||
|
}
|
||||||
// 表单样式
|
|
||||||
.login-form {
|
// 表单样式
|
||||||
:global(.adm-form) {
|
.login-form {
|
||||||
--adm-font-size-main: 14px;
|
:global(.adm-form) {
|
||||||
}
|
--adm-font-size-main: 14px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.input-group {
|
|
||||||
margin-bottom: 18px;
|
.input-group {
|
||||||
}
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
.input-label {
|
|
||||||
display: block;
|
.input-label {
|
||||||
font-size: 13px;
|
display: block;
|
||||||
font-weight: 600;
|
font-size: 13px;
|
||||||
color: #333;
|
font-weight: 600;
|
||||||
margin-bottom: 6px;
|
color: #333;
|
||||||
}
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
.input-wrapper {
|
|
||||||
position: relative;
|
.input-wrapper {
|
||||||
display: flex;
|
position: relative;
|
||||||
align-items: center;
|
display: flex;
|
||||||
background: #f8f9fa;
|
align-items: center;
|
||||||
border: 2px solid transparent;
|
background: #f8f9fa;
|
||||||
border-radius: 10px;
|
border: 2px solid transparent;
|
||||||
transition: all 0.3s ease;
|
border-radius: 10px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
&:focus-within {
|
|
||||||
border-color: var(--primary-color);
|
&:focus-within {
|
||||||
background: white;
|
border-color: var(--primary-color);
|
||||||
box-shadow: 0 0 0 3px var(--primary-shadow-light);
|
background: white;
|
||||||
}
|
box-shadow: 0 0 0 3px var(--primary-shadow-light);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.input-prefix {
|
|
||||||
padding: 0 12px;
|
.input-prefix {
|
||||||
color: #666;
|
padding: 0 12px;
|
||||||
font-size: 13px;
|
color: #666;
|
||||||
font-weight: 500;
|
font-size: 13px;
|
||||||
border-right: 1px solid #e5e5e5;
|
font-weight: 500;
|
||||||
}
|
border-right: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
.phone-input,
|
|
||||||
.password-input,
|
.phone-input,
|
||||||
.code-input {
|
.password-input,
|
||||||
flex: 1;
|
.code-input {
|
||||||
border: none !important;
|
flex: 1;
|
||||||
background: transparent !important;
|
border: none !important;
|
||||||
padding: 12px 14px !important;
|
background: transparent !important;
|
||||||
font-size: 15px !important;
|
padding: 12px 14px !important;
|
||||||
color: #333 !important;
|
font-size: 15px !important;
|
||||||
|
color: #333 !important;
|
||||||
&::placeholder {
|
|
||||||
color: #999;
|
&::placeholder {
|
||||||
}
|
color: #999;
|
||||||
|
}
|
||||||
&:focus {
|
|
||||||
box-shadow: none !important;
|
&:focus {
|
||||||
}
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.eye-icon {
|
|
||||||
padding: 0 12px;
|
.eye-icon {
|
||||||
color: #666;
|
padding: 0 12px;
|
||||||
cursor: pointer;
|
color: #666;
|
||||||
transition: color 0.3s ease;
|
cursor: pointer;
|
||||||
|
transition: color 0.3s ease;
|
||||||
&:hover {
|
|
||||||
color: var(--primary-color);
|
&:hover {
|
||||||
}
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.send-code-btn {
|
|
||||||
padding: 6px 12px;
|
.send-code-btn {
|
||||||
margin-right: 6px;
|
padding: 6px 12px;
|
||||||
background: var(--primary-gradient);
|
margin-right: 6px;
|
||||||
color: white;
|
background: var(--primary-gradient);
|
||||||
border: none;
|
color: white;
|
||||||
border-radius: 6px;
|
border: none;
|
||||||
font-size: 11px;
|
border-radius: 6px;
|
||||||
font-weight: 500;
|
font-size: 11px;
|
||||||
cursor: pointer;
|
font-weight: 500;
|
||||||
transition: all 0.3s ease;
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
&:hover:not(.disabled) {
|
|
||||||
transform: translateY(-1px);
|
&:hover:not(.disabled) {
|
||||||
box-shadow: 0 3px 8px var(--primary-shadow);
|
transform: translateY(-1px);
|
||||||
}
|
box-shadow: 0 3px 8px var(--primary-shadow);
|
||||||
|
}
|
||||||
&.disabled {
|
|
||||||
background: #e5e5e5;
|
&.disabled {
|
||||||
color: #999;
|
background: #e5e5e5;
|
||||||
cursor: not-allowed;
|
color: #999;
|
||||||
transform: none;
|
cursor: not-allowed;
|
||||||
box-shadow: none;
|
transform: none;
|
||||||
}
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.agreement-section {
|
|
||||||
margin-bottom: 24px;
|
.agreement-section {
|
||||||
}
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
.agreement-checkbox {
|
|
||||||
display: flex;
|
.agreement-checkbox {
|
||||||
align-items: center;
|
display: flex;
|
||||||
gap: 6px;
|
align-items: center;
|
||||||
font-size: 12px;
|
gap: 6px;
|
||||||
color: #666;
|
font-size: 12px;
|
||||||
line-height: 1.3;
|
color: #666;
|
||||||
white-space: nowrap;
|
line-height: 1.3;
|
||||||
|
white-space: nowrap;
|
||||||
:global(.adm-checkbox) {
|
|
||||||
margin-top: 0;
|
:global(.adm-checkbox) {
|
||||||
flex-shrink: 0;
|
margin-top: 0;
|
||||||
transform: scale(0.8);
|
flex-shrink: 0;
|
||||||
}
|
transform: scale(0.8);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.agreement-text {
|
|
||||||
flex: 1;
|
.agreement-text {
|
||||||
display: flex;
|
flex: 1;
|
||||||
align-items: center;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
align-items: center;
|
||||||
white-space: nowrap;
|
flex-wrap: nowrap;
|
||||||
overflow: visible;
|
white-space: nowrap;
|
||||||
text-overflow: clip;
|
overflow: visible;
|
||||||
font-size: 13px;
|
text-overflow: clip;
|
||||||
}
|
font-size: 13px;
|
||||||
|
}
|
||||||
.agreement-link {
|
|
||||||
color: var(--primary-color);
|
.agreement-link {
|
||||||
cursor: pointer;
|
color: var(--primary-color);
|
||||||
text-decoration: none;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
text-decoration: none;
|
||||||
font-size: 11px;
|
white-space: nowrap;
|
||||||
|
font-size: 11px;
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
&:hover {
|
||||||
}
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.login-btn {
|
|
||||||
height: 46px;
|
.login-btn {
|
||||||
font-size: 15px;
|
height: 46px;
|
||||||
font-weight: 600;
|
font-size: 15px;
|
||||||
border-radius: 10px;
|
font-weight: 600;
|
||||||
background: var(--primary-gradient);
|
border-radius: 10px;
|
||||||
border: none;
|
background: var(--primary-gradient);
|
||||||
box-shadow: 0 6px 12px var(--primary-shadow);
|
border: none;
|
||||||
transition: all 0.3s ease;
|
box-shadow: 0 6px 12px var(--primary-shadow);
|
||||||
|
transition: all 0.3s ease;
|
||||||
&:hover:not(:disabled) {
|
|
||||||
transform: translateY(-1px);
|
&:hover:not(:disabled) {
|
||||||
box-shadow: 0 8px 16px var(--primary-shadow-dark);
|
transform: translateY(-1px);
|
||||||
}
|
box-shadow: 0 8px 16px var(--primary-shadow-dark);
|
||||||
|
}
|
||||||
&:disabled {
|
|
||||||
background: #e5e5e5;
|
&:disabled {
|
||||||
color: #999;
|
background: #e5e5e5;
|
||||||
transform: none;
|
color: #999;
|
||||||
box-shadow: none;
|
transform: none;
|
||||||
}
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.divider {
|
|
||||||
position: relative;
|
.divider {
|
||||||
text-align: center;
|
position: relative;
|
||||||
margin: 24px 0;
|
text-align: center;
|
||||||
|
margin: 24px 0;
|
||||||
&::before {
|
|
||||||
content: '';
|
&::before {
|
||||||
position: absolute;
|
content: "";
|
||||||
top: 50%;
|
position: absolute;
|
||||||
left: 0;
|
top: 50%;
|
||||||
right: 0;
|
left: 0;
|
||||||
height: 1px;
|
right: 0;
|
||||||
background: #e5e5e5;
|
height: 1px;
|
||||||
}
|
background: #e5e5e5;
|
||||||
|
}
|
||||||
span {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
span {
|
||||||
padding: 0 12px;
|
background: rgba(255, 255, 255, 0.95);
|
||||||
color: #999;
|
padding: 0 12px;
|
||||||
font-size: 11px;
|
color: #999;
|
||||||
font-weight: 500;
|
font-size: 11px;
|
||||||
}
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.third-party-login {
|
|
||||||
display: flex;
|
.third-party-login {
|
||||||
justify-content: center;
|
display: flex;
|
||||||
gap: 20px;
|
justify-content: center;
|
||||||
}
|
gap: 20px;
|
||||||
|
}
|
||||||
.third-party-item {
|
|
||||||
display: flex;
|
.third-party-item {
|
||||||
flex-direction: column;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
gap: 6px;
|
align-items: center;
|
||||||
cursor: pointer;
|
gap: 6px;
|
||||||
padding: 12px;
|
cursor: pointer;
|
||||||
border-radius: 10px;
|
padding: 12px;
|
||||||
transition: all 0.3s ease;
|
border-radius: 10px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
&:hover {
|
|
||||||
background: #f8f9fa;
|
&:hover {
|
||||||
transform: translateY(-1px);
|
background: #f8f9fa;
|
||||||
}
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
span {
|
|
||||||
font-size: 11px;
|
span {
|
||||||
color: #666;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
color: #666;
|
||||||
}
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.wechat-icon,
|
|
||||||
.apple-icon {
|
.wechat-icon,
|
||||||
width: 36px;
|
.apple-icon {
|
||||||
height: 36px;
|
width: 36px;
|
||||||
border-radius: 10px;
|
height: 36px;
|
||||||
display: flex;
|
border-radius: 10px;
|
||||||
align-items: center;
|
display: flex;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
color: white;
|
justify-content: center;
|
||||||
font-size: 18px;
|
color: white;
|
||||||
transition: all 0.3s ease;
|
font-size: 18px;
|
||||||
}
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
.wechat-icon {
|
|
||||||
background: #07c160;
|
.wechat-icon {
|
||||||
box-shadow: 0 3px 8px rgba(7, 193, 96, 0.3);
|
background: #07c160;
|
||||||
|
box-shadow: 0 3px 8px rgba(7, 193, 96, 0.3);
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.4);
|
&:hover {
|
||||||
}
|
box-shadow: 0 4px 12px rgba(7, 193, 96, 0.4);
|
||||||
|
}
|
||||||
svg {
|
|
||||||
width: 20px;
|
svg {
|
||||||
height: 20px;
|
width: 20px;
|
||||||
}
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.apple-icon {
|
|
||||||
background: #000;
|
.apple-icon {
|
||||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
|
background: #000;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
&:hover {
|
||||||
}
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
svg {
|
|
||||||
width: 20px;
|
svg {
|
||||||
height: 20px;
|
width: 20px;
|
||||||
}
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 响应式设计
|
|
||||||
@media (max-width: 480px) {
|
// 响应式设计
|
||||||
.login-container {
|
@media (max-width: 480px) {
|
||||||
padding: 24px 20px;
|
.login-container {
|
||||||
margin: 0 12px;
|
padding: 24px 20px;
|
||||||
}
|
margin: 0 12px;
|
||||||
|
}
|
||||||
.app-name {
|
|
||||||
font-size: 22px;
|
.app-name {
|
||||||
}
|
font-size: 22px;
|
||||||
|
}
|
||||||
.third-party-login {
|
|
||||||
gap: 16px;
|
.third-party-login {
|
||||||
}
|
gap: 16px;
|
||||||
|
}
|
||||||
.third-party-item {
|
|
||||||
padding: 10px;
|
.third-party-item {
|
||||||
}
|
padding: 10px;
|
||||||
|
}
|
||||||
.wechat-icon,
|
|
||||||
.apple-icon {
|
.wechat-icon,
|
||||||
width: 32px;
|
.apple-icon {
|
||||||
height: 32px;
|
width: 32px;
|
||||||
}
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,160 +1,160 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { NavBar, Tabs } from "antd-mobile";
|
import { NavBar, Tabs } from "antd-mobile";
|
||||||
import { ArrowLeftOutlined } from "@ant-design/icons";
|
import { ArrowLeftOutlined } from "@ant-design/icons";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
import NavCommon from "@/components/NavCommon";
|
import NavCommon from "@/components/NavCommon";
|
||||||
import DeviceSelection from "@/components/DeviceSelection";
|
import DeviceSelection from "@/components/DeviceSelection";
|
||||||
import FriendSelection from "@/components/FriendSelection";
|
import FriendSelection from "@/components/FriendSelection";
|
||||||
import GroupSelection from "@/components/GroupSelection";
|
import GroupSelection from "@/components/GroupSelection";
|
||||||
import ContentLibrarySelection from "@/components/ContentLibrarySelection";
|
import ContentLibrarySelection from "@/components/ContentLibrarySelection";
|
||||||
import AccountSelection from "@/components/AccountSelection";
|
import AccountSelection from "@/components/AccountSelection";
|
||||||
|
|
||||||
const ComponentTest: React.FC = () => {
|
const ComponentTest: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [activeTab, setActiveTab] = useState("devices");
|
const [activeTab, setActiveTab] = useState("devices");
|
||||||
|
|
||||||
// 设备选择状态
|
// 设备选择状态
|
||||||
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
|
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
|
||||||
|
|
||||||
// 好友选择状态
|
// 好友选择状态
|
||||||
const [selectedFriends, setSelectedFriends] = useState<string[]>([]);
|
const [selectedFriends, setSelectedFriends] = useState<string[]>([]);
|
||||||
|
|
||||||
// 群组选择状态
|
// 群组选择状态
|
||||||
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
|
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
|
||||||
|
|
||||||
// 内容库选择状态
|
// 内容库选择状态
|
||||||
const [selectedLibraries, setSelectedLibraries] = useState<string[]>([]);
|
const [selectedLibraries, setSelectedLibraries] = useState<string[]>([]);
|
||||||
|
|
||||||
const [selectedAccounts, setSelectedAccounts] = useState<string[]>([]);
|
const [selectedAccounts, setSelectedAccounts] = useState<string[]>([]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout header={<NavCommon title="组件调试" />}>
|
<Layout header={<NavCommon title="组件调试" />}>
|
||||||
<div style={{ padding: 16 }}>
|
<div style={{ padding: 16 }}>
|
||||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||||
<Tabs.Tab title="设备选择" key="devices">
|
<Tabs.Tab title="设备选择" key="devices">
|
||||||
<div style={{ padding: "16px 0" }}>
|
<div style={{ padding: "16px 0" }}>
|
||||||
<h3 style={{ marginBottom: 16 }}>DeviceSelection 组件测试</h3>
|
<h3 style={{ marginBottom: 16 }}>DeviceSelection 组件测试</h3>
|
||||||
<DeviceSelection
|
<DeviceSelection
|
||||||
selectedDevices={selectedDevices}
|
selectedDevices={selectedDevices}
|
||||||
onSelect={setSelectedDevices}
|
onSelect={setSelectedDevices}
|
||||||
placeholder="请选择设备"
|
placeholder="请选择设备"
|
||||||
showSelectedList={true}
|
showSelectedList={true}
|
||||||
selectedListMaxHeight={300}
|
selectedListMaxHeight={300}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
background: "#f5f5f5",
|
background: "#f5f5f5",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong>已选设备:</strong> {selectedDevices.length} 个
|
<strong>已选设备:</strong> {selectedDevices.length} 个
|
||||||
<br />
|
<br />
|
||||||
<strong>设备ID:</strong> {selectedDevices.join(", ") || "无"}
|
<strong>设备ID:</strong> {selectedDevices.join(", ") || "无"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
|
||||||
<Tabs.Tab title="好友选择" key="friends">
|
<Tabs.Tab title="好友选择" key="friends">
|
||||||
<div style={{ padding: "16px 0" }}>
|
<div style={{ padding: "16px 0" }}>
|
||||||
<h3 style={{ marginBottom: 16 }}>FriendSelection 组件测试</h3>
|
<h3 style={{ marginBottom: 16 }}>FriendSelection 组件测试</h3>
|
||||||
<FriendSelection
|
<FriendSelection
|
||||||
selectedFriends={selectedFriends}
|
selectedFriends={selectedFriends}
|
||||||
onSelect={setSelectedFriends}
|
onSelect={setSelectedFriends}
|
||||||
placeholder="请选择微信好友"
|
placeholder="请选择微信好友"
|
||||||
showSelectedList={true}
|
showSelectedList={true}
|
||||||
selectedListMaxHeight={300}
|
selectedListMaxHeight={300}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
background: "#f5f5f5",
|
background: "#f5f5f5",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong>已选好友:</strong> {selectedFriends.length} 个
|
<strong>已选好友:</strong> {selectedFriends.length} 个
|
||||||
<br />
|
<br />
|
||||||
<strong>好友ID:</strong> {selectedFriends.join(", ") || "无"}
|
<strong>好友ID:</strong> {selectedFriends.join(", ") || "无"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
|
||||||
<Tabs.Tab title="群组选择" key="groups">
|
<Tabs.Tab title="群组选择" key="groups">
|
||||||
<div style={{ padding: "16px 0" }}>
|
<div style={{ padding: "16px 0" }}>
|
||||||
<h3 style={{ marginBottom: 16 }}>GroupSelection 组件测试</h3>
|
<h3 style={{ marginBottom: 16 }}>GroupSelection 组件测试</h3>
|
||||||
<GroupSelection
|
<GroupSelection
|
||||||
selectedGroups={selectedGroups}
|
selectedGroups={selectedGroups}
|
||||||
onSelect={setSelectedGroups}
|
onSelect={setSelectedGroups}
|
||||||
placeholder="请选择微信群组"
|
placeholder="请选择微信群组"
|
||||||
showSelectedList={true}
|
showSelectedList={true}
|
||||||
selectedListMaxHeight={300}
|
selectedListMaxHeight={300}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
background: "#f5f5f5",
|
background: "#f5f5f5",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong>已选群组:</strong> {selectedGroups.length} 个
|
<strong>已选群组:</strong> {selectedGroups.length} 个
|
||||||
<br />
|
<br />
|
||||||
<strong>群组ID:</strong> {selectedGroups.join(", ") || "无"}
|
<strong>群组ID:</strong> {selectedGroups.join(", ") || "无"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
|
||||||
<Tabs.Tab title="内容库选择" key="libraries">
|
<Tabs.Tab title="内容库选择" key="libraries">
|
||||||
<div style={{ padding: "16px 0" }}>
|
<div style={{ padding: "16px 0" }}>
|
||||||
<h3 style={{ marginBottom: 16 }}>
|
<h3 style={{ marginBottom: 16 }}>
|
||||||
ContentLibrarySelection 组件测试
|
ContentLibrarySelection 组件测试
|
||||||
</h3>
|
</h3>
|
||||||
<ContentLibrarySelection
|
<ContentLibrarySelection
|
||||||
selectedLibraries={selectedLibraries}
|
selectedLibraries={selectedLibraries}
|
||||||
onSelect={setSelectedLibraries}
|
onSelect={setSelectedLibraries}
|
||||||
placeholder="请选择内容库"
|
placeholder="请选择内容库"
|
||||||
showSelectedList={true}
|
showSelectedList={true}
|
||||||
selectedListMaxHeight={300}
|
selectedListMaxHeight={300}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
background: "#f5f5f5",
|
background: "#f5f5f5",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong>已选内容库:</strong> {selectedLibraries.length} 个
|
<strong>已选内容库:</strong> {selectedLibraries.length} 个
|
||||||
<br />
|
<br />
|
||||||
<strong>内容库ID:</strong>{" "}
|
<strong>内容库ID:</strong>{" "}
|
||||||
{selectedLibraries.join(", ") || "无"}
|
{selectedLibraries.join(", ") || "无"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
|
||||||
<Tabs.Tab title="账号选择" key="accounts">
|
<Tabs.Tab title="账号选择" key="accounts">
|
||||||
<div style={{ padding: "16px 0" }}>
|
<div style={{ padding: "16px 0" }}>
|
||||||
<h3 style={{ marginBottom: 16 }}>AccountSelection 组件测试</h3>
|
<h3 style={{ marginBottom: 16 }}>AccountSelection 组件测试</h3>
|
||||||
<AccountSelection
|
<AccountSelection
|
||||||
value={selectedAccounts}
|
value={selectedAccounts}
|
||||||
onChange={setSelectedAccounts}
|
onChange={setSelectedAccounts}
|
||||||
// 可根据实际API和props补充其它参数
|
// 可根据实际API和props补充其它参数
|
||||||
/>
|
/>
|
||||||
<div style={{ marginTop: 16 }}>
|
<div style={{ marginTop: 16 }}>
|
||||||
<strong>已选账号:</strong>
|
<strong>已选账号:</strong>
|
||||||
{selectedAccounts.length > 0
|
{selectedAccounts.length > 0
|
||||||
? selectedAccounts.join(", ")
|
? selectedAccounts.join(", ")
|
||||||
: "无"}
|
: "无"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ComponentTest;
|
export default ComponentTest;
|
||||||
|
|||||||
@@ -1,61 +1,61 @@
|
|||||||
// 内容库表单数据类型定义
|
// 内容库表单数据类型定义
|
||||||
export interface ContentLibrary {
|
export interface ContentLibrary {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
sourceType: number; // 1=微信好友, 2=聊天群
|
sourceType: number; // 1=微信好友, 2=聊天群
|
||||||
creatorName?: string;
|
creatorName?: string;
|
||||||
updateTime: string;
|
updateTime: string;
|
||||||
status: number; // 0=未启用, 1=已启用
|
status: number; // 0=未启用, 1=已启用
|
||||||
itemCount?: number;
|
itemCount?: number;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
sourceFriends?: string[];
|
sourceFriends?: string[];
|
||||||
sourceGroups?: string[];
|
sourceGroups?: string[];
|
||||||
keywordInclude?: string[];
|
keywordInclude?: string[];
|
||||||
keywordExclude?: string[];
|
keywordExclude?: string[];
|
||||||
aiPrompt?: string;
|
aiPrompt?: string;
|
||||||
timeEnabled?: number;
|
timeEnabled?: number;
|
||||||
timeStart?: string;
|
timeStart?: string;
|
||||||
timeEnd?: string;
|
timeEnd?: string;
|
||||||
selectedFriends?: any[];
|
selectedFriends?: any[];
|
||||||
selectedGroups?: any[];
|
selectedGroups?: any[];
|
||||||
selectedGroupMembers?: WechatGroupMember[];
|
selectedGroupMembers?: WechatGroupMember[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 微信群成员
|
// 微信群成员
|
||||||
export interface WechatGroupMember {
|
export interface WechatGroupMember {
|
||||||
id: string;
|
id: string;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
wechatId: string;
|
wechatId: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
gender?: "male" | "female";
|
gender?: "male" | "female";
|
||||||
role?: "owner" | "admin" | "member";
|
role?: "owner" | "admin" | "member";
|
||||||
joinTime?: string;
|
joinTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API 响应类型
|
// API 响应类型
|
||||||
export interface ApiResponse<T = any> {
|
export interface ApiResponse<T = any> {
|
||||||
code: number;
|
code: number;
|
||||||
msg: string;
|
msg: string;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建内容库参数
|
// 创建内容库参数
|
||||||
export interface CreateContentLibraryParams {
|
export interface CreateContentLibraryParams {
|
||||||
name: string;
|
name: string;
|
||||||
sourceType: number;
|
sourceType: number;
|
||||||
sourceFriends?: string[];
|
sourceFriends?: string[];
|
||||||
sourceGroups?: string[];
|
sourceGroups?: string[];
|
||||||
keywordInclude?: string[];
|
keywordInclude?: string[];
|
||||||
keywordExclude?: string[];
|
keywordExclude?: string[];
|
||||||
aiPrompt?: string;
|
aiPrompt?: string;
|
||||||
timeEnabled?: number;
|
timeEnabled?: number;
|
||||||
timeStart?: string;
|
timeStart?: string;
|
||||||
timeEnd?: string;
|
timeEnd?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新内容库参数
|
// 更新内容库参数
|
||||||
export interface UpdateContentLibraryParams
|
export interface UpdateContentLibraryParams
|
||||||
extends Partial<CreateContentLibraryParams> {
|
extends Partial<CreateContentLibraryParams> {
|
||||||
id: string;
|
id: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,138 +1,140 @@
|
|||||||
.form-page {
|
.form-page {
|
||||||
background: #f7f8fa;
|
background: #f7f8fa;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-main {
|
.form-main {
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 16px 0 0 0;
|
padding: 16px 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-section {
|
.form-section {
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-label {
|
.form-label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #222;
|
color: #222;
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #222;
|
color: #222;
|
||||||
margin-top: 28px;
|
margin-top: 28px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-block {
|
.section-block {
|
||||||
padding: 12px 0 8px 0;
|
padding: 12px 0 8px 0;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs-bar {
|
.tabs-bar {
|
||||||
.adm-tabs-header {
|
.adm-tabs-header {
|
||||||
background: #f7f8fa;
|
background: #f7f8fa;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.adm-tabs-tab {
|
.adm-tabs-tab {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapse {
|
.collapse {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
.adm-collapse-panel-content {
|
.adm-collapse-panel-content {
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
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;
|
||||||
}
|
}
|
||||||
.form-label {
|
.form-label {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
.adm-input {
|
.adm-input {
|
||||||
min-height: 42px;
|
min-height: 42px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-row, .section-block {
|
.ai-row,
|
||||||
display: flex;
|
.section-block {
|
||||||
align-items: center;
|
display: flex;
|
||||||
gap: 12px;
|
align-items: center;
|
||||||
}
|
gap: 12px;
|
||||||
|
}
|
||||||
.ai-desc {
|
|
||||||
color: #888;
|
.ai-desc {
|
||||||
font-size: 13px;
|
color: #888;
|
||||||
flex: 1;
|
font-size: 13px;
|
||||||
}
|
flex: 1;
|
||||||
|
}
|
||||||
.date-row, .section-block {
|
|
||||||
display: flex;
|
.date-row,
|
||||||
gap: 12px;
|
.section-block {
|
||||||
align-items: center;
|
display: flex;
|
||||||
}
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
.adm-input {
|
}
|
||||||
min-height: 44px;
|
|
||||||
font-size: 15px;
|
.adm-input {
|
||||||
border-radius: 8px;
|
min-height: 44px;
|
||||||
}
|
font-size: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
.submit-btn {
|
}
|
||||||
margin-top: 32px;
|
|
||||||
height: 48px !important;
|
.submit-btn {
|
||||||
border-radius: 10px !important;
|
margin-top: 32px;
|
||||||
font-size: 17px;
|
height: 48px !important;
|
||||||
font-weight: 600;
|
border-radius: 10px !important;
|
||||||
letter-spacing: 1px;
|
font-size: 17px;
|
||||||
}
|
font-weight: 600;
|
||||||
|
letter-spacing: 1px;
|
||||||
@media (max-width: 600px) {
|
}
|
||||||
.form-main {
|
|
||||||
max-width: 100vw;
|
@media (max-width: 600px) {
|
||||||
padding: 0;
|
.form-main {
|
||||||
}
|
max-width: 100vw;
|
||||||
.form-card {
|
padding: 0;
|
||||||
border-radius: 0;
|
}
|
||||||
box-shadow: none;
|
.form-card {
|
||||||
padding: 16px 6px 12px 6px;
|
border-radius: 0;
|
||||||
}
|
box-shadow: none;
|
||||||
.section-title {
|
padding: 16px 6px 12px 6px;
|
||||||
font-size: 15px;
|
}
|
||||||
margin-top: 22px;
|
.section-title {
|
||||||
margin-bottom: 8px;
|
font-size: 15px;
|
||||||
}
|
margin-top: 22px;
|
||||||
.submit-btn {
|
margin-bottom: 8px;
|
||||||
height: 44px !important;
|
}
|
||||||
font-size: 15px;
|
.submit-btn {
|
||||||
}
|
height: 44px !important;
|
||||||
}
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,66 +1,66 @@
|
|||||||
// 内容库接口类型定义
|
// 内容库接口类型定义
|
||||||
export interface ContentLibrary {
|
export interface ContentLibrary {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
sourceType: number; // 1=微信好友, 2=聊天群
|
sourceType: number; // 1=微信好友, 2=聊天群
|
||||||
creatorName?: string;
|
creatorName?: string;
|
||||||
updateTime: string;
|
updateTime: string;
|
||||||
status: number; // 0=未启用, 1=已启用
|
status: number; // 0=未启用, 1=已启用
|
||||||
itemCount?: number;
|
itemCount?: number;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
sourceFriends?: string[];
|
sourceFriends?: string[];
|
||||||
sourceGroups?: string[];
|
sourceGroups?: string[];
|
||||||
keywordInclude?: string[];
|
keywordInclude?: string[];
|
||||||
keywordExclude?: string[];
|
keywordExclude?: string[];
|
||||||
aiPrompt?: string;
|
aiPrompt?: string;
|
||||||
timeEnabled?: number;
|
timeEnabled?: number;
|
||||||
timeStart?: string;
|
timeStart?: string;
|
||||||
timeEnd?: string;
|
timeEnd?: string;
|
||||||
selectedFriends?: any[];
|
selectedFriends?: any[];
|
||||||
selectedGroups?: any[];
|
selectedGroups?: any[];
|
||||||
selectedGroupMembers?: WechatGroupMember[];
|
selectedGroupMembers?: WechatGroupMember[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 微信群成员
|
// 微信群成员
|
||||||
export interface WechatGroupMember {
|
export interface WechatGroupMember {
|
||||||
id: string;
|
id: string;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
wechatId: string;
|
wechatId: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
gender?: "male" | "female";
|
gender?: "male" | "female";
|
||||||
role?: "owner" | "admin" | "member";
|
role?: "owner" | "admin" | "member";
|
||||||
joinTime?: string;
|
joinTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API 响应类型
|
// API 响应类型
|
||||||
export interface ApiResponse<T = any> {
|
export interface ApiResponse<T = any> {
|
||||||
code: number;
|
code: number;
|
||||||
msg: string;
|
msg: string;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LibraryListResponse {
|
export interface LibraryListResponse {
|
||||||
list: ContentLibrary[];
|
list: ContentLibrary[];
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建内容库参数
|
// 创建内容库参数
|
||||||
export interface CreateContentLibraryParams {
|
export interface CreateContentLibraryParams {
|
||||||
name: string;
|
name: string;
|
||||||
sourceType: number;
|
sourceType: number;
|
||||||
sourceFriends?: string[];
|
sourceFriends?: string[];
|
||||||
sourceGroups?: string[];
|
sourceGroups?: string[];
|
||||||
keywordInclude?: string[];
|
keywordInclude?: string[];
|
||||||
keywordExclude?: string[];
|
keywordExclude?: string[];
|
||||||
aiPrompt?: string;
|
aiPrompt?: string;
|
||||||
timeEnabled?: number;
|
timeEnabled?: number;
|
||||||
timeStart?: string;
|
timeStart?: string;
|
||||||
timeEnd?: string;
|
timeEnd?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新内容库参数
|
// 更新内容库参数
|
||||||
export interface UpdateContentLibraryParams
|
export interface UpdateContentLibraryParams
|
||||||
extends Partial<CreateContentLibraryParams> {
|
extends Partial<CreateContentLibraryParams> {
|
||||||
id: string;
|
id: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,223 +1,217 @@
|
|||||||
.content-library-page {
|
.content-library-page {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
background: white;
|
background: white;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input-wrapper {
|
.search-input-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-icon {
|
.search-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
color: #999;
|
color: #999;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
padding-left: 36px;
|
padding-left: 36px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid #e0e0e0;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: #1677ff;
|
border-color: #1677ff;
|
||||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.create-btn {
|
||||||
|
border-radius: 20px;
|
||||||
.create-btn {
|
padding: 0 16px;
|
||||||
border-radius: 20px;
|
}
|
||||||
padding: 0 16px;
|
|
||||||
}
|
.spinning {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
.spinning {
|
}
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
@keyframes spin {
|
||||||
|
from {
|
||||||
@keyframes spin {
|
transform: rotate(0deg);
|
||||||
from {
|
}
|
||||||
transform: rotate(0deg);
|
to {
|
||||||
}
|
transform: rotate(360deg);
|
||||||
to {
|
}
|
||||||
transform: rotate(360deg);
|
}
|
||||||
}
|
|
||||||
}
|
.tabs {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.tabs {
|
.library-list {
|
||||||
flex:1;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.library-list {
|
.loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: center;
|
||||||
gap: 12px;
|
align-items: center;
|
||||||
}
|
padding: 40px 0;
|
||||||
|
}
|
||||||
.loading {
|
|
||||||
display: flex;
|
.empty-state {
|
||||||
justify-content: center;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
padding: 40px 0;
|
align-items: center;
|
||||||
}
|
justify-content: center;
|
||||||
|
padding: 60px 20px;
|
||||||
.empty-state {
|
text-align: center;
|
||||||
display: flex;
|
background: white;
|
||||||
flex-direction: column;
|
border-radius: 8px;
|
||||||
align-items: center;
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
justify-content: center;
|
}
|
||||||
padding: 60px 20px;
|
|
||||||
text-align: center;
|
.empty-icon {
|
||||||
background: white;
|
font-size: 48px;
|
||||||
border-radius: 8px;
|
margin-bottom: 16px;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-icon {
|
.empty-text {
|
||||||
font-size: 48px;
|
color: #999;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 20px;
|
||||||
opacity: 0.6;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-text {
|
.empty-btn {
|
||||||
color: #999;
|
border-radius: 20px;
|
||||||
margin-bottom: 20px;
|
padding: 0 20px;
|
||||||
font-size: 14px;
|
}
|
||||||
}
|
|
||||||
|
.library-card {
|
||||||
.empty-btn {
|
background: white;
|
||||||
border-radius: 20px;
|
border-radius: 8px;
|
||||||
padding: 0 20px;
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
}
|
border: none;
|
||||||
|
|
||||||
.library-card {
|
&:hover {
|
||||||
background: white;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
border-radius: 8px;
|
}
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
}
|
||||||
border: none;
|
|
||||||
|
.card-header {
|
||||||
&:hover {
|
display: flex;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
justify-content: space-between;
|
||||||
}
|
align-items: flex-start;
|
||||||
}
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
.card-header {
|
|
||||||
display: flex;
|
.library-info {
|
||||||
justify-content: space-between;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
margin-bottom: 12px;
|
gap: 8px;
|
||||||
}
|
flex: 1;
|
||||||
|
}
|
||||||
.library-info {
|
|
||||||
display: flex;
|
.library-name {
|
||||||
align-items: center;
|
font-size: 16px;
|
||||||
gap: 8px;
|
font-weight: 600;
|
||||||
flex: 1;
|
color: #333;
|
||||||
}
|
margin: 0;
|
||||||
|
}
|
||||||
.library-name {
|
|
||||||
font-size: 16px;
|
.status-tag {
|
||||||
font-weight: 600;
|
font-size: 12px;
|
||||||
color: #333;
|
padding: 2px 8px;
|
||||||
margin: 0;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-tag {
|
.menu-btn {
|
||||||
font-size: 12px;
|
background: none;
|
||||||
padding: 2px 8px;
|
border: none;
|
||||||
border-radius: 10px;
|
padding: 4px;
|
||||||
}
|
cursor: pointer;
|
||||||
|
color: #999;
|
||||||
.menu-btn {
|
border-radius: 4px;
|
||||||
background: none;
|
|
||||||
border: none;
|
&:hover {
|
||||||
padding: 4px;
|
background: #f5f5f5;
|
||||||
cursor: pointer;
|
color: #666;
|
||||||
color: #999;
|
}
|
||||||
border-radius: 4px;
|
}
|
||||||
|
|
||||||
&:hover {
|
.menu-dropdown {
|
||||||
background: #f5f5f5;
|
position: absolute;
|
||||||
color: #666;
|
right: 0;
|
||||||
}
|
top: 100%;
|
||||||
}
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
.menu-dropdown {
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
position: absolute;
|
z-index: 1000;
|
||||||
right: 0;
|
min-width: 120px;
|
||||||
top: 100%;
|
padding: 4px;
|
||||||
background: white;
|
margin-top: 4px;
|
||||||
border-radius: 8px;
|
}
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
z-index: 1000;
|
.menu-item {
|
||||||
min-width: 120px;
|
display: flex;
|
||||||
padding: 4px;
|
align-items: center;
|
||||||
margin-top: 4px;
|
gap: 8px;
|
||||||
}
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
.menu-item {
|
border-radius: 4px;
|
||||||
display: flex;
|
font-size: 14px;
|
||||||
align-items: center;
|
color: #333;
|
||||||
gap: 8px;
|
transition: background 0.2s;
|
||||||
padding: 8px 12px;
|
|
||||||
cursor: pointer;
|
&:hover {
|
||||||
border-radius: 4px;
|
background: #f5f5f5;
|
||||||
font-size: 14px;
|
}
|
||||||
color: #333;
|
|
||||||
transition: background 0.2s;
|
&.danger {
|
||||||
|
color: #ff4d4f;
|
||||||
&:hover {
|
|
||||||
background: #f5f5f5;
|
&:hover {
|
||||||
}
|
background: #fff2f0;
|
||||||
|
}
|
||||||
&.danger {
|
}
|
||||||
color: #ff4d4f;
|
}
|
||||||
|
|
||||||
&:hover {
|
.card-content {
|
||||||
background: #fff2f0;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
}
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-content {
|
.info-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
gap: 6px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-row {
|
.label {
|
||||||
display: flex;
|
color: #999;
|
||||||
align-items: center;
|
min-width: 70px;
|
||||||
font-size: 13px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.value {
|
||||||
color: #999;
|
color: #333;
|
||||||
min-width: 70px;
|
flex: 1;
|
||||||
margin-right: 8px;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
color: #333;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,317 +1,317 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Toast,
|
Toast,
|
||||||
SpinLoading,
|
SpinLoading,
|
||||||
Dialog,
|
Dialog,
|
||||||
Card,
|
Card,
|
||||||
Avatar,
|
Avatar,
|
||||||
Tag,
|
Tag,
|
||||||
} from "antd-mobile";
|
} from "antd-mobile";
|
||||||
import { Input } from "antd";
|
import { Input } from "antd";
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
MoreOutlined,
|
MoreOutlined,
|
||||||
} 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 { getContentLibraryList, deleteContentLibrary } from "./api";
|
import { getContentLibraryList, deleteContentLibrary } from "./api";
|
||||||
import { ContentLibrary } from "./data";
|
import { ContentLibrary } from "./data";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
import { Tabs } from "antd-mobile";
|
import { Tabs } from "antd-mobile";
|
||||||
|
|
||||||
// 卡片菜单组件
|
// 卡片菜单组件
|
||||||
interface CardMenuProps {
|
interface CardMenuProps {
|
||||||
onView: () => void;
|
onView: () => void;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onViewMaterials: () => void;
|
onViewMaterials: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CardMenu: React.FC<CardMenuProps> = ({
|
const CardMenu: React.FC<CardMenuProps> = ({
|
||||||
onView,
|
onView,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onViewMaterials,
|
onViewMaterials,
|
||||||
}) => {
|
}) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const menuRef = React.useRef<HTMLDivElement>(null);
|
const menuRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
function handleClickOutside(event: MouseEvent) {
|
function handleClickOutside(event: MouseEvent) {
|
||||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (open) document.addEventListener("mousedown", handleClickOutside);
|
if (open) document.addEventListener("mousedown", handleClickOutside);
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
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 && (
|
||||||
<div ref={menuRef} className={style["menu-dropdown"]}>
|
<div ref={menuRef} className={style["menu-dropdown"]}>
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onEdit();
|
onEdit();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
className={style["menu-item"]}
|
className={style["menu-item"]}
|
||||||
>
|
>
|
||||||
<EditOutlined />
|
<EditOutlined />
|
||||||
编辑
|
编辑
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onDelete();
|
onDelete();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
className={`${style["menu-item"]} ${style["danger"]}`}
|
className={`${style["menu-item"]} ${style["danger"]}`}
|
||||||
>
|
>
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
删除
|
删除
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onViewMaterials();
|
onViewMaterials();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
className={style["menu-item"]}
|
className={style["menu-item"]}
|
||||||
>
|
>
|
||||||
<EyeOutlined />
|
<EyeOutlined />
|
||||||
查看素材
|
查看素材
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ContentLibraryList: React.FC = () => {
|
const ContentLibraryList: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [libraries, setLibraries] = useState<ContentLibrary[]>([]);
|
const [libraries, setLibraries] = useState<ContentLibrary[]>([]);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [activeTab, setActiveTab] = useState("all");
|
const [activeTab, setActiveTab] = useState("all");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
// 获取内容库列表
|
// 获取内容库列表
|
||||||
const fetchLibraries = useCallback(async () => {
|
const fetchLibraries = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await getContentLibraryList({
|
const response = await getContentLibraryList({
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
keyword: searchQuery,
|
keyword: searchQuery,
|
||||||
sourceType:
|
sourceType:
|
||||||
activeTab !== "all" ? (activeTab === "friends" ? 1 : 2) : undefined,
|
activeTab !== "all" ? (activeTab === "friends" ? 1 : 2) : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
setLibraries(response.list || []);
|
setLibraries(response.list || []);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("获取内容库列表失败:", error);
|
console.error("获取内容库列表失败:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [searchQuery, activeTab]);
|
}, [searchQuery, activeTab]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchLibraries();
|
fetchLibraries();
|
||||||
}, [fetchLibraries]);
|
}, [fetchLibraries]);
|
||||||
|
|
||||||
const handleCreateNew = () => {
|
const handleCreateNew = () => {
|
||||||
navigate("/content/new");
|
navigate("/content/new");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (id: string) => {
|
const handleEdit = (id: string) => {
|
||||||
navigate(`/content/edit/${id}`);
|
navigate(`/content/edit/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
const result = await Dialog.confirm({
|
const result = await Dialog.confirm({
|
||||||
content: "确定要删除这个内容库吗?",
|
content: "确定要删除这个内容库吗?",
|
||||||
confirmText: "删除",
|
confirmText: "删除",
|
||||||
cancelText: "取消",
|
cancelText: "取消",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
try {
|
try {
|
||||||
const response = await deleteContentLibrary(id);
|
const response = await deleteContentLibrary(id);
|
||||||
if (response.code === 200) {
|
if (response.code === 200) {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
content: "删除成功",
|
content: "删除成功",
|
||||||
position: "top",
|
position: "top",
|
||||||
});
|
});
|
||||||
fetchLibraries();
|
fetchLibraries();
|
||||||
} else {
|
} else {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
content: response.msg || "删除失败",
|
content: response.msg || "删除失败",
|
||||||
position: "top",
|
position: "top",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("删除内容库失败:", error);
|
console.error("删除内容库失败:", error);
|
||||||
Toast.show({
|
Toast.show({
|
||||||
content: error?.message || "请检查网络连接",
|
content: error?.message || "请检查网络连接",
|
||||||
position: "top",
|
position: "top",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewMaterials = (id: string) => {
|
const handleViewMaterials = (id: string) => {
|
||||||
navigate(`/content/materials/${id}`);
|
navigate(`/content/materials/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
fetchLibraries();
|
fetchLibraries();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
fetchLibraries();
|
fetchLibraries();
|
||||||
};
|
};
|
||||||
|
|
||||||
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())
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
header={
|
header={
|
||||||
<>
|
<>
|
||||||
<NavCommon
|
<NavCommon
|
||||||
title="内容库"
|
title="内容库"
|
||||||
right={
|
right={
|
||||||
<Button size="small" color="primary" onClick={handleCreateNew}>
|
<Button size="small" color="primary" onClick={handleCreateNew}>
|
||||||
<PlusOutlined /> 新建内容库
|
<PlusOutlined /> 新建内容库
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 搜索栏 */}
|
{/* 搜索栏 */}
|
||||||
<div className="search-bar">
|
<div className="search-bar">
|
||||||
<div className="search-input-wrapper">
|
<div className="search-input-wrapper">
|
||||||
<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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
className="refresh-btn"
|
className="refresh-btn"
|
||||||
>
|
>
|
||||||
<ReloadOutlined />
|
<ReloadOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 标签页 */}
|
{/* 标签页 */}
|
||||||
<div className={style["tabs"]}>
|
<div className={style["tabs"]}>
|
||||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||||
<Tabs.Tab title="全部" key="all" />
|
<Tabs.Tab title="全部" key="all" />
|
||||||
<Tabs.Tab title="微信好友" key="friends" />
|
<Tabs.Tab title="微信好友" key="friends" />
|
||||||
<Tabs.Tab title="聊天群" key="groups" />
|
<Tabs.Tab title="聊天群" key="groups" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={style["content-library-page"]}>
|
<div className={style["content-library-page"]}>
|
||||||
{/* 内容库列表 */}
|
{/* 内容库列表 */}
|
||||||
<div className={style["library-list"]}>
|
<div className={style["library-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>
|
||||||
) : filteredLibraries.length === 0 ? (
|
) : filteredLibraries.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"]}>
|
||||||
暂无内容库,快去新建一个吧!
|
暂无内容库,快去新建一个吧!
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={handleCreateNew}
|
onClick={handleCreateNew}
|
||||||
className={style["empty-btn"]}
|
className={style["empty-btn"]}
|
||||||
>
|
>
|
||||||
新建内容库
|
新建内容库
|
||||||
</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"]}>
|
||||||
<h3 className={style["library-name"]}>{library.name}</h3>
|
<h3 className={style["library-name"]}>{library.name}</h3>
|
||||||
<Tag
|
<Tag
|
||||||
color={library.status === 1 ? "success" : "default"}
|
color={library.status === 1 ? "success" : "default"}
|
||||||
className={style["status-tag"]}
|
className={style["status-tag"]}
|
||||||
>
|
>
|
||||||
{library.status === 1 ? "已启用" : "未启用"}
|
{library.status === 1 ? "已启用" : "未启用"}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
<CardMenu
|
<CardMenu
|
||||||
onView={() => navigate(`/content/${library.id}`)}
|
onView={() => navigate(`/content/${library.id}`)}
|
||||||
onEdit={() => handleEdit(library.id)}
|
onEdit={() => handleEdit(library.id)}
|
||||||
onDelete={() => handleDelete(library.id)}
|
onDelete={() => handleDelete(library.id)}
|
||||||
onViewMaterials={() => handleViewMaterials(library.id)}
|
onViewMaterials={() => handleViewMaterials(library.id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["card-content"]}>
|
<div className={style["card-content"]}>
|
||||||
<div className={style["info-row"]}>
|
<div className={style["info-row"]}>
|
||||||
<span className={style["label"]}>来源:</span>
|
<span className={style["label"]}>来源:</span>
|
||||||
<span className={style["value"]}>
|
<span className={style["value"]}>
|
||||||
{library.sourceType === 1 ? "微信好友" : "聊天群"}
|
{library.sourceType === 1 ? "微信好友" : "聊天群"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["info-row"]}>
|
<div className={style["info-row"]}>
|
||||||
<span className={style["label"]}>创建人:</span>
|
<span className={style["label"]}>创建人:</span>
|
||||||
<span className={style["value"]}>
|
<span className={style["value"]}>
|
||||||
{library.creatorName || "系统"}
|
{library.creatorName || "系统"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["info-row"]}>
|
<div className={style["info-row"]}>
|
||||||
<span className={style["label"]}>内容数量:</span>
|
<span className={style["label"]}>内容数量:</span>
|
||||||
<span className={style["value"]}>
|
<span className={style["value"]}>
|
||||||
{library.itemCount || 0}
|
{library.itemCount || 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["info-row"]}>
|
<div className={style["info-row"]}>
|
||||||
<span className={style["label"]}>更新时间:</span>
|
<span className={style["label"]}>更新时间:</span>
|
||||||
<span className={style["value"]}>
|
<span className={style["value"]}>
|
||||||
{new Date(library.updateTime).toLocaleString("zh-CN", {
|
{new Date(library.updateTime).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",
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ContentLibraryList;
|
export default ContentLibraryList;
|
||||||
|
|||||||
@@ -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,85 +1,93 @@
|
|||||||
// 素材数据类型定义
|
// 素材数据类型定义
|
||||||
export interface ContentItem {
|
export interface ContentItem {
|
||||||
id: string;
|
id: number; // 修改为number类型
|
||||||
libraryId: string;
|
libraryId: number; // 修改为number类型
|
||||||
title: string;
|
type?: string;
|
||||||
content: string;
|
contentType: number; // 0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文
|
||||||
contentType: number; // 0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文
|
title: string;
|
||||||
contentTypeName?: string;
|
content: string;
|
||||||
resUrls?: string[];
|
contentAi?: string;
|
||||||
urls?: string[];
|
contentData?: any;
|
||||||
comment?: string;
|
snsId?: string | null;
|
||||||
sendTime?: string;
|
msgId?: string | null;
|
||||||
createTime: string;
|
wechatId?: string | null;
|
||||||
updateTime: string;
|
friendId?: string | null;
|
||||||
wechatId?: string;
|
createMomentTime?: number;
|
||||||
wechatNickname?: string;
|
createTime: string;
|
||||||
wechatAvatar?: string;
|
updateTime: string;
|
||||||
snsId?: string;
|
coverImage?: string;
|
||||||
msgId?: string;
|
resUrls?: string[];
|
||||||
type?: string;
|
urls?: any[];
|
||||||
contentData?: string;
|
location?: string | null;
|
||||||
createMomentTime?: string;
|
lat?: string;
|
||||||
createMessageTime?: string;
|
lng?: string;
|
||||||
createMomentTimeFormatted?: string;
|
status?: number;
|
||||||
createMessageTimeFormatted?: string;
|
isDel?: number;
|
||||||
}
|
delTime?: number;
|
||||||
|
wechatChatroomId?: string | null;
|
||||||
// 内容库类型
|
senderNickname?: string;
|
||||||
export interface ContentLibrary {
|
createMessageTime?: string | null;
|
||||||
id: string;
|
comment?: string;
|
||||||
name: string;
|
sendTime?: string; // 字符串格式的时间
|
||||||
sourceType: number; // 1=微信好友, 2=聊天群
|
sendTimes?: number;
|
||||||
creatorName?: string;
|
contentTypeName?: string;
|
||||||
updateTime: string;
|
}
|
||||||
status: number; // 0=未启用, 1=已启用
|
|
||||||
itemCount?: number;
|
// 内容库类型
|
||||||
createTime: string;
|
export interface ContentLibrary {
|
||||||
sourceFriends?: string[];
|
id: string;
|
||||||
sourceGroups?: string[];
|
name: string;
|
||||||
keywordInclude?: string[];
|
sourceType: number; // 1=微信好友, 2=聊天群
|
||||||
keywordExclude?: string[];
|
creatorName?: string;
|
||||||
aiPrompt?: string;
|
updateTime: string;
|
||||||
timeEnabled?: number;
|
status: number; // 0=未启用, 1=已启用
|
||||||
timeStart?: string;
|
itemCount?: number;
|
||||||
timeEnd?: string;
|
createTime: string;
|
||||||
selectedFriends?: any[];
|
sourceFriends?: string[];
|
||||||
selectedGroups?: any[];
|
sourceGroups?: string[];
|
||||||
selectedGroupMembers?: WechatGroupMember[];
|
keywordInclude?: string[];
|
||||||
}
|
keywordExclude?: string[];
|
||||||
|
aiPrompt?: string;
|
||||||
// 微信群成员
|
timeEnabled?: number;
|
||||||
export interface WechatGroupMember {
|
timeStart?: string;
|
||||||
id: string;
|
timeEnd?: string;
|
||||||
nickname: string;
|
selectedFriends?: any[];
|
||||||
wechatId: string;
|
selectedGroups?: any[];
|
||||||
avatar: string;
|
selectedGroupMembers?: WechatGroupMember[];
|
||||||
gender?: "male" | "female";
|
}
|
||||||
role?: "owner" | "admin" | "member";
|
|
||||||
joinTime?: string;
|
// 微信群成员
|
||||||
}
|
export interface WechatGroupMember {
|
||||||
|
id: string;
|
||||||
// API 响应类型
|
nickname: string;
|
||||||
export interface ApiResponse<T = any> {
|
wechatId: string;
|
||||||
code: number;
|
avatar: string;
|
||||||
msg: string;
|
gender?: "male" | "female";
|
||||||
data: T;
|
role?: "owner" | "admin" | "member";
|
||||||
}
|
joinTime?: string;
|
||||||
|
}
|
||||||
// 创建素材参数
|
|
||||||
export interface CreateContentItemParams {
|
// API 响应类型
|
||||||
libraryId: string;
|
export interface ApiResponse<T = any> {
|
||||||
title: string;
|
code: number;
|
||||||
content: string;
|
msg: string;
|
||||||
contentType: number;
|
data: T;
|
||||||
resUrls?: string[];
|
}
|
||||||
urls?: string[];
|
|
||||||
comment?: string;
|
// 创建素材参数
|
||||||
sendTime?: string;
|
export interface CreateContentItemParams {
|
||||||
}
|
libraryId: string;
|
||||||
|
title: string;
|
||||||
// 更新素材参数
|
content: string;
|
||||||
export interface UpdateContentItemParams
|
contentType: number;
|
||||||
extends Partial<CreateContentItemParams> {
|
resUrls?: string[];
|
||||||
id: string;
|
urls?: string[];
|
||||||
}
|
comment?: string;
|
||||||
|
sendTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新素材参数
|
||||||
|
export interface UpdateContentItemParams
|
||||||
|
extends Partial<CreateContentItemParams> {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,125 +1,160 @@
|
|||||||
.form-page {
|
.form-page {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 40px 0;
|
padding: 40px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-card {
|
.form-card {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
border: none;
|
border: none;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea {
|
.form-item {
|
||||||
border-radius: 6px;
|
margin-bottom: 16px;
|
||||||
border: 1px solid #d9d9d9;
|
}
|
||||||
|
|
||||||
&:focus {
|
.form-label {
|
||||||
border-color: #1677ff;
|
display: block;
|
||||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
font-size: 14px;
|
||||||
}
|
font-weight: 500;
|
||||||
}
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
.time-picker {
|
|
||||||
width: 100%;
|
.required {
|
||||||
border-radius: 6px;
|
color: #ff4d4f;
|
||||||
|
margin-right: 4px;
|
||||||
&:focus {
|
}
|
||||||
border-color: #1677ff;
|
}
|
||||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
|
||||||
}
|
.form-input {
|
||||||
}
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
.select-option {
|
border-radius: 6px;
|
||||||
display: flex;
|
border: 1px solid #d9d9d9;
|
||||||
align-items: center;
|
padding: 0 12px;
|
||||||
gap: 8px;
|
font-size: 14px;
|
||||||
|
|
||||||
.anticon {
|
&:focus {
|
||||||
font-size: 16px;
|
border-color: #1677ff;
|
||||||
color: #1677ff;
|
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-actions {
|
.form-select {
|
||||||
display: flex;
|
width: 100%;
|
||||||
gap: 12px;
|
border-radius: 6px;
|
||||||
padding: 16px;
|
|
||||||
background: white;
|
&:focus {
|
||||||
border-radius: 8px;
|
border-color: #1677ff;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
||||||
margin-top: 16px;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-btn {
|
.form-textarea {
|
||||||
flex: 1;
|
width: 100%;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid #d9d9d9;
|
||||||
|
padding: 8px 12px;
|
||||||
&:hover {
|
font-size: 14px;
|
||||||
border-color: #1677ff;
|
resize: vertical;
|
||||||
color: #1677ff;
|
|
||||||
}
|
&:focus {
|
||||||
}
|
border-color: #1677ff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
||||||
.submit-btn {
|
}
|
||||||
flex: 1;
|
}
|
||||||
border-radius: 6px;
|
|
||||||
}
|
.select-option {
|
||||||
|
display: flex;
|
||||||
// 覆盖 antd-mobile 的默认样式
|
align-items: center;
|
||||||
:global {
|
gap: 8px;
|
||||||
.adm-form-item {
|
|
||||||
margin-bottom: 16px;
|
.anticon {
|
||||||
}
|
font-size: 16px;
|
||||||
|
color: #1677ff;
|
||||||
.adm-form-item-label {
|
}
|
||||||
font-size: 14px;
|
}
|
||||||
color: #333;
|
|
||||||
font-weight: 500;
|
.form-actions {
|
||||||
}
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
.adm-input {
|
padding: 16px;
|
||||||
border-radius: 6px;
|
background: white;
|
||||||
border: 1px solid #d9d9d9;
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
&:focus {
|
margin-top: 16px;
|
||||||
border-color: #1677ff;
|
}
|
||||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
|
||||||
}
|
.back-btn {
|
||||||
}
|
flex: 1;
|
||||||
|
border-radius: 6px;
|
||||||
.adm-select {
|
border: 1px solid #d9d9d9;
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid #d9d9d9;
|
&:hover {
|
||||||
|
border-color: #1677ff;
|
||||||
&:focus {
|
color: #1677ff;
|
||||||
border-color: #1677ff;
|
}
|
||||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
}
|
||||||
}
|
|
||||||
}
|
.submit-btn {
|
||||||
}
|
flex: 1;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆盖 antd-mobile 的默认样式
|
||||||
|
:global {
|
||||||
|
.adm-form-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adm-form-item-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adm-input {
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.adm-select {
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,318 +1,403 @@
|
|||||||
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,
|
FileTextOutlined,
|
||||||
FileTextOutlined,
|
AppstoreOutlined,
|
||||||
AppstoreOutlined,
|
} 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 {
|
import VideoUpload from "@/components/Upload/VideoUpload";
|
||||||
getContentItemDetail,
|
import {
|
||||||
createContentItem,
|
getContentItemDetail,
|
||||||
updateContentItem,
|
createContentItem,
|
||||||
getContentLibraryDetail,
|
updateContentItem,
|
||||||
} 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;
|
const { TextArea } = Input;
|
||||||
const { TextArea } = Input;
|
|
||||||
|
// 内容类型选项
|
||||||
// 内容类型选项
|
const contentTypeOptions = [
|
||||||
const contentTypeOptions = [
|
{ value: 1, label: "图片", icon: <PictureOutlined /> },
|
||||||
{ value: 1, label: "图片", icon: <PictureOutlined /> },
|
{ value: 2, label: "链接", icon: <LinkOutlined /> },
|
||||||
{ value: 2, label: "链接", icon: <LinkOutlined /> },
|
{ 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 navigate = useNavigate();
|
||||||
const MaterialForm: React.FC = () => {
|
const { id: libraryId, materialId } = useParams<{
|
||||||
const navigate = useNavigate();
|
id: string;
|
||||||
const { id: libraryId, materialId } = useParams<{
|
materialId: string;
|
||||||
id: string;
|
}>();
|
||||||
materialId: string;
|
const [loading, setLoading] = useState(false);
|
||||||
}>();
|
const [saving, setSaving] = useState(false);
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [loading, setLoading] = useState(false);
|
// 表单状态
|
||||||
const [saving, setSaving] = useState(false);
|
const [contentType, setContentType] = useState<number>(4);
|
||||||
const [material, setMaterial] = useState<ContentItem | null>(null);
|
const [title, setTitle] = useState("");
|
||||||
const [library, setLibrary] = useState<ContentLibrary | null>(null);
|
const [content, setContent] = useState("");
|
||||||
const [contentType, setContentType] = useState<number>(4);
|
const [comment, setComment] = useState("");
|
||||||
|
const [sendTime, setSendTime] = useState("");
|
||||||
const isEdit = !!materialId;
|
const [resUrls, setResUrls] = useState<string[]>([]);
|
||||||
|
|
||||||
// 获取内容库详情
|
// 链接相关状态
|
||||||
useEffect(() => {
|
const [linkDesc, setLinkDesc] = useState("");
|
||||||
if (libraryId) {
|
const [linkImage, setLinkImage] = useState("");
|
||||||
fetchLibraryDetail();
|
const [linkUrl, setLinkUrl] = useState("");
|
||||||
}
|
|
||||||
}, [libraryId]);
|
// 小程序相关状态
|
||||||
|
const [appTitle, setAppTitle] = useState("");
|
||||||
// 获取素材详情
|
const [appId, setAppId] = useState("");
|
||||||
useEffect(() => {
|
|
||||||
if (isEdit && materialId) {
|
const isEdit = !!materialId;
|
||||||
fetchMaterialDetail();
|
|
||||||
}
|
// 获取素材详情
|
||||||
}, [isEdit, materialId]);
|
const fetchMaterialDetail = useCallback(async () => {
|
||||||
|
if (!materialId) return;
|
||||||
const fetchLibraryDetail = async () => {
|
setLoading(true);
|
||||||
if (!libraryId) return;
|
try {
|
||||||
try {
|
const response = await getContentItemDetail(materialId);
|
||||||
const response = await getContentLibraryDetail(libraryId);
|
// 填充表单数据
|
||||||
if (response.code === 200 && response.data) {
|
setTitle(response.title || "");
|
||||||
setLibrary(response.data);
|
setContent(response.content || "");
|
||||||
}
|
setContentType(response.contentType || 4);
|
||||||
} catch (error) {
|
setComment(response.comment || "");
|
||||||
console.error("获取内容库详情失败:", error);
|
|
||||||
}
|
// 处理时间格式 - sendTime是字符串格式,需要转换为datetime-local格式
|
||||||
};
|
if (response.sendTime) {
|
||||||
|
// 将 "2025-07-28 16:11:00" 转换为 "2025-07-28T16:11"
|
||||||
const fetchMaterialDetail = async () => {
|
const dateTime = new Date(response.sendTime);
|
||||||
if (!materialId) return;
|
setSendTime(dateTime.toISOString().slice(0, 16));
|
||||||
setLoading(true);
|
} else {
|
||||||
try {
|
setSendTime("");
|
||||||
const response = await getContentItemDetail(materialId);
|
}
|
||||||
if (response.code === 200 && response.data) {
|
|
||||||
setMaterial(response.data);
|
setResUrls(response.resUrls || []);
|
||||||
setContentType(response.data.contentType);
|
|
||||||
|
// 设置链接相关数据
|
||||||
// 填充表单数据
|
if (response.urls && response.urls.length > 0) {
|
||||||
form.setFieldsValue({
|
const firstUrl = response.urls[0];
|
||||||
title: response.data.title,
|
if (typeof firstUrl === "object" && firstUrl !== null) {
|
||||||
content: response.data.content,
|
setLinkDesc(firstUrl.desc || "");
|
||||||
contentType: response.data.contentType,
|
setLinkImage(firstUrl.image || "");
|
||||||
comment: response.data.comment || "",
|
setLinkUrl(firstUrl.url || "");
|
||||||
sendTime: response.data.sendTime || "",
|
}
|
||||||
resUrls: response.data.resUrls || [],
|
}
|
||||||
urls: response.data.urls || [],
|
} catch (error: unknown) {
|
||||||
});
|
console.error("获取素材详情失败:", error);
|
||||||
} else {
|
} finally {
|
||||||
Toast.show({
|
setLoading(false);
|
||||||
content: response.msg || "获取素材详情失败",
|
}
|
||||||
position: "top",
|
}, [materialId]);
|
||||||
});
|
|
||||||
}
|
useEffect(() => {
|
||||||
} catch (error: any) {
|
if (isEdit && materialId) {
|
||||||
console.error("获取素材详情失败:", error);
|
fetchMaterialDetail();
|
||||||
Toast.show({
|
}
|
||||||
content: error?.message || "请检查网络连接",
|
}, [isEdit, materialId, fetchMaterialDetail]);
|
||||||
position: "top",
|
|
||||||
});
|
const handleSubmit = async () => {
|
||||||
} finally {
|
if (!libraryId) return;
|
||||||
setLoading(false);
|
|
||||||
}
|
if (!content.trim()) {
|
||||||
};
|
Toast.show({
|
||||||
|
content: "请输入素材内容",
|
||||||
const handleSubmit = async (values: any) => {
|
position: "top",
|
||||||
if (!libraryId) return;
|
});
|
||||||
|
return;
|
||||||
setSaving(true);
|
}
|
||||||
try {
|
|
||||||
const params = {
|
setSaving(true);
|
||||||
libraryId,
|
try {
|
||||||
title: values.title,
|
// 构建urls数据
|
||||||
content: values.content,
|
let finalUrls: { desc: string; image: string; url: string }[] = [];
|
||||||
contentType: values.contentType,
|
if (contentType === 2 && linkUrl) {
|
||||||
comment: values.comment || "",
|
finalUrls = [
|
||||||
sendTime: values.sendTime || "",
|
{
|
||||||
resUrls: values.resUrls || [],
|
desc: linkDesc,
|
||||||
urls: values.urls || [],
|
image: linkImage,
|
||||||
};
|
url: linkUrl,
|
||||||
|
},
|
||||||
let response;
|
];
|
||||||
if (isEdit) {
|
}
|
||||||
response = await updateContentItem({
|
|
||||||
id: materialId!,
|
const params = {
|
||||||
...params,
|
libraryId,
|
||||||
});
|
title,
|
||||||
} else {
|
content,
|
||||||
response = await createContentItem(params);
|
contentType,
|
||||||
}
|
comment,
|
||||||
|
sendTime: sendTime || "",
|
||||||
if (response.code === 200) {
|
resUrls,
|
||||||
Toast.show({
|
urls: finalUrls,
|
||||||
content: isEdit ? "更新成功" : "创建成功",
|
type: contentType,
|
||||||
position: "top",
|
};
|
||||||
});
|
|
||||||
navigate(`/content/materials/${libraryId}`);
|
if (isEdit) {
|
||||||
} else {
|
await updateContentItem({
|
||||||
Toast.show({
|
id: materialId!,
|
||||||
content: response.msg || (isEdit ? "更新失败" : "创建失败"),
|
...params,
|
||||||
position: "top",
|
});
|
||||||
});
|
} else {
|
||||||
}
|
await createContentItem(params);
|
||||||
} catch (error: any) {
|
}
|
||||||
console.error("保存素材失败:", error);
|
|
||||||
Toast.show({
|
// 直接使用返回数据,无需判断code
|
||||||
content: error?.message || "请检查网络连接",
|
Toast.show({
|
||||||
position: "top",
|
content: isEdit ? "更新成功" : "创建成功",
|
||||||
});
|
position: "top",
|
||||||
} finally {
|
});
|
||||||
setSaving(false);
|
navigate(`/content/materials/${libraryId}`);
|
||||||
}
|
} catch (error: unknown) {
|
||||||
};
|
console.error("保存素材失败:", error);
|
||||||
|
Toast.show({
|
||||||
const handleBack = () => {
|
content: error instanceof Error ? error.message : "请检查网络连接",
|
||||||
navigate(`/content/materials/${libraryId}`);
|
position: "top",
|
||||||
};
|
});
|
||||||
|
} finally {
|
||||||
if (loading) {
|
setSaving(false);
|
||||||
return (
|
}
|
||||||
<Layout header={<NavCommon title={isEdit ? "编辑素材" : "新建素材"} />}>
|
};
|
||||||
<div className={style["loading"]}>
|
|
||||||
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
const handleBack = () => {
|
||||||
</div>
|
navigate(`/content/materials/${libraryId}`);
|
||||||
</Layout>
|
};
|
||||||
);
|
|
||||||
}
|
if (loading) {
|
||||||
|
return (
|
||||||
return (
|
<Layout header={<NavCommon title={isEdit ? "编辑素材" : "新建素材"} />}>
|
||||||
<Layout header={<NavCommon title={isEdit ? "编辑素材" : "新建素材"} />}>
|
<div className={style["loading"]}>
|
||||||
<div className={style["form-page"]}>
|
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||||
<Form
|
</div>
|
||||||
form={form}
|
</Layout>
|
||||||
layout="vertical"
|
);
|
||||||
onFinish={handleSubmit}
|
}
|
||||||
className={style["form"]}
|
|
||||||
initialValues={{
|
return (
|
||||||
contentType: 4,
|
<Layout
|
||||||
resUrls: [],
|
header={<NavCommon title={isEdit ? "编辑素材" : "新建素材"} />}
|
||||||
urls: [],
|
footer={
|
||||||
}}
|
<div className={style["form-actions"]}>
|
||||||
>
|
<Button
|
||||||
{/* 基本信息 */}
|
fill="outline"
|
||||||
<Card className={style["form-card"]}>
|
onClick={handleBack}
|
||||||
<div className={style["card-title"]}>基本信息</div>
|
className={style["back-btn"]}
|
||||||
|
>
|
||||||
<Form.Item
|
<ArrowLeftOutlined />
|
||||||
name="title"
|
返回
|
||||||
label="素材标题"
|
</Button>
|
||||||
rules={[{ required: true, message: "请输入素材标题" }]}
|
<Button
|
||||||
>
|
color="primary"
|
||||||
<Input placeholder="请输入素材标题" />
|
onClick={handleSubmit}
|
||||||
</Form.Item>
|
loading={saving}
|
||||||
|
className={style["submit-btn"]}
|
||||||
<Form.Item
|
>
|
||||||
name="contentType"
|
<SaveOutlined />
|
||||||
label="内容类型"
|
{isEdit ? " 保存修改" : " 保存素材"}
|
||||||
rules={[{ required: true, message: "请选择内容类型" }]}
|
</Button>
|
||||||
>
|
</div>
|
||||||
<Select
|
}
|
||||||
placeholder="请选择内容类型"
|
>
|
||||||
onChange={(value) => setContentType(value)}
|
<div className={style["form-page"]}>
|
||||||
>
|
<div className={style["form"]}>
|
||||||
{contentTypeOptions.map((option) => (
|
{/* 基础信息 */}
|
||||||
<Option key={option.value} value={option.value}>
|
<Card className={style["form-card"]}>
|
||||||
<div className={style["select-option"]}>
|
<div className={style["card-title"]}>基础信息</div>
|
||||||
{option.icon}
|
|
||||||
<span>{option.label}</span>
|
<div className={style["form-item"]}>
|
||||||
</div>
|
<label className={style["form-label"]}>发布时间</label>
|
||||||
</Option>
|
<Input
|
||||||
))}
|
type="datetime-local"
|
||||||
</Select>
|
value={sendTime}
|
||||||
</Form.Item>
|
onChange={e => setSendTime(e.target.value)}
|
||||||
|
placeholder="请选择发布时间"
|
||||||
<Form.Item
|
className={style["form-input"]}
|
||||||
name="content"
|
/>
|
||||||
label="内容"
|
</div>
|
||||||
rules={[{ required: true, message: "请输入内容" }]}
|
|
||||||
>
|
<div className={style["form-item"]}>
|
||||||
<TextArea
|
<label className={style["form-label"]}>
|
||||||
placeholder="请输入内容"
|
<span className={style["required"]}>*</span>类型
|
||||||
rows={6}
|
</label>
|
||||||
className={style["textarea"]}
|
<Select
|
||||||
/>
|
value={contentType}
|
||||||
</Form.Item>
|
onChange={value => setContentType(value)}
|
||||||
</Card>
|
placeholder="请选择类型"
|
||||||
|
className={style["form-select"]}
|
||||||
{/* 资源设置 */}
|
>
|
||||||
<Card className={style["form-card"]}>
|
{contentTypeOptions.map(option => (
|
||||||
<div className={style["card-title"]}>资源设置</div>
|
<Option key={option.value} value={option.value}>
|
||||||
|
<div className={style["select-option"]}>
|
||||||
<Form.Item
|
{option.icon}
|
||||||
name="resUrls"
|
<span>{option.label}</span>
|
||||||
label="资源链接"
|
</div>
|
||||||
extra="图片、视频等资源链接,多个用换行分隔"
|
</Option>
|
||||||
>
|
))}
|
||||||
<TextArea
|
</Select>
|
||||||
placeholder="请输入资源链接,多个用换行分隔"
|
</div>
|
||||||
rows={4}
|
</Card>
|
||||||
className={style["textarea"]}
|
|
||||||
/>
|
{/* 内容信息 */}
|
||||||
</Form.Item>
|
<Card className={style["form-card"]}>
|
||||||
|
<div className={style["card-title"]}>内容信息</div>
|
||||||
<Form.Item
|
|
||||||
name="urls"
|
<div className={style["form-item"]}>
|
||||||
label="外部链接"
|
<label className={style["form-label"]}>
|
||||||
extra="外部网页链接,多个用换行分隔"
|
<span className={style["required"]}>*</span>内容
|
||||||
>
|
</label>
|
||||||
<TextArea
|
<TextArea
|
||||||
placeholder="请输入外部链接,多个用换行分隔"
|
value={content}
|
||||||
rows={4}
|
onChange={e => setContent(e.target.value)}
|
||||||
className={style["textarea"]}
|
placeholder="请输入内容"
|
||||||
/>
|
rows={6}
|
||||||
</Form.Item>
|
className={style["form-textarea"]}
|
||||||
</Card>
|
/>
|
||||||
|
</div>
|
||||||
{/* 其他设置 */}
|
|
||||||
<Card className={style["form-card"]}>
|
{/* 链接类型特有字段 */}
|
||||||
<div className={style["card-title"]}>其他设置</div>
|
{contentType === 2 && (
|
||||||
|
<>
|
||||||
<Form.Item name="comment" label="备注" extra="素材备注信息">
|
<div className={style["form-item"]}>
|
||||||
<TextArea
|
<label className={style["form-label"]}>
|
||||||
placeholder="请输入备注信息"
|
<span className={style["required"]}>*</span>描述
|
||||||
rows={3}
|
</label>
|
||||||
className={style["textarea"]}
|
<Input
|
||||||
/>
|
value={linkDesc}
|
||||||
</Form.Item>
|
onChange={e => setLinkDesc(e.target.value)}
|
||||||
|
placeholder="请输入描述"
|
||||||
<Form.Item
|
className={style["form-input"]}
|
||||||
name="sendTime"
|
/>
|
||||||
label="发送时间"
|
</div>
|
||||||
extra="计划发送时间(可选)"
|
|
||||||
>
|
<div className={style["form-item"]}>
|
||||||
<TimePicker
|
<label className={style["form-label"]}>封面图</label>
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
<UploadComponent
|
||||||
placeholder="选择发送时间"
|
value={linkImage ? [linkImage] : []}
|
||||||
className={style["time-picker"]}
|
onChange={urls => setLinkImage(urls[0] || "")}
|
||||||
/>
|
count={1}
|
||||||
</Form.Item>
|
/>
|
||||||
</Card>
|
</div>
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
<div className={style["form-item"]}>
|
||||||
<div className={style["form-actions"]}>
|
<label className={style["form-label"]}>
|
||||||
<Button
|
<span className={style["required"]}>*</span>链接地址
|
||||||
fill="outline"
|
</label>
|
||||||
onClick={handleBack}
|
<Input
|
||||||
className={style["back-btn"]}
|
value={linkUrl}
|
||||||
>
|
onChange={e => setLinkUrl(e.target.value)}
|
||||||
<ArrowLeftOutlined />
|
placeholder="请输入链接地址"
|
||||||
返回
|
className={style["form-input"]}
|
||||||
</Button>
|
/>
|
||||||
<Button
|
</div>
|
||||||
color="primary"
|
</>
|
||||||
type="submit"
|
)}
|
||||||
loading={saving}
|
|
||||||
className={style["submit-btn"]}
|
{/* 视频类型特有字段 */}
|
||||||
>
|
{contentType === 3 && (
|
||||||
<SaveOutlined />
|
<div className={style["form-item"]}>
|
||||||
{isEdit ? "更新" : "创建"}
|
<label className={style["form-label"]}>视频上传</label>
|
||||||
</Button>
|
<VideoUpload
|
||||||
</div>
|
value={resUrls[0] || ""}
|
||||||
</Form>
|
onChange={url => setResUrls([url])}
|
||||||
</div>
|
/>
|
||||||
</Layout>
|
</div>
|
||||||
);
|
)}
|
||||||
};
|
</Card>
|
||||||
|
|
||||||
export default MaterialForm;
|
{/* 素材上传(仅图片类型和小程序类型) */}
|
||||||
|
{[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"]}>
|
||||||
|
<div className={style["card-title"]}>评论/备注</div>
|
||||||
|
|
||||||
|
<div className={style["form-item"]}>
|
||||||
|
<label className={style["form-label"]}>备注</label>
|
||||||
|
<TextArea
|
||||||
|
value={comment}
|
||||||
|
onChange={e => setComment(e.target.value)}
|
||||||
|
placeholder="请输入评论或备注"
|
||||||
|
rows={4}
|
||||||
|
className={style["form-textarea"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MaterialForm;
|
||||||
|
|||||||
@@ -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,98 +1,106 @@
|
|||||||
// 素材数据类型定义
|
// 素材数据类型定义
|
||||||
export interface ContentItem {
|
export interface ContentItem {
|
||||||
id: string;
|
id: number;
|
||||||
libraryId: string;
|
libraryId: number;
|
||||||
title: string;
|
type: string;
|
||||||
content: string;
|
contentType: number; // 0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文
|
||||||
contentType: number; // 0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文
|
title: string;
|
||||||
contentTypeName?: string;
|
content: string;
|
||||||
resUrls?: string[];
|
contentAi?: string | null;
|
||||||
urls?: string[];
|
contentData?: string | null;
|
||||||
comment?: string;
|
snsId?: string | null;
|
||||||
sendTime?: string;
|
msgId?: string | null;
|
||||||
createTime: string;
|
wechatId?: string | null;
|
||||||
updateTime: string;
|
friendId?: string | null;
|
||||||
wechatId?: string;
|
createMomentTime: number;
|
||||||
wechatNickname?: string;
|
createTime: string;
|
||||||
wechatAvatar?: string;
|
updateTime: string;
|
||||||
snsId?: string;
|
coverImage: string;
|
||||||
msgId?: string;
|
resUrls: string[];
|
||||||
type?: string;
|
urls: { desc: string; image: string; url: string }[];
|
||||||
contentData?: string;
|
location?: string | null;
|
||||||
createMomentTime?: string;
|
lat: string;
|
||||||
createMessageTime?: string;
|
lng: string;
|
||||||
createMomentTimeFormatted?: string;
|
status: number;
|
||||||
createMessageTimeFormatted?: string;
|
isDel: number;
|
||||||
}
|
delTime: number;
|
||||||
|
wechatChatroomId?: string | null;
|
||||||
// 内容库类型
|
senderNickname: string;
|
||||||
export interface ContentLibrary {
|
createMessageTime?: string | null;
|
||||||
id: string;
|
comment: string;
|
||||||
name: string;
|
sendTime: number;
|
||||||
sourceType: number; // 1=微信好友, 2=聊天群
|
sendTimes: number;
|
||||||
creatorName?: string;
|
contentTypeName: string;
|
||||||
updateTime: string;
|
}
|
||||||
status: number; // 0=未启用, 1=已启用
|
|
||||||
itemCount?: number;
|
// 内容库类型
|
||||||
createTime: string;
|
export interface ContentLibrary {
|
||||||
sourceFriends?: string[];
|
id: string;
|
||||||
sourceGroups?: string[];
|
name: string;
|
||||||
keywordInclude?: string[];
|
sourceType: number; // 1=微信好友, 2=聊天群
|
||||||
keywordExclude?: string[];
|
creatorName?: string;
|
||||||
aiPrompt?: string;
|
updateTime: string;
|
||||||
timeEnabled?: number;
|
status: number; // 0=未启用, 1=已启用
|
||||||
timeStart?: string;
|
itemCount?: number;
|
||||||
timeEnd?: string;
|
createTime: string;
|
||||||
selectedFriends?: any[];
|
sourceFriends?: string[];
|
||||||
selectedGroups?: any[];
|
sourceGroups?: string[];
|
||||||
selectedGroupMembers?: WechatGroupMember[];
|
keywordInclude?: string[];
|
||||||
}
|
keywordExclude?: string[];
|
||||||
|
aiPrompt?: string;
|
||||||
// 微信群成员
|
timeEnabled?: number;
|
||||||
export interface WechatGroupMember {
|
timeStart?: string;
|
||||||
id: string;
|
timeEnd?: string;
|
||||||
nickname: string;
|
selectedFriends?: any[];
|
||||||
wechatId: string;
|
selectedGroups?: any[];
|
||||||
avatar: string;
|
selectedGroupMembers?: WechatGroupMember[];
|
||||||
gender?: "male" | "female";
|
}
|
||||||
role?: "owner" | "admin" | "member";
|
|
||||||
joinTime?: string;
|
// 微信群成员
|
||||||
}
|
export interface WechatGroupMember {
|
||||||
|
id: string;
|
||||||
// API 响应类型
|
nickname: string;
|
||||||
export interface ApiResponse<T = any> {
|
wechatId: string;
|
||||||
code: number;
|
avatar: string;
|
||||||
msg: string;
|
gender?: "male" | "female";
|
||||||
data: T;
|
role?: "owner" | "admin" | "member";
|
||||||
}
|
joinTime?: string;
|
||||||
|
}
|
||||||
export interface ItemListResponse {
|
|
||||||
list: ContentItem[];
|
// API 响应类型
|
||||||
total: number;
|
export interface ApiResponse<T = any> {
|
||||||
}
|
code: number;
|
||||||
|
msg: string;
|
||||||
// 获取素材列表参数
|
data: T;
|
||||||
export interface GetContentItemListParams {
|
}
|
||||||
libraryId: string;
|
|
||||||
page?: number;
|
export interface ItemListResponse {
|
||||||
limit?: number;
|
list: ContentItem[];
|
||||||
keyword?: string;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建素材参数
|
// 获取素材列表参数
|
||||||
export interface CreateContentItemParams {
|
export interface GetContentItemListParams {
|
||||||
libraryId: string;
|
libraryId: string;
|
||||||
title: string;
|
page?: number;
|
||||||
content: string;
|
limit?: number;
|
||||||
contentType: number;
|
keyword?: string;
|
||||||
resUrls?: string[];
|
}
|
||||||
urls?: string[];
|
|
||||||
comment?: string;
|
// 创建素材参数
|
||||||
sendTime?: string;
|
export interface CreateContentItemParams {
|
||||||
}
|
libraryId: string;
|
||||||
|
title: string;
|
||||||
// 更新素材参数
|
content: string;
|
||||||
export interface UpdateContentItemParams
|
contentType: number;
|
||||||
extends Partial<CreateContentItemParams> {
|
resUrls?: string[];
|
||||||
id: string;
|
urls?: (string | { desc?: string; image?: string; url: string })[];
|
||||||
}
|
comment?: string;
|
||||||
|
sendTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新素材参数
|
||||||
|
export interface UpdateContentItemParams
|
||||||
|
extends Partial<CreateContentItemParams> {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,253 +1,615 @@
|
|||||||
.materials-page {
|
.materials-page {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: #f5f5f5;
|
}
|
||||||
min-height: 100vh;
|
|
||||||
}
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
.search-bar {
|
align-items: center;
|
||||||
display: flex;
|
gap: 8px;
|
||||||
align-items: center;
|
margin-bottom: 16px;
|
||||||
gap: 8px;
|
background: white;
|
||||||
margin-bottom: 16px;
|
padding: 12px;
|
||||||
background: white;
|
border-radius: 8px;
|
||||||
padding: 12px;
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 8px;
|
}
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
.search-input-wrapper {
|
||||||
|
position: relative;
|
||||||
.search-input-wrapper {
|
flex: 1;
|
||||||
position: relative;
|
}
|
||||||
flex: 1;
|
|
||||||
}
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
.search-icon {
|
left: 12px;
|
||||||
position: absolute;
|
top: 50%;
|
||||||
left: 12px;
|
transform: translateY(-50%);
|
||||||
top: 50%;
|
color: #999;
|
||||||
transform: translateY(-50%);
|
z-index: 1;
|
||||||
color: #999;
|
}
|
||||||
z-index: 1;
|
|
||||||
}
|
.search-input {
|
||||||
|
padding-left: 36px;
|
||||||
.search-input {
|
border-radius: 20px;
|
||||||
padding-left: 36px;
|
border: 1px solid #e0e0e0;
|
||||||
border-radius: 20px;
|
|
||||||
border: 1px solid #e0e0e0;
|
&:focus {
|
||||||
|
border-color: #1677ff;
|
||||||
&:focus {
|
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
||||||
border-color: #1677ff;
|
}
|
||||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
}
|
||||||
}
|
|
||||||
}
|
.create-btn {
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 0 16px;
|
||||||
.create-btn {
|
}
|
||||||
border-radius: 20px;
|
|
||||||
padding: 0 16px;
|
.spinning {
|
||||||
}
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
.spinning {
|
|
||||||
animation: spin 1s linear infinite;
|
@keyframes spin {
|
||||||
}
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
@keyframes spin {
|
}
|
||||||
from {
|
to {
|
||||||
transform: rotate(0deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
to {
|
}
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
.materials-list {
|
||||||
}
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
.materials-list {
|
gap: 12px;
|
||||||
display: flex;
|
}
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
.loading {
|
||||||
}
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
.loading {
|
align-items: center;
|
||||||
display: flex;
|
padding: 40px 0;
|
||||||
justify-content: center;
|
}
|
||||||
align-items: center;
|
|
||||||
padding: 40px 0;
|
.empty-state {
|
||||||
}
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
.empty-state {
|
align-items: center;
|
||||||
display: flex;
|
justify-content: center;
|
||||||
flex-direction: column;
|
padding: 60px 20px;
|
||||||
align-items: center;
|
text-align: center;
|
||||||
justify-content: center;
|
background: white;
|
||||||
padding: 60px 20px;
|
border-radius: 8px;
|
||||||
text-align: center;
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
background: white;
|
}
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
.empty-icon {
|
||||||
}
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
.empty-icon {
|
opacity: 0.6;
|
||||||
font-size: 48px;
|
}
|
||||||
margin-bottom: 16px;
|
|
||||||
opacity: 0.6;
|
.empty-text {
|
||||||
}
|
color: #999;
|
||||||
|
margin-bottom: 20px;
|
||||||
.empty-text {
|
font-size: 14px;
|
||||||
color: #999;
|
}
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: 14px;
|
.empty-btn {
|
||||||
}
|
border-radius: 20px;
|
||||||
|
padding: 0 20px;
|
||||||
.empty-btn {
|
}
|
||||||
border-radius: 20px;
|
|
||||||
padding: 0 20px;
|
.material-card {
|
||||||
}
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
.material-card {
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
background: white;
|
border: none;
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
&:hover {
|
||||||
border: none;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
&:hover {
|
}
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
.card-header {
|
||||||
}
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
.card-header {
|
align-items: flex-start;
|
||||||
display: flex;
|
margin-bottom: 16px;
|
||||||
justify-content: space-between;
|
}
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 12px;
|
.avatar-section {
|
||||||
}
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
.material-info {
|
gap: 12px;
|
||||||
display: flex;
|
flex: 1;
|
||||||
align-items: center;
|
}
|
||||||
gap: 8px;
|
|
||||||
flex: 1;
|
.avatar {
|
||||||
}
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
.material-title {
|
border-radius: 50%;
|
||||||
display: flex;
|
background: #e6f7ff;
|
||||||
align-items: center;
|
display: flex;
|
||||||
gap: 6px;
|
align-items: center;
|
||||||
font-size: 16px;
|
justify-content: center;
|
||||||
font-weight: 600;
|
}
|
||||||
color: #333;
|
|
||||||
}
|
.avatar-icon {
|
||||||
|
font-size: 24px;
|
||||||
.content-icon {
|
color: #1677ff;
|
||||||
font-size: 16px;
|
}
|
||||||
color: #1677ff;
|
|
||||||
}
|
.header-info {
|
||||||
|
display: flex;
|
||||||
.type-tag {
|
flex-direction: column;
|
||||||
font-size: 12px;
|
gap: 4px;
|
||||||
padding: 2px 8px;
|
}
|
||||||
border-radius: 10px;
|
|
||||||
}
|
.creator-name {
|
||||||
|
font-size: 16px;
|
||||||
.menu-btn {
|
font-weight: 600;
|
||||||
background: none;
|
color: #333;
|
||||||
border: none;
|
line-height: 1.2;
|
||||||
padding: 4px;
|
}
|
||||||
cursor: pointer;
|
|
||||||
color: #999;
|
.material-id {
|
||||||
border-radius: 4px;
|
background: #e6f7ff;
|
||||||
|
color: #1677ff;
|
||||||
&:hover {
|
font-size: 12px;
|
||||||
background: #f5f5f5;
|
font-weight: 600;
|
||||||
color: #666;
|
border-radius: 12px;
|
||||||
}
|
padding: 2px 8px;
|
||||||
}
|
display: inline-block;
|
||||||
|
}
|
||||||
.menu-dropdown {
|
|
||||||
position: absolute;
|
.material-title {
|
||||||
right: 0;
|
font-size: 16px;
|
||||||
top: 100%;
|
font-weight: 600;
|
||||||
background: white;
|
color: #333;
|
||||||
border-radius: 8px;
|
margin-bottom: 12px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
line-height: 1.4;
|
||||||
z-index: 1000;
|
}
|
||||||
min-width: 120px;
|
|
||||||
padding: 4px;
|
.content-icon {
|
||||||
margin-top: 4px;
|
font-size: 16px;
|
||||||
}
|
color: #1677ff;
|
||||||
|
}
|
||||||
.menu-item {
|
|
||||||
display: flex;
|
.type-tag {
|
||||||
align-items: center;
|
font-size: 12px;
|
||||||
gap: 8px;
|
padding: 2px 8px;
|
||||||
padding: 8px 12px;
|
border-radius: 10px;
|
||||||
cursor: pointer;
|
}
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
.menu-btn {
|
||||||
color: #333;
|
background: none;
|
||||||
transition: background 0.2s;
|
border: none;
|
||||||
|
padding: 4px;
|
||||||
&:hover {
|
cursor: pointer;
|
||||||
background: #f5f5f5;
|
color: #999;
|
||||||
}
|
border-radius: 4px;
|
||||||
|
|
||||||
&.danger {
|
&:hover {
|
||||||
color: #ff4d4f;
|
background: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
&:hover {
|
}
|
||||||
background: #fff2f0;
|
}
|
||||||
}
|
|
||||||
}
|
.menu-dropdown {
|
||||||
}
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
.card-content {
|
top: 100%;
|
||||||
display: flex;
|
background: white;
|
||||||
flex-direction: column;
|
border-radius: 8px;
|
||||||
gap: 8px;
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
}
|
z-index: 1000;
|
||||||
|
min-width: 120px;
|
||||||
.content-preview {
|
padding: 4px;
|
||||||
color: #666;
|
margin-top: 4px;
|
||||||
font-size: 14px;
|
}
|
||||||
line-height: 1.5;
|
|
||||||
max-height: 60px;
|
.menu-item {
|
||||||
overflow: hidden;
|
display: flex;
|
||||||
text-overflow: ellipsis;
|
align-items: center;
|
||||||
display: -webkit-box;
|
gap: 8px;
|
||||||
-webkit-line-clamp: 3;
|
padding: 8px 12px;
|
||||||
-webkit-box-orient: vertical;
|
cursor: pointer;
|
||||||
}
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
.material-meta {
|
color: #333;
|
||||||
display: flex;
|
transition: background 0.2s;
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
&:hover {
|
||||||
font-size: 12px;
|
background: #f5f5f5;
|
||||||
color: #999;
|
}
|
||||||
}
|
|
||||||
|
&.danger {
|
||||||
.meta-item {
|
color: #ff4d4f;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
&:hover {
|
||||||
}
|
background: #fff2f0;
|
||||||
|
}
|
||||||
.pagination-wrapper {
|
}
|
||||||
display: flex;
|
}
|
||||||
justify-content: center;
|
|
||||||
margin-top: 20px;
|
.link-preview {
|
||||||
padding: 16px;
|
display: flex;
|
||||||
background: white;
|
align-items: flex-start;
|
||||||
border-radius: 8px;
|
gap: 12px;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
padding: 12px;
|
||||||
}
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
.pagination {
|
margin-bottom: 16px;
|
||||||
:global {
|
border: 1px solid #e9ecef;
|
||||||
.adm-pagination-item {
|
cursor: pointer;
|
||||||
border-radius: 6px;
|
transition: all 0.2s ease;
|
||||||
margin: 0 2px;
|
|
||||||
|
&:hover {
|
||||||
&.adm-pagination-item-active {
|
background: #e9ecef;
|
||||||
background: #1677ff;
|
border-color: #1677ff;
|
||||||
color: white;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
.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;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #1677ff;
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #ff4d4f;
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #ff7875;
|
||||||
|
border-color: #ff7875;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: white;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容类型标签样式
|
||||||
|
.content-type-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片类型预览样式
|
||||||
|
.material-image-preview {
|
||||||
|
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,387 +1,413 @@
|
|||||||
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,
|
import {
|
||||||
SpinLoading,
|
PlusOutlined,
|
||||||
Dialog,
|
SearchOutlined,
|
||||||
Card,
|
ReloadOutlined,
|
||||||
Avatar,
|
EditOutlined,
|
||||||
Tag,
|
DeleteOutlined,
|
||||||
} from "antd-mobile";
|
UserOutlined,
|
||||||
import { Pagination, Input } from "antd";
|
BarChartOutlined,
|
||||||
import {
|
PictureOutlined,
|
||||||
PlusOutlined,
|
LinkOutlined,
|
||||||
SearchOutlined,
|
VideoCameraOutlined,
|
||||||
ReloadOutlined,
|
FileTextOutlined,
|
||||||
EditOutlined,
|
AppstoreOutlined,
|
||||||
DeleteOutlined,
|
} from "@ant-design/icons";
|
||||||
EyeOutlined,
|
import Layout from "@/components/Layout/Layout";
|
||||||
MoreOutlined,
|
import NavCommon from "@/components/NavCommon";
|
||||||
PictureOutlined,
|
import { getContentItemList, deleteContentItem } from "./api";
|
||||||
LinkOutlined,
|
import { ContentItem } from "./data";
|
||||||
VideoCameraOutlined,
|
import style from "./index.module.scss";
|
||||||
FileTextOutlined,
|
|
||||||
AppstoreOutlined,
|
// 内容类型配置
|
||||||
} from "@ant-design/icons";
|
const contentTypeConfig = {
|
||||||
import Layout from "@/components/Layout/Layout";
|
1: { label: "图片", icon: PictureOutlined, color: "#52c41a" },
|
||||||
import NavCommon from "@/components/NavCommon";
|
2: { label: "链接", icon: LinkOutlined, color: "#1890ff" },
|
||||||
import {
|
3: { label: "视频", icon: VideoCameraOutlined, color: "#722ed1" },
|
||||||
getContentItemList,
|
4: { label: "文本", icon: FileTextOutlined, color: "#fa8c16" },
|
||||||
deleteContentItem,
|
5: { label: "小程序", icon: AppstoreOutlined, color: "#eb2f96" },
|
||||||
getContentLibraryDetail,
|
6: { label: "图文", icon: PictureOutlined, color: "#13c2c2" },
|
||||||
} from "./api";
|
};
|
||||||
import { ContentItem, ContentLibrary } from "./data";
|
|
||||||
import style from "./index.module.scss";
|
const MaterialsList: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
// 卡片菜单组件
|
const { id } = useParams<{ id: string }>();
|
||||||
interface CardMenuProps {
|
const [materials, setMaterials] = useState<ContentItem[]>([]);
|
||||||
onView: () => void;
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
onEdit: () => void;
|
const [loading, setLoading] = useState(false);
|
||||||
onDelete: () => void;
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
}
|
const [total, setTotal] = useState(0);
|
||||||
|
const pageSize = 20;
|
||||||
const CardMenu: React.FC<CardMenuProps> = ({ onView, onEdit, onDelete }) => {
|
|
||||||
const [open, setOpen] = useState(false);
|
// 获取素材列表
|
||||||
const menuRef = React.useRef<HTMLDivElement>(null);
|
const fetchMaterials = useCallback(async () => {
|
||||||
|
if (!id) return;
|
||||||
React.useEffect(() => {
|
setLoading(true);
|
||||||
function handleClickOutside(event: MouseEvent) {
|
try {
|
||||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
const response = await getContentItemList({
|
||||||
setOpen(false);
|
libraryId: id,
|
||||||
}
|
page: currentPage,
|
||||||
}
|
limit: pageSize,
|
||||||
if (open) document.addEventListener("mousedown", handleClickOutside);
|
keyword: searchQuery,
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
});
|
||||||
}, [open]);
|
|
||||||
|
setMaterials(response.list || []);
|
||||||
return (
|
setTotal(response.total || 0);
|
||||||
<div style={{ position: "relative" }}>
|
} catch (error: unknown) {
|
||||||
<button onClick={() => setOpen((v) => !v)} className={style["menu-btn"]}>
|
console.error("获取素材列表失败:", error);
|
||||||
<MoreOutlined />
|
Toast.show({
|
||||||
</button>
|
content: error instanceof Error ? error.message : "请检查网络连接",
|
||||||
{open && (
|
position: "top",
|
||||||
<div ref={menuRef} className={style["menu-dropdown"]}>
|
});
|
||||||
<div
|
} finally {
|
||||||
onClick={() => {
|
setLoading(false);
|
||||||
onView();
|
}
|
||||||
setOpen(false);
|
}, [id, currentPage, searchQuery]);
|
||||||
}}
|
|
||||||
className={style["menu-item"]}
|
useEffect(() => {
|
||||||
>
|
fetchMaterials();
|
||||||
<EyeOutlined />
|
}, [fetchMaterials]);
|
||||||
查看
|
|
||||||
</div>
|
const handleCreateNew = () => {
|
||||||
<div
|
navigate(`/content/materials/new/${id}`);
|
||||||
onClick={() => {
|
};
|
||||||
onEdit();
|
|
||||||
setOpen(false);
|
const handleEdit = (materialId: number) => {
|
||||||
}}
|
navigate(`/content/materials/edit/${id}/${materialId}`);
|
||||||
className={style["menu-item"]}
|
};
|
||||||
>
|
|
||||||
<EditOutlined />
|
const handleDelete = async (materialId: number) => {
|
||||||
编辑
|
const result = await Dialog.confirm({
|
||||||
</div>
|
content: "确定要删除这个素材吗?",
|
||||||
<div
|
confirmText: "删除",
|
||||||
onClick={() => {
|
cancelText: "取消",
|
||||||
onDelete();
|
});
|
||||||
setOpen(false);
|
|
||||||
}}
|
if (result) {
|
||||||
className={`${style["menu-item"]} ${style["danger"]}`}
|
try {
|
||||||
>
|
await deleteContentItem(materialId.toString());
|
||||||
<DeleteOutlined />
|
Toast.show({
|
||||||
删除
|
content: "删除成功",
|
||||||
</div>
|
position: "top",
|
||||||
</div>
|
});
|
||||||
)}
|
fetchMaterials();
|
||||||
</div>
|
} catch (error: unknown) {
|
||||||
);
|
console.error("删除素材失败:", error);
|
||||||
};
|
Toast.show({
|
||||||
|
content: error instanceof Error ? error.message : "请检查网络连接",
|
||||||
// 内容类型图标映射
|
position: "top",
|
||||||
const getContentTypeIcon = (type: number) => {
|
});
|
||||||
switch (type) {
|
}
|
||||||
case 1:
|
}
|
||||||
return <PictureOutlined className={style["content-icon"]} />;
|
};
|
||||||
case 2:
|
|
||||||
return <LinkOutlined className={style["content-icon"]} />;
|
const handleView = (materialId: number) => {
|
||||||
case 3:
|
// 可以跳转到素材详情页面或显示弹窗
|
||||||
return <VideoCameraOutlined className={style["content-icon"]} />;
|
console.log("查看素材:", materialId);
|
||||||
case 4:
|
};
|
||||||
return <FileTextOutlined className={style["content-icon"]} />;
|
|
||||||
case 5:
|
const handleSearch = () => {
|
||||||
return <AppstoreOutlined className={style["content-icon"]} />;
|
setCurrentPage(1);
|
||||||
default:
|
fetchMaterials();
|
||||||
return <FileTextOutlined className={style["content-icon"]} />;
|
};
|
||||||
}
|
|
||||||
};
|
const handleRefresh = () => {
|
||||||
|
fetchMaterials();
|
||||||
// 内容类型文字映射
|
};
|
||||||
const getContentTypeText = (type: number) => {
|
|
||||||
switch (type) {
|
const handlePageChange = (page: number) => {
|
||||||
case 1:
|
setCurrentPage(page);
|
||||||
return "图片";
|
};
|
||||||
case 2:
|
|
||||||
return "链接";
|
// 渲染内容类型标签
|
||||||
case 3:
|
const renderContentTypeTag = (contentType: number) => {
|
||||||
return "视频";
|
const config =
|
||||||
case 4:
|
contentTypeConfig[contentType as keyof typeof contentTypeConfig];
|
||||||
return "文本";
|
if (!config) return null;
|
||||||
case 5:
|
|
||||||
return "小程序";
|
const IconComponent = config.icon;
|
||||||
case 6:
|
return (
|
||||||
return "图文";
|
<div
|
||||||
default:
|
className={style["content-type-tag"]}
|
||||||
return "未知";
|
style={{ backgroundColor: config.color + "20", color: config.color }}
|
||||||
}
|
>
|
||||||
};
|
<IconComponent style={{ fontSize: 12, marginRight: 4 }} />
|
||||||
|
{config.label}
|
||||||
const MaterialsList: React.FC = () => {
|
</div>
|
||||||
const navigate = useNavigate();
|
);
|
||||||
const { id } = useParams<{ id: string }>();
|
};
|
||||||
const [materials, setMaterials] = useState<ContentItem[]>([]);
|
|
||||||
const [library, setLibrary] = useState<ContentLibrary | null>(null);
|
// 渲染素材内容预览
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const renderContentPreview = (material: ContentItem) => {
|
||||||
const [loading, setLoading] = useState(false);
|
const { contentType, content, resUrls, urls, coverImage } = material;
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const [total, setTotal] = useState(0);
|
switch (contentType) {
|
||||||
const pageSize = 20;
|
case 1: // 图片
|
||||||
|
return (
|
||||||
// 获取内容库详情
|
<div className={style["material-image-preview"]}>
|
||||||
const fetchLibraryDetail = useCallback(async () => {
|
{resUrls && resUrls.length > 0 ? (
|
||||||
if (!id) return;
|
<div
|
||||||
try {
|
className={`${style["image-grid"]} ${
|
||||||
const response = await getContentLibraryDetail(id);
|
resUrls.length === 1
|
||||||
if (response.code === 200 && response.data) {
|
? style.single
|
||||||
setLibrary(response.data);
|
: resUrls.length === 2
|
||||||
}
|
? style.double
|
||||||
} catch (error) {
|
: resUrls.length === 3
|
||||||
console.error("获取内容库详情失败:", error);
|
? style.triple
|
||||||
}
|
: resUrls.length === 4
|
||||||
}, [id]);
|
? style.quad
|
||||||
|
: style.grid
|
||||||
// 获取素材列表
|
}`}
|
||||||
const fetchMaterials = useCallback(async () => {
|
>
|
||||||
if (!id) return;
|
{resUrls.slice(0, 9).map((url, index) => (
|
||||||
setLoading(true);
|
<img key={index} src={url} alt={`图片${index + 1}`} />
|
||||||
try {
|
))}
|
||||||
const response = await getContentItemList({
|
{resUrls.length > 9 && (
|
||||||
libraryId: id,
|
<div className={style["image-more"]}>
|
||||||
page: currentPage,
|
+{resUrls.length - 9}
|
||||||
limit: pageSize,
|
</div>
|
||||||
keyword: searchQuery,
|
)}
|
||||||
});
|
</div>
|
||||||
|
) : coverImage ? (
|
||||||
if (response.code === 200 && response.data) {
|
<div className={`${style["image-grid"]} ${style.single}`}>
|
||||||
setMaterials(response.data.list || []);
|
<img src={coverImage} alt="封面图" />
|
||||||
setTotal(response.data.total || 0);
|
</div>
|
||||||
} else {
|
) : (
|
||||||
Toast.show({
|
<div className={style["no-image"]}>暂无图片</div>
|
||||||
content: response.msg || "获取素材列表失败",
|
)}
|
||||||
position: "top",
|
</div>
|
||||||
});
|
);
|
||||||
}
|
|
||||||
} catch (error: any) {
|
case 2: // 链接
|
||||||
console.error("获取素材列表失败:", error);
|
return (
|
||||||
Toast.show({
|
<div className={style["material-link-preview"]}>
|
||||||
content: error?.message || "请检查网络连接",
|
{urls && urls.length > 0 && (
|
||||||
position: "top",
|
<div
|
||||||
});
|
className={style["link-card"]}
|
||||||
} finally {
|
onClick={() => {
|
||||||
setLoading(false);
|
window.open(urls[0].url, "_blank");
|
||||||
}
|
}}
|
||||||
}, [id, currentPage, searchQuery]);
|
>
|
||||||
|
{urls[0].image && (
|
||||||
useEffect(() => {
|
<div className={style["link-image"]}>
|
||||||
fetchLibraryDetail();
|
<img src={urls[0].image} alt="链接预览" />
|
||||||
}, [fetchLibraryDetail]);
|
</div>
|
||||||
|
)}
|
||||||
useEffect(() => {
|
<div className={style["link-content"]}>
|
||||||
fetchMaterials();
|
<div className={style["link-title"]}>
|
||||||
}, [fetchMaterials]);
|
{urls[0].desc || "链接"}
|
||||||
|
</div>
|
||||||
const handleCreateNew = () => {
|
<div className={style["link-url"]}>{urls[0].url}</div>
|
||||||
navigate(`/content/materials/new/${id}`);
|
</div>
|
||||||
};
|
</div>
|
||||||
|
)}
|
||||||
const handleEdit = (materialId: string) => {
|
</div>
|
||||||
navigate(`/content/materials/edit/${id}/${materialId}`);
|
);
|
||||||
};
|
|
||||||
|
case 3: // 视频
|
||||||
const handleDelete = async (materialId: string) => {
|
return (
|
||||||
const result = await Dialog.confirm({
|
<div className={style["material-video-preview"]}>
|
||||||
content: "确定要删除这个素材吗?",
|
{resUrls && resUrls.length > 0 ? (
|
||||||
confirmText: "删除",
|
<div className={style["video-thumbnail"]}>
|
||||||
cancelText: "取消",
|
<video src={resUrls[0]} controls />
|
||||||
});
|
</div>
|
||||||
|
) : (
|
||||||
if (result) {
|
<div className={style["no-video"]}>暂无视频</div>
|
||||||
try {
|
)}
|
||||||
const response = await deleteContentItem(materialId);
|
</div>
|
||||||
if (response.code === 200) {
|
);
|
||||||
Toast.show({
|
|
||||||
content: "删除成功",
|
case 4: // 文本
|
||||||
position: "top",
|
return (
|
||||||
});
|
<div className={style["material-text-preview"]}>
|
||||||
fetchMaterials();
|
<div className={style["text-content"]}>
|
||||||
} else {
|
{content.length > 100
|
||||||
Toast.show({
|
? `${content.substring(0, 100)}...`
|
||||||
content: response.msg || "删除失败",
|
: content}
|
||||||
position: "top",
|
</div>
|
||||||
});
|
</div>
|
||||||
}
|
);
|
||||||
} catch (error: any) {
|
|
||||||
console.error("删除素材失败:", error);
|
case 5: // 小程序
|
||||||
Toast.show({
|
return (
|
||||||
content: error?.message || "请检查网络连接",
|
<div className={style["material-miniprogram-preview"]}>
|
||||||
position: "top",
|
{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 || "小程序"}
|
||||||
const handleView = (materialId: string) => {
|
</div>
|
||||||
// 可以跳转到素材详情页面或显示弹窗
|
</div>
|
||||||
console.log("查看素材:", materialId);
|
</div>
|
||||||
};
|
)}
|
||||||
|
</div>
|
||||||
const handleSearch = () => {
|
);
|
||||||
setCurrentPage(1);
|
|
||||||
fetchMaterials();
|
case 6: // 图文
|
||||||
};
|
return (
|
||||||
|
<div className={style["material-article-preview"]}>
|
||||||
const handleRefresh = () => {
|
{coverImage && (
|
||||||
fetchMaterials();
|
<div className={style["article-image"]}>
|
||||||
};
|
<img src={coverImage} alt="文章封面" />
|
||||||
|
</div>
|
||||||
const handlePageChange = (page: number) => {
|
)}
|
||||||
setCurrentPage(page);
|
<div className={style["article-content"]}>
|
||||||
};
|
<div className={style["article-title"]}>
|
||||||
|
{material.title || "图文内容"}
|
||||||
const filteredMaterials = materials.filter(
|
</div>
|
||||||
(material) =>
|
<div className={style["article-text"]}>
|
||||||
material.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
{content.length > 80
|
||||||
material.content?.toLowerCase().includes(searchQuery.toLowerCase())
|
? `${content.substring(0, 80)}...`
|
||||||
);
|
: content}
|
||||||
|
</div>
|
||||||
return (
|
</div>
|
||||||
<Layout
|
</div>
|
||||||
header={<NavCommon title={`${library?.name || "内容库"} - 素材管理`} />}
|
);
|
||||||
>
|
|
||||||
<div className={style["materials-page"]}>
|
default:
|
||||||
{/* 搜索和操作栏 */}
|
return (
|
||||||
<div className={style["search-bar"]}>
|
<div className={style["material-default-preview"]}>
|
||||||
<div className={style["search-input-wrapper"]}>
|
<div className={style["default-content"]}>{content}</div>
|
||||||
<SearchOutlined className={style["search-icon"]} />
|
</div>
|
||||||
<Input
|
);
|
||||||
placeholder="搜索素材..."
|
}
|
||||||
value={searchQuery}
|
};
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
onPressEnter={handleSearch}
|
return (
|
||||||
className={style["search-input"]}
|
<Layout
|
||||||
/>
|
header={
|
||||||
</div>
|
<>
|
||||||
<Button
|
<NavCommon
|
||||||
size="small"
|
title="素材管理"
|
||||||
onClick={handleRefresh}
|
right={
|
||||||
disabled={loading}
|
<Button type="primary" onClick={handleCreateNew}>
|
||||||
className={style["refresh-btn"]}
|
<PlusOutlined /> 新建素材
|
||||||
>
|
</Button>
|
||||||
<ReloadOutlined className={loading ? style["spinning"] : ""} />
|
}
|
||||||
</Button>
|
/>
|
||||||
<Button
|
{/* 搜索栏 */}
|
||||||
color="primary"
|
<div className="search-bar">
|
||||||
size="small"
|
<div className="search-input-wrapper">
|
||||||
onClick={handleCreateNew}
|
<Input
|
||||||
className={style["create-btn"]}
|
placeholder="搜索素材内容"
|
||||||
>
|
value={searchQuery}
|
||||||
<PlusOutlined />
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
新建
|
prefix={<SearchOutlined />}
|
||||||
</Button>
|
allowClear
|
||||||
</div>
|
size="large"
|
||||||
|
/>
|
||||||
{/* 素材列表 */}
|
</div>
|
||||||
<div className={style["materials-list"]}>
|
<Button
|
||||||
{loading ? (
|
onClick={handleRefresh}
|
||||||
<div className={style["loading"]}>
|
loading={loading}
|
||||||
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
icon={<ReloadOutlined />}
|
||||||
</div>
|
size="large"
|
||||||
) : filteredMaterials.length === 0 ? (
|
></Button>
|
||||||
<div className={style["empty-state"]}>
|
</div>
|
||||||
<div className={style["empty-icon"]}>📄</div>
|
</>
|
||||||
<div className={style["empty-text"]}>
|
}
|
||||||
暂无素材,快去新建一个吧!
|
footer={
|
||||||
</div>
|
<div className={style["pagination-wrapper"]}>
|
||||||
<Button
|
<Pagination
|
||||||
color="primary"
|
current={currentPage}
|
||||||
size="small"
|
pageSize={pageSize}
|
||||||
onClick={handleCreateNew}
|
total={total}
|
||||||
className={style["empty-btn"]}
|
onChange={handlePageChange}
|
||||||
>
|
showSizeChanger={false}
|
||||||
新建素材
|
/>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
}
|
||||||
) : (
|
loading={loading}
|
||||||
<>
|
>
|
||||||
{filteredMaterials.map((material) => (
|
<div className={style["materials-page"]}>
|
||||||
<Card key={material.id} className={style["material-card"]}>
|
{/* 素材列表 */}
|
||||||
<div className={style["card-header"]}>
|
<div className={style["materials-list"]}>
|
||||||
<div className={style["material-info"]}>
|
{loading ? (
|
||||||
<div className={style["material-title"]}>
|
<div className={style["loading"]}>
|
||||||
{getContentTypeIcon(material.contentType)}
|
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||||
<span>{material.title || "无标题"}</span>
|
</div>
|
||||||
</div>
|
) : materials.length === 0 ? (
|
||||||
<Tag color="blue" className={style["type-tag"]}>
|
<div className={style["empty-state"]}>
|
||||||
{getContentTypeText(material.contentType)}
|
<div className={style["empty-icon"]}>📄</div>
|
||||||
</Tag>
|
<div className={style["empty-text"]}>
|
||||||
</div>
|
暂无素材,快去新建一个吧!
|
||||||
<CardMenu
|
</div>
|
||||||
onView={() => handleView(material.id)}
|
<Button
|
||||||
onEdit={() => handleEdit(material.id)}
|
color="primary"
|
||||||
onDelete={() => handleDelete(material.id)}
|
onClick={handleCreateNew}
|
||||||
/>
|
className={style["empty-btn"]}
|
||||||
</div>
|
>
|
||||||
<div className={style["card-content"]}>
|
新建素材
|
||||||
<div className={style["content-preview"]}>
|
</Button>
|
||||||
{material.content?.substring(0, 100)}
|
</div>
|
||||||
{material.content &&
|
) : (
|
||||||
material.content.length > 100 &&
|
<>
|
||||||
"..."}
|
{materials.map(material => (
|
||||||
</div>
|
<Card key={material.id} className={style["material-card"]}>
|
||||||
<div className={style["material-meta"]}>
|
{/* 顶部信息 */}
|
||||||
<span className={style["meta-item"]}>
|
<div className={style["card-header"]}>
|
||||||
创建时间:
|
<div className={style["avatar-section"]}>
|
||||||
{new Date(material.createTime).toLocaleString("zh-CN")}
|
<div className={style["avatar"]}>
|
||||||
</span>
|
<UserOutlined className={style["avatar-icon"]} />
|
||||||
{material.sendTime && (
|
</div>
|
||||||
<span className={style["meta-item"]}>
|
<div className={style["header-info"]}>
|
||||||
发送时间:
|
<span className={style["creator-name"]}>
|
||||||
{new Date(material.sendTime).toLocaleString("zh-CN")}
|
{material.senderNickname || "系统创建"}
|
||||||
</span>
|
</span>
|
||||||
)}
|
<span className={style["material-id"]}>
|
||||||
</div>
|
ID: {material.id}
|
||||||
</div>
|
</span>
|
||||||
</Card>
|
</div>
|
||||||
))}
|
</div>
|
||||||
|
{renderContentTypeTag(material.contentType)}
|
||||||
{/* 分页 */}
|
</div>
|
||||||
{total > pageSize && (
|
{/* 标题 */}
|
||||||
<div className={style["pagination-wrapper"]}>
|
{material.contentType != 4 && (
|
||||||
<Pagination
|
<div className={style["card-title"]}>
|
||||||
total={total}
|
{material.content}
|
||||||
pageSize={pageSize}
|
</div>
|
||||||
current={currentPage}
|
)}
|
||||||
onChange={handlePageChange}
|
{/* 内容预览 */}
|
||||||
className={style["pagination"]}
|
{renderContentPreview(material)}
|
||||||
/>
|
|
||||||
</div>
|
{/* 操作按钮区 */}
|
||||||
)}
|
<div className={style["action-buttons"]}>
|
||||||
</>
|
<div className={style["action-btn-group"]}>
|
||||||
)}
|
<Button
|
||||||
</div>
|
onClick={() => handleEdit(material.id)}
|
||||||
</div>
|
className={style["action-btn"]}
|
||||||
</Layout>
|
>
|
||||||
);
|
<EditOutlined />
|
||||||
};
|
编辑
|
||||||
|
</Button>
|
||||||
export default MaterialsList;
|
<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>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MaterialsList;
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
@@ -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;
|
||||||
@@ -117,10 +117,10 @@
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
animation: pulse 1.5s ease-in-out infinite;
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
span:empty::before {
|
span:empty::before {
|
||||||
content: '';
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
@@ -128,9 +128,9 @@
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
animation: pulse 1.5s ease-in-out infinite;
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
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% {
|
||||||
@@ -157,7 +158,7 @@
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
@@ -173,9 +174,9 @@
|
|||||||
color: #333;
|
color: #333;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -248,7 +249,7 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #f1f5f9;
|
background: #f1f5f9;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
@@ -294,62 +295,62 @@
|
|||||||
.home-page {
|
.home-page {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
padding: 10px 6px;
|
padding: 10px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon {
|
.stat-icon {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-grid,
|
.scene-grid,
|
||||||
.today-grid {
|
.today-grid {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-icon {
|
.scene-icon {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-value {
|
.scene-value {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-label {
|
.scene-label {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.today-value {
|
.today-value {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.today-label {
|
.today-label {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
min-height: 140px;
|
min-height: 140px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import request from "@/api/request";
|
import request from "@/api/request";
|
||||||
// 首页仪表盘总览
|
// 首页仪表盘总览
|
||||||
export function getDashboard() {
|
export function getDashboard() {
|
||||||
return request("/v1/dashboard", {}, "GET");
|
return request("/v1/dashboard", {}, "GET");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -139,35 +139,34 @@
|
|||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.adm-list-item-content-prefix) {
|
:global(.adm-list-item-content-prefix) {
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.adm-list-item-content-main) {
|
:global(.adm-list-item-content-main) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.adm-list-item-title) {
|
:global(.adm-list-item-title) {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.adm-list-item-description) {
|
:global(.adm-list-item-description) {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.adm-list-item-content-arrow) {
|
:global(.adm-list-item-content-arrow) {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.logout-btn {
|
.logout-btn {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
@@ -180,32 +179,32 @@
|
|||||||
.mine-page {
|
.mine-page {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
background: #666;
|
background: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-name {
|
.user-name {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-card {
|
.menu-card {
|
||||||
:global(.adm-list-item) {
|
:global(.adm-list-item) {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|
||||||
:global(.adm-list-item-content-prefix) {
|
:global(.adm-list-item-content-prefix) {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.adm-list-item-title) {
|
:global(.adm-list-item-title) {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -1,127 +1,127 @@
|
|||||||
.recharge-page {
|
.recharge-page {
|
||||||
padding: 16px 0 60px 0;
|
padding: 16px 0 60px 0;
|
||||||
background: #f7f8fa;
|
background: #f7f8fa;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.balance-card {
|
.balance-card {
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
background: #f6ffed;
|
background: #f6ffed;
|
||||||
border: 1px solid #b7eb8f;
|
border: 1px solid #b7eb8f;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 18px 0 18px 0;
|
padding: 18px 0 18px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
.balance-content {
|
.balance-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
color: #16b364;
|
color: #16b364;
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
}
|
}
|
||||||
.wallet-icon {
|
.wallet-icon {
|
||||||
color: #16b364;
|
color: #16b364;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.balance-info {
|
.balance-info {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.balance-label {
|
.balance-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: #666;
|
color: #666;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
.balance-amount {
|
.balance-amount {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #16b364;
|
color: #16b364;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-card {
|
.quick-card {
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
.quick-list {
|
.quick-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.desc-card {
|
.desc-card {
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
background: #fffbe6;
|
background: #fffbe6;
|
||||||
border: 1px solid #ffe58f;
|
border: 1px solid #ffe58f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warn-card {
|
.warn-card {
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
background: #fff2e8;
|
background: #fff2e8;
|
||||||
border: 1px solid #ffbb96;
|
border: 1px solid #ffbb96;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-title {
|
.quick-title {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.quick-list {
|
.quick-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.quick-btn {
|
.quick-btn {
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
.quick-btn-active {
|
.quick-btn-active {
|
||||||
@extend .quick-btn;
|
@extend .quick-btn;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.recharge-main-btn {
|
.recharge-main-btn {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
.desc-title {
|
.desc-title {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.desc-text {
|
.desc-text {
|
||||||
color: #666;
|
color: #666;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.warn-content {
|
.warn-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
color: #faad14;
|
color: #faad14;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.warn-icon {
|
.warn-icon {
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
color: #faad14;
|
color: #faad14;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.warn-info {
|
.warn-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.warn-title {
|
.warn-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
.warn-text {
|
.warn-text {
|
||||||
color: #faad14;
|
color: #faad14;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +1,101 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Card, Button, Toast, NavBar } from "antd-mobile";
|
import { Card, Button, Toast, NavBar } from "antd-mobile";
|
||||||
import { useUserStore } from "@/store/module/user";
|
import { useUserStore } from "@/store/module/user";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
import { WalletOutlined, WarningOutlined } from "@ant-design/icons";
|
import { WalletOutlined, WarningOutlined } from "@ant-design/icons";
|
||||||
import NavCommon from "@/components/NavCommon";
|
import NavCommon from "@/components/NavCommon";
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
|
|
||||||
const quickAmounts = [50, 100, 200, 500, 1000];
|
const quickAmounts = [50, 100, 200, 500, 1000];
|
||||||
|
|
||||||
const Recharge: React.FC = () => {
|
const Recharge: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useUserStore();
|
const { user } = useUserStore();
|
||||||
// 假设余额从后端接口获取,实际可用props或store传递
|
// 假设余额从后端接口获取,实际可用props或store传递
|
||||||
const [balance, setBalance] = useState(0);
|
const [balance, setBalance] = useState(0);
|
||||||
const [selected, setSelected] = useState<number | null>(null);
|
const [selected, setSelected] = useState<number | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
// 充值操作
|
// 充值操作
|
||||||
const handleRecharge = async () => {
|
const handleRecharge = async () => {
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
Toast.show({ content: "请选择充值金额", position: "top" });
|
Toast.show({ content: "请选择充值金额", position: "top" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout header={<NavCommon title="账户充值" />}>
|
<Layout header={<NavCommon title="账户充值" />}>
|
||||||
<div className={style["recharge-page"]}>
|
<div className={style["recharge-page"]}>
|
||||||
<Card className={style["balance-card"]}>
|
<Card className={style["balance-card"]}>
|
||||||
<div className={style["balance-content"]}>
|
<div className={style["balance-content"]}>
|
||||||
<WalletOutlined className={style["wallet-icon"]} />
|
<WalletOutlined className={style["wallet-icon"]} />
|
||||||
<div className={style["balance-info"]}>
|
<div className={style["balance-info"]}>
|
||||||
<div className={style["balance-label"]}>当前余额</div>
|
<div className={style["balance-label"]}>当前余额</div>
|
||||||
<div className={style["balance-amount"]}>
|
<div className={style["balance-amount"]}>
|
||||||
¥{balance.toFixed(2)}
|
¥{balance.toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<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"}
|
||||||
className={
|
className={
|
||||||
selected === amt
|
selected === amt
|
||||||
? style["quick-btn-active"]
|
? style["quick-btn-active"]
|
||||||
: style["quick-btn"]
|
: style["quick-btn"]
|
||||||
}
|
}
|
||||||
onClick={() => setSelected(amt)}
|
onClick={() => setSelected(amt)}
|
||||||
>
|
>
|
||||||
¥{amt}
|
¥{amt}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
block
|
block
|
||||||
color="primary"
|
color="primary"
|
||||||
size="large"
|
size="large"
|
||||||
className={style["recharge-main-btn"]}
|
className={style["recharge-main-btn"]}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onClick={handleRecharge}
|
onClick={handleRecharge}
|
||||||
>
|
>
|
||||||
立即充值
|
立即充值
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className={style["desc-card"]}>
|
<Card className={style["desc-card"]}>
|
||||||
<div className={style["desc-title"]}>服务消耗</div>
|
<div className={style["desc-title"]}>服务消耗</div>
|
||||||
<div className={style["desc-text"]}>
|
<div className={style["desc-text"]}>
|
||||||
使用以下服务将从余额中扣除相应费用。
|
使用以下服务将从余额中扣除相应费用。
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
{balance < 10 && (
|
{balance < 10 && (
|
||||||
<Card className={style["warn-card"]}>
|
<Card className={style["warn-card"]}>
|
||||||
<div className={style["warn-content"]}>
|
<div className={style["warn-content"]}>
|
||||||
<WarningOutlined className={style["warn-icon"]} />
|
<WarningOutlined className={style["warn-icon"]} />
|
||||||
<div className={style["warn-info"]}>
|
<div className={style["warn-info"]}>
|
||||||
<div className={style["warn-title"]}>余额不足提醒</div>
|
<div className={style["warn-title"]}>余额不足提醒</div>
|
||||||
<div className={style["warn-text"]}>
|
<div className={style["warn-text"]}>
|
||||||
当前余额较低,建议及时充值以免影响服务使用
|
当前余额较低,建议及时充值以免影响服务使用
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Recharge;
|
export default Recharge;
|
||||||
|
|||||||
@@ -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;
|
totalSpent?: number;
|
||||||
avatar: string;
|
interactionCount?: number;
|
||||||
gender: number; // 0-未知, 1-男, 2-女
|
conversionRate?: number;
|
||||||
};
|
tags?: string[];
|
||||||
accountAge: string;
|
packages?: string[];
|
||||||
activityLevel: {
|
interactions?: Array<{
|
||||||
allTimes: number;
|
id: string;
|
||||||
dayTimes: number;
|
type: string;
|
||||||
};
|
content: string;
|
||||||
accountWeight: {
|
timestamp: string;
|
||||||
ageWeight: number;
|
value?: number;
|
||||||
activityWeigth: number;
|
}>;
|
||||||
restrictWeight: number;
|
}
|
||||||
realNameWeight: number;
|
|
||||||
scope: number;
|
|
||||||
};
|
|
||||||
statistics: {
|
|
||||||
todayAdded: number;
|
|
||||||
addLimit: number;
|
|
||||||
};
|
|
||||||
restrictions: Array<{
|
|
||||||
id: number;
|
|
||||||
date: number | null;
|
|
||||||
level: number; // 1-轻微, 2-中等, 3-严重
|
|
||||||
reason: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,379 +1,300 @@
|
|||||||
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 Layout from "@/components/Layout/Layout";
|
||||||
import {
|
import { getTrafficPoolDetail } from "./api";
|
||||||
UserOutlined,
|
import type { TrafficPoolUserDetail } from "./data";
|
||||||
MessageOutlined,
|
import { Card, Button, Avatar, Tag, Spin } from "antd";
|
||||||
TeamOutlined,
|
|
||||||
ClockCircleOutlined,
|
const tabList = [
|
||||||
ExclamationCircleOutlined,
|
{ key: "base", label: "基本信息" },
|
||||||
PlusOutlined,
|
{ key: "journey", label: "用户旅程" },
|
||||||
} from "@ant-design/icons";
|
{ key: "tags", label: "用户标签" },
|
||||||
import Layout from "@/components/Layout/Layout";
|
];
|
||||||
import NavCommon from "@/components/NavCommon";
|
|
||||||
import { getTrafficPoolDetail } from "./api";
|
const TrafficPoolDetail: React.FC = () => {
|
||||||
import type { TrafficPoolUserDetail } from "./data";
|
const { id } = useParams();
|
||||||
import styles from "./index.module.scss";
|
const navigate = useNavigate();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const TrafficPoolDetail: React.FC = () => {
|
const [user, setUser] = useState<TrafficPoolUserDetail | null>(null);
|
||||||
const { id } = useParams();
|
const [activeTab, setActiveTab] = useState<"base" | "journey" | "tags">(
|
||||||
const navigate = useNavigate();
|
"base"
|
||||||
const [loading, setLoading] = useState(true);
|
);
|
||||||
const [user, setUser] = useState<TrafficPoolUserDetail | null>(null);
|
|
||||||
|
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]);
|
|
||||||
|
if (loading) {
|
||||||
const getGenderText = (gender: number) => {
|
return (
|
||||||
switch (gender) {
|
<Layout>
|
||||||
case 1:
|
<div style={{ textAlign: "center", padding: "64px 0" }}>
|
||||||
return "男";
|
<Spin size="large" />
|
||||||
case 2:
|
</div>
|
||||||
return "女";
|
</Layout>
|
||||||
default:
|
);
|
||||||
return "未知";
|
}
|
||||||
}
|
if (!user) {
|
||||||
};
|
return (
|
||||||
|
<Layout>
|
||||||
const getGenderColor = (gender: number) => {
|
<div style={{ textAlign: "center", color: "#aaa", padding: "64px 0" }}>
|
||||||
switch (gender) {
|
未找到该用户
|
||||||
case 1:
|
</div>
|
||||||
return "#1677ff";
|
</Layout>
|
||||||
case 2:
|
);
|
||||||
return "#eb2f96";
|
}
|
||||||
default:
|
|
||||||
return "#999";
|
return (
|
||||||
}
|
<Layout
|
||||||
};
|
header={
|
||||||
|
<div
|
||||||
const getRestrictionLevelText = (level: number) => {
|
style={{
|
||||||
switch (level) {
|
display: "flex",
|
||||||
case 1:
|
alignItems: "center",
|
||||||
return "轻微";
|
height: 48,
|
||||||
case 2:
|
borderBottom: "1px solid #eee",
|
||||||
return "中等";
|
background: "#fff",
|
||||||
case 3:
|
}}
|
||||||
return "严重";
|
>
|
||||||
default:
|
<Button
|
||||||
return "未知";
|
type="link"
|
||||||
}
|
onClick={() => navigate(-1)}
|
||||||
};
|
style={{ marginRight: 8 }}
|
||||||
|
>
|
||||||
const getRestrictionLevelColor = (level: number) => {
|
< 返回
|
||||||
switch (level) {
|
</Button>
|
||||||
case 1:
|
<div style={{ fontWeight: 600, fontSize: 18 }}>用户详情</div>
|
||||||
return "warning";
|
</div>
|
||||||
case 2:
|
}
|
||||||
return "danger";
|
>
|
||||||
case 3:
|
<div style={{ padding: 16 }}>
|
||||||
return "danger";
|
{/* 顶部信息 */}
|
||||||
default:
|
<div
|
||||||
return "default";
|
style={{
|
||||||
}
|
display: "flex",
|
||||||
};
|
alignItems: "center",
|
||||||
|
gap: 16,
|
||||||
const formatDate = (timestamp: number | null) => {
|
marginBottom: 16,
|
||||||
if (!timestamp) return "--";
|
}}
|
||||||
try {
|
>
|
||||||
const date = new Date(timestamp * 1000);
|
<Avatar src={user.avatar} size={64} />
|
||||||
return date.toLocaleDateString("zh-CN");
|
<div>
|
||||||
} catch (error) {
|
<div style={{ fontSize: 20, fontWeight: 600 }}>{user.nickname}</div>
|
||||||
return "--";
|
<div style={{ color: "#1677ff", fontSize: 14, margin: "4px 0" }}>
|
||||||
}
|
{user.wechatId}
|
||||||
};
|
</div>
|
||||||
|
{user.packages &&
|
||||||
const formatAccountAge = (dateString: string) => {
|
user.packages.length > 0 &&
|
||||||
if (!dateString) return "--";
|
user.packages.map(pkg => (
|
||||||
try {
|
<Tag color="purple" key={pkg} style={{ marginRight: 4 }}>
|
||||||
const date = new Date(dateString);
|
{pkg}
|
||||||
return date.toLocaleDateString("zh-CN");
|
</Tag>
|
||||||
} catch (error) {
|
))}
|
||||||
return dateString;
|
</div>
|
||||||
}
|
</div>
|
||||||
};
|
{/* Tab栏 */}
|
||||||
|
<div
|
||||||
if (!user) {
|
style={{
|
||||||
return (
|
display: "flex",
|
||||||
<Layout header={<NavCommon title="用户详情" />} loading={loading}>
|
gap: 24,
|
||||||
<div className={styles.emptyState}>
|
borderBottom: "1px solid #eee",
|
||||||
<div className={styles.emptyText}>未找到该用户</div>
|
marginBottom: 16,
|
||||||
</div>
|
}}
|
||||||
</Layout>
|
>
|
||||||
);
|
{tabList.map(tab => (
|
||||||
}
|
<div
|
||||||
|
key={tab.key}
|
||||||
return (
|
style={{
|
||||||
<Layout header={<NavCommon title="用户详情" />} loading={loading}>
|
padding: "8px 0",
|
||||||
<div className={styles.container}>
|
fontWeight: 500,
|
||||||
{/* 用户基本信息 */}
|
color: activeTab === tab.key ? "#1677ff" : "#888",
|
||||||
<Card className={styles.userCard}>
|
borderBottom:
|
||||||
<div className={styles.userInfo}>
|
activeTab === tab.key ? "2px solid #1677ff" : "none",
|
||||||
<Avatar
|
cursor: "pointer",
|
||||||
src={user.userInfo.avatar}
|
fontSize: 16,
|
||||||
className={styles.avatar}
|
}}
|
||||||
fallback={<UserOutlined />}
|
onClick={() => setActiveTab(tab.key as any)}
|
||||||
/>
|
>
|
||||||
<div className={styles.userDetails}>
|
{tab.label}
|
||||||
<div className={styles.nickname}>{user.userInfo.nickname}</div>
|
</div>
|
||||||
<div className={styles.wechatId}>{user.userInfo.wechatId}</div>
|
))}
|
||||||
<div className={styles.alias}>别名:{user.userInfo.alias}</div>
|
</div>
|
||||||
<div className={styles.tags}>
|
{/* Tab内容 */}
|
||||||
<Tag
|
{activeTab === "base" && (
|
||||||
color="primary"
|
<>
|
||||||
fill="outline"
|
<Card style={{ marginBottom: 16 }} title="关键信息">
|
||||||
className={styles.genderTag}
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 24 }}>
|
||||||
style={{ color: getGenderColor(user.userInfo.gender) }}
|
<div>设备:{user.deviceName || "--"}</div>
|
||||||
>
|
<div>微信号:{user.wechatAccountName || "--"}</div>
|
||||||
{getGenderText(user.userInfo.gender)}
|
<div>客服:{user.customerServiceName || "--"}</div>
|
||||||
</Tag>
|
<div>添加时间:{user.addTime || "--"}</div>
|
||||||
{user.userInfo.weight && (
|
<div>最近互动:{user.lastInteraction || "--"}</div>
|
||||||
<Tag
|
</div>
|
||||||
color="success"
|
</Card>
|
||||||
fill="outline"
|
<Card style={{ marginBottom: 16 }} title="RFM评分">
|
||||||
className={styles.weightTag}
|
<div style={{ display: "flex", gap: 32 }}>
|
||||||
>
|
<div>
|
||||||
权重: {user.userInfo.weight}
|
<div
|
||||||
</Tag>
|
style={{ fontSize: 20, fontWeight: 600, color: "#1677ff" }}
|
||||||
)}
|
>
|
||||||
</div>
|
{user.rfmScore?.recency ?? "-"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div style={{ fontSize: 12, color: "#888" }}>最近性(R)</div>
|
||||||
</Card>
|
</div>
|
||||||
|
<div>
|
||||||
{/* Tab内容 */}
|
<div
|
||||||
<Tabs className={styles.tabs}>
|
style={{ fontSize: 20, fontWeight: 600, color: "#52c41a" }}
|
||||||
<Tabs.Tab title="基本信息" key="base">
|
>
|
||||||
<div className={styles.tabContent}>
|
{user.rfmScore?.frequency ?? "-"}
|
||||||
{/* 账户信息 */}
|
</div>
|
||||||
<Card title="账户信息" className={styles.infoCard}>
|
<div style={{ fontSize: 12, color: "#888" }}>频率(F)</div>
|
||||||
<List>
|
</div>
|
||||||
<List.Item extra={formatAccountAge(user.accountAge)}>
|
<div>
|
||||||
注册时间
|
<div
|
||||||
</List.Item>
|
style={{ fontSize: 20, fontWeight: 600, color: "#eb2f96" }}
|
||||||
<List.Item
|
>
|
||||||
extra={`${user.statistics.todayAdded}/${user.statistics.addLimit}`}
|
{user.rfmScore?.monetary ?? "-"}
|
||||||
>
|
</div>
|
||||||
今日添加
|
<div style={{ fontSize: 12, color: "#888" }}>金额(M)</div>
|
||||||
</List.Item>
|
</div>
|
||||||
<List.Item extra={user.activityLevel.allTimes}>
|
</div>
|
||||||
总消息数
|
</Card>
|
||||||
</List.Item>
|
<Card style={{ marginBottom: 16 }} title="统计数据">
|
||||||
<List.Item extra={user.activityLevel.dayTimes}>
|
<div style={{ display: "flex", gap: 32 }}>
|
||||||
今日消息
|
<div>
|
||||||
</List.Item>
|
<div
|
||||||
</List>
|
style={{ fontSize: 18, fontWeight: 600, color: "#52c41a" }}
|
||||||
</Card>
|
>
|
||||||
|
¥{user.totalSpent ?? "-"}
|
||||||
{/* 好友统计 */}
|
</div>
|
||||||
<Card title="好友统计" className={styles.infoCard}>
|
<div style={{ fontSize: 12, color: "#888" }}>总消费</div>
|
||||||
<div className={styles.statsGrid}>
|
</div>
|
||||||
<div className={styles.statItem}>
|
<div>
|
||||||
<div
|
<div
|
||||||
className={styles.statValue}
|
style={{ fontSize: 18, fontWeight: 600, color: "#1677ff" }}
|
||||||
style={{ color: "#1677ff" }}
|
>
|
||||||
>
|
{user.interactionCount ?? "-"}
|
||||||
{user.userInfo.friendShip.totalFriend}
|
</div>
|
||||||
</div>
|
<div style={{ fontSize: 12, color: "#888" }}>互动次数</div>
|
||||||
<div className={styles.statLabel}>总好友</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div className={styles.statItem}>
|
<div
|
||||||
<div
|
style={{ fontSize: 18, fontWeight: 600, color: "#faad14" }}
|
||||||
className={styles.statValue}
|
>
|
||||||
style={{ color: "#1677ff" }}
|
{user.conversionRate ?? "-"}
|
||||||
>
|
</div>
|
||||||
{user.userInfo.friendShip.maleFriend}
|
<div style={{ fontSize: 12, color: "#888" }}>转化率</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.statLabel}>男性好友</div>
|
<div>
|
||||||
</div>
|
<div
|
||||||
<div className={styles.statItem}>
|
style={{ fontSize: 18, fontWeight: 600, color: "#ff4d4f" }}
|
||||||
<div
|
>
|
||||||
className={styles.statValue}
|
{user.status === "failed"
|
||||||
style={{ color: "#eb2f96" }}
|
? "添加失败"
|
||||||
>
|
: user.status === "added"
|
||||||
{user.userInfo.friendShip.femaleFriend}
|
? "添加成功"
|
||||||
</div>
|
: "未添加"}
|
||||||
<div className={styles.statLabel}>女性好友</div>
|
</div>
|
||||||
</div>
|
<div style={{ fontSize: 12, color: "#888" }}>添加状态</div>
|
||||||
<div className={styles.statItem}>
|
</div>
|
||||||
<div className={styles.statValue} style={{ color: "#999" }}>
|
</div>
|
||||||
{user.userInfo.friendShip.unknowFriend}
|
</Card>
|
||||||
</div>
|
</>
|
||||||
<div className={styles.statLabel}>未知性别</div>
|
)}
|
||||||
</div>
|
{activeTab === "journey" && (
|
||||||
<div className={styles.statItem}>
|
<Card title="互动记录">
|
||||||
<div
|
{user.interactions && user.interactions.length > 0 ? (
|
||||||
className={styles.statValue}
|
user.interactions.slice(0, 4).map(it => (
|
||||||
style={{ color: "#52c41a" }}
|
<div
|
||||||
>
|
key={it.id}
|
||||||
{user.userInfo.friendShip.groupNumber}
|
style={{
|
||||||
</div>
|
display: "flex",
|
||||||
<div className={styles.statLabel}>群聊数量</div>
|
alignItems: "center",
|
||||||
</div>
|
gap: 12,
|
||||||
</div>
|
borderBottom: "1px solid #f0f0f0",
|
||||||
</Card>
|
padding: "12px 0",
|
||||||
|
}}
|
||||||
{/* 活跃度统计 */}
|
>
|
||||||
<Card title="活跃度统计" className={styles.infoCard}>
|
<div style={{ fontSize: 22 }}>
|
||||||
<div className={styles.statsGrid}>
|
{it.type === "click" && "📱"}
|
||||||
<div className={styles.statItem}>
|
{it.type === "message" && "💬"}
|
||||||
<div
|
{it.type === "purchase" && "💲"}
|
||||||
className={styles.statValue}
|
{it.type === "view" && "👁️"}
|
||||||
style={{ color: "#52c41a" }}
|
</div>
|
||||||
>
|
<div style={{ flex: 1 }}>
|
||||||
{user.userInfo.activity.totalMsgCount}
|
<div style={{ fontWeight: 500 }}>
|
||||||
</div>
|
{it.type === "click" && "点击行为"}
|
||||||
<div className={styles.statLabel}>总消息数</div>
|
{it.type === "message" && "消息互动"}
|
||||||
</div>
|
{it.type === "purchase" && "购买行为"}
|
||||||
<div className={styles.statItem}>
|
{it.type === "view" && "页面浏览"}
|
||||||
<div
|
</div>
|
||||||
className={styles.statValue}
|
<div style={{ color: "#888", fontSize: 13 }}>
|
||||||
style={{ color: "#faad14" }}
|
{it.content}
|
||||||
>
|
{it.type === "purchase" && it.value && (
|
||||||
{user.userInfo.activity.sevenDayMsgCount}
|
<span
|
||||||
</div>
|
style={{
|
||||||
<div className={styles.statLabel}>7天消息</div>
|
color: "#52c41a",
|
||||||
</div>
|
fontWeight: 600,
|
||||||
<div className={styles.statItem}>
|
marginLeft: 4,
|
||||||
<div
|
}}
|
||||||
className={styles.statValue}
|
>
|
||||||
style={{ color: "#722ed1" }}
|
¥{it.value}
|
||||||
>
|
</span>
|
||||||
{user.userInfo.activity.thirtyDayMsgCount}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.statLabel}>30天消息</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div className={styles.statItem}>
|
style={{
|
||||||
<div
|
fontSize: 12,
|
||||||
className={styles.statValue}
|
color: "#aaa",
|
||||||
style={{ color: "#13c2c2" }}
|
whiteSpace: "nowrap",
|
||||||
>
|
}}
|
||||||
{user.userInfo.activity.yesterdayMsgCount}
|
>
|
||||||
</div>
|
{it.timestamp}
|
||||||
<div className={styles.statLabel}>昨日消息</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))
|
||||||
</Card>
|
) : (
|
||||||
|
<div
|
||||||
{/* 账户权重 */}
|
style={{
|
||||||
<Card title="账户权重" className={styles.infoCard}>
|
color: "#aaa",
|
||||||
<div className={styles.statsGrid}>
|
textAlign: "center",
|
||||||
<div className={styles.statItem}>
|
padding: "24px 0",
|
||||||
<div
|
}}
|
||||||
className={styles.statValue}
|
>
|
||||||
style={{ color: "#1677ff" }}
|
暂无互动记录
|
||||||
>
|
</div>
|
||||||
{user.accountWeight.ageWeight}
|
)}
|
||||||
</div>
|
</Card>
|
||||||
<div className={styles.statLabel}>年龄权重</div>
|
)}
|
||||||
</div>
|
{activeTab === "tags" && (
|
||||||
<div className={styles.statItem}>
|
<Card title="用户标签">
|
||||||
<div
|
<div style={{ marginBottom: 12 }}>
|
||||||
className={styles.statValue}
|
{user.tags && user.tags.length > 0 ? (
|
||||||
style={{ color: "#52c41a" }}
|
user.tags.map(tag => (
|
||||||
>
|
<Tag
|
||||||
{user.accountWeight.activityWeigth}
|
key={tag}
|
||||||
</div>
|
color="blue"
|
||||||
<div className={styles.statLabel}>活跃权重</div>
|
style={{ marginRight: 8, marginBottom: 8 }}
|
||||||
</div>
|
>
|
||||||
<div className={styles.statItem}>
|
{tag}
|
||||||
<div
|
</Tag>
|
||||||
className={styles.statValue}
|
))
|
||||||
style={{ color: "#faad14" }}
|
) : (
|
||||||
>
|
<span style={{ color: "#aaa" }}>暂无标签</span>
|
||||||
{user.accountWeight.restrictWeight}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.statLabel}>限制权重</div>
|
<Button type="dashed" block>
|
||||||
</div>
|
➕ 添加新标签
|
||||||
<div className={styles.statItem}>
|
</Button>
|
||||||
<div
|
</Card>
|
||||||
className={styles.statValue}
|
)}
|
||||||
style={{ color: "#722ed1" }}
|
</div>
|
||||||
>
|
</Layout>
|
||||||
{user.accountWeight.realNameWeight}
|
);
|
||||||
</div>
|
};
|
||||||
<div className={styles.statLabel}>实名权重</div>
|
|
||||||
</div>
|
export default TrafficPoolDetail;
|
||||||
<div className={styles.statItem}>
|
|
||||||
<div
|
|
||||||
className={styles.statValue}
|
|
||||||
style={{ color: "#13c2c2" }}
|
|
||||||
>
|
|
||||||
{user.accountWeight.scope}
|
|
||||||
</div>
|
|
||||||
<div className={styles.statLabel}>综合评分</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</Tabs.Tab>
|
|
||||||
|
|
||||||
<Tabs.Tab title="限制记录" key="restrictions">
|
|
||||||
<div className={styles.tabContent}>
|
|
||||||
<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>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TrafficPoolDetail;
|
|
||||||
|
|||||||
@@ -1,47 +1,47 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Modal, Selector } from "antd-mobile";
|
import { Modal, Selector } from "antd-mobile";
|
||||||
import type { PackageOption } from "./data";
|
import type { PackageOption } from "./data";
|
||||||
|
|
||||||
interface BatchAddModalProps {
|
interface BatchAddModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
packageOptions: PackageOption[];
|
packageOptions: PackageOption[];
|
||||||
batchTarget: string;
|
batchTarget: string;
|
||||||
setBatchTarget: (v: string) => void;
|
setBatchTarget: (v: string) => void;
|
||||||
selectedCount: number;
|
selectedCount: number;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BatchAddModal: React.FC<BatchAddModalProps> = ({
|
const BatchAddModal: React.FC<BatchAddModalProps> = ({
|
||||||
visible,
|
visible,
|
||||||
onClose,
|
onClose,
|
||||||
packageOptions,
|
packageOptions,
|
||||||
batchTarget,
|
batchTarget,
|
||||||
setBatchTarget,
|
setBatchTarget,
|
||||||
selectedCount,
|
selectedCount,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}) => (
|
}) => (
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
title="批量加入分组"
|
title="批量加入分组"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
footer={[
|
footer={[
|
||||||
{ text: "取消", onClick: onClose },
|
{ text: "取消", onClick: onClose },
|
||||||
{ text: "确定", onClick: onConfirm },
|
{ text: "确定", onClick: onConfirm },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<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 }}>
|
||||||
将选中的{selectedCount}个用户加入所选分组
|
将选中的{selectedCount}个用户加入所选分组
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default BatchAddModal;
|
export default BatchAddModal;
|
||||||
|
|||||||
@@ -1,84 +1,84 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Card, Button } from "antd-mobile";
|
import { Card, Button } from "antd-mobile";
|
||||||
|
|
||||||
interface DataAnalysisPanelProps {
|
interface DataAnalysisPanelProps {
|
||||||
stats: {
|
stats: {
|
||||||
total: number;
|
total: number;
|
||||||
highValue: number;
|
highValue: number;
|
||||||
added: number;
|
added: number;
|
||||||
pending: number;
|
pending: number;
|
||||||
failed: number;
|
failed: number;
|
||||||
addSuccessRate: number;
|
addSuccessRate: number;
|
||||||
};
|
};
|
||||||
showStats: boolean;
|
showStats: boolean;
|
||||||
setShowStats: (v: boolean) => void;
|
setShowStats: (v: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataAnalysisPanel: React.FC<DataAnalysisPanelProps> = ({
|
const DataAnalysisPanel: React.FC<DataAnalysisPanelProps> = ({
|
||||||
stats,
|
stats,
|
||||||
showStats,
|
showStats,
|
||||||
setShowStats,
|
setShowStats,
|
||||||
}) => {
|
}) => {
|
||||||
if (!showStats) return null;
|
if (!showStats) return null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
background: "#fff",
|
background: "#fff",
|
||||||
padding: "16px",
|
padding: "16px",
|
||||||
margin: "8px 0",
|
margin: "8px 0",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
boxShadow: "0 2px 8px rgba(0,0,0,0.04)",
|
boxShadow: "0 2px 8px rgba(0,0,0,0.04)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", gap: 16, marginBottom: 12 }}>
|
<div style={{ display: "flex", gap: 16, marginBottom: 12 }}>
|
||||||
<Card style={{ flex: 1 }}>
|
<Card style={{ flex: 1 }}>
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#1677ff" }}>
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#1677ff" }}>
|
||||||
{stats.total}
|
{stats.total}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>总用户数</div>
|
<div style={{ fontSize: 13, color: "#888" }}>总用户数</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card style={{ flex: 1 }}>
|
<Card style={{ flex: 1 }}>
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#eb2f96" }}>
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#eb2f96" }}>
|
||||||
{stats.highValue}
|
{stats.highValue}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>高价值用户</div>
|
<div style={{ fontSize: 13, color: "#888" }}>高价值用户</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", gap: 16 }}>
|
<div style={{ display: "flex", gap: 16 }}>
|
||||||
<Card style={{ flex: 1 }}>
|
<Card style={{ flex: 1 }}>
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#52c41a" }}>
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#52c41a" }}>
|
||||||
{stats.addSuccessRate}%
|
{stats.addSuccessRate}%
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>添加成功率</div>
|
<div style={{ fontSize: 13, color: "#888" }}>添加成功率</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card style={{ flex: 1 }}>
|
<Card style={{ flex: 1 }}>
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#faad14" }}>
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#faad14" }}>
|
||||||
{stats.added}
|
{stats.added}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>已添加</div>
|
<div style={{ fontSize: 13, color: "#888" }}>已添加</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card style={{ flex: 1 }}>
|
<Card style={{ flex: 1 }}>
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#bfbfbf" }}>
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#bfbfbf" }}>
|
||||||
{stats.pending}
|
{stats.pending}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>待添加</div>
|
<div style={{ fontSize: 13, color: "#888" }}>待添加</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card style={{ flex: 1 }}>
|
<Card style={{ flex: 1 }}>
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#ff4d4f" }}>
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#ff4d4f" }}>
|
||||||
{stats.failed}
|
{stats.failed}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>添加失败</div>
|
<div style={{ fontSize: 13, color: "#888" }}>添加失败</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
style={{ marginTop: 12 }}
|
style={{ marginTop: 12 }}
|
||||||
onClick={() => setShowStats(false)}
|
onClick={() => setShowStats(false)}
|
||||||
>
|
>
|
||||||
收起分析
|
收起分析
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DataAnalysisPanel;
|
export default DataAnalysisPanel;
|
||||||
|
|||||||
@@ -1,118 +1,118 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Popup } from "antd-mobile";
|
import { Popup } from "antd-mobile";
|
||||||
import { Select, Button } from "antd";
|
import { Select, Button } from "antd";
|
||||||
import type {
|
import type {
|
||||||
DeviceOption,
|
DeviceOption,
|
||||||
PackageOption,
|
PackageOption,
|
||||||
ValueLevel,
|
ValueLevel,
|
||||||
UserStatus,
|
UserStatus,
|
||||||
} from "./data";
|
} from "./data";
|
||||||
|
|
||||||
interface FilterModalProps {
|
interface FilterModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
deviceOptions: DeviceOption[];
|
deviceOptions: DeviceOption[];
|
||||||
packageOptions: PackageOption[];
|
packageOptions: PackageOption[];
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
setDeviceId: (v: string) => void;
|
setDeviceId: (v: string) => void;
|
||||||
packageId: string;
|
packageId: string;
|
||||||
setPackageId: (v: string) => void;
|
setPackageId: (v: string) => void;
|
||||||
valueLevel: ValueLevel;
|
valueLevel: ValueLevel;
|
||||||
setValueLevel: (v: ValueLevel) => void;
|
setValueLevel: (v: ValueLevel) => void;
|
||||||
userStatus: UserStatus;
|
userStatus: UserStatus;
|
||||||
setUserStatus: (v: UserStatus) => void;
|
setUserStatus: (v: UserStatus) => void;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueLevelOptions = [
|
const valueLevelOptions = [
|
||||||
{ label: "全部价值", value: "all" },
|
{ label: "全部价值", value: "all" },
|
||||||
{ label: "高价值", value: "high" },
|
{ label: "高价值", value: "high" },
|
||||||
{ label: "中价值", value: "medium" },
|
{ label: "中价值", value: "medium" },
|
||||||
{ label: "低价值", value: "low" },
|
{ label: "低价值", value: "low" },
|
||||||
];
|
];
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
{ label: "全部状态", value: "all" },
|
{ label: "全部状态", value: "all" },
|
||||||
{ label: "已添加", value: "added" },
|
{ label: "已添加", value: "added" },
|
||||||
{ label: "待添加", value: "pending" },
|
{ label: "待添加", value: "pending" },
|
||||||
{ label: "添加失败", value: "failed" },
|
{ label: "添加失败", value: "failed" },
|
||||||
{ label: "重复", value: "duplicate" },
|
{ label: "重复", value: "duplicate" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const FilterModal: React.FC<FilterModalProps> = ({
|
const FilterModal: React.FC<FilterModalProps> = ({
|
||||||
visible,
|
visible,
|
||||||
onClose,
|
onClose,
|
||||||
deviceOptions,
|
deviceOptions,
|
||||||
packageOptions,
|
packageOptions,
|
||||||
deviceId,
|
deviceId,
|
||||||
setDeviceId,
|
setDeviceId,
|
||||||
packageId,
|
packageId,
|
||||||
setPackageId,
|
setPackageId,
|
||||||
valueLevel,
|
valueLevel,
|
||||||
setValueLevel,
|
setValueLevel,
|
||||||
userStatus,
|
userStatus,
|
||||||
setUserStatus,
|
setUserStatus,
|
||||||
onReset,
|
onReset,
|
||||||
}) => (
|
}) => (
|
||||||
<Popup
|
<Popup
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onMaskClick={onClose}
|
onMaskClick={onClose}
|
||||||
position="right"
|
position="right"
|
||||||
bodyStyle={{ width: "80vw", maxWidth: 360, padding: 24 }}
|
bodyStyle={{ width: "80vw", maxWidth: 360, padding: 24 }}
|
||||||
>
|
>
|
||||||
<div style={{ fontWeight: 600, fontSize: 18, marginBottom: 20 }}>
|
<div style={{ fontWeight: 600, fontSize: 18, marginBottom: 20 }}>
|
||||||
筛选选项
|
筛选选项
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginBottom: 20 }}>
|
<div style={{ marginBottom: 20 }}>
|
||||||
<div style={{ marginBottom: 6 }}>设备</div>
|
<div style={{ marginBottom: 6 }}>设备</div>
|
||||||
<Select
|
<Select
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
value={deviceId}
|
value={deviceId}
|
||||||
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>
|
||||||
<div style={{ marginBottom: 20 }}>
|
<div style={{ marginBottom: 20 }}>
|
||||||
<div style={{ marginBottom: 6 }}>流量池</div>
|
<div style={{ marginBottom: 6 }}>流量池</div>
|
||||||
<Select
|
<Select
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
value={packageId}
|
value={packageId}
|
||||||
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>
|
||||||
<div style={{ marginBottom: 20 }}>
|
<div style={{ marginBottom: 20 }}>
|
||||||
<div style={{ marginBottom: 6 }}>用户价值</div>
|
<div style={{ marginBottom: 6 }}>用户价值</div>
|
||||||
<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>
|
||||||
<div style={{ marginBottom: 20 }}>
|
<div style={{ marginBottom: 20 }}>
|
||||||
<div style={{ marginBottom: 6 }}>添加状态</div>
|
<div style={{ marginBottom: 6 }}>添加状态</div>
|
||||||
<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>
|
||||||
<div style={{ display: "flex", gap: 12, marginTop: 32 }}>
|
<div style={{ display: "flex", gap: 12, marginTop: 32 }}>
|
||||||
<Button onClick={onReset} style={{ flex: 1 }}>
|
<Button onClick={onReset} style={{ flex: 1 }}>
|
||||||
重置筛选
|
重置筛选
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="primary" onClick={onClose} style={{ flex: 1 }}>
|
<Button type="primary" onClick={onClose} style={{ flex: 1 }}>
|
||||||
应用筛选
|
应用筛选
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default FilterModal;
|
export default FilterModal;
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
// 流量池用户类型
|
// 流量池用户类型
|
||||||
export interface TrafficPoolUser {
|
export interface TrafficPoolUser {
|
||||||
id: number;
|
id: number;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
mobile: string;
|
mobile: string;
|
||||||
wechatId: string;
|
wechatId: string;
|
||||||
fromd: string;
|
fromd: string;
|
||||||
status: number;
|
status: number;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
companyId: number;
|
companyId: number;
|
||||||
sourceId: string;
|
sourceId: string;
|
||||||
type: number;
|
type: number;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
gender: number;
|
gender: number;
|
||||||
phone: string;
|
phone: string;
|
||||||
packages: string[];
|
packages: string[];
|
||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 列表响应类型
|
// 列表响应类型
|
||||||
export interface TrafficPoolUserListResponse {
|
export interface TrafficPoolUserListResponse {
|
||||||
list: TrafficPoolUser[];
|
list: TrafficPoolUser[];
|
||||||
total: number;
|
total: number;
|
||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设备类型
|
// 设备类型
|
||||||
export interface DeviceOption {
|
export interface DeviceOption {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分组类型
|
// 分组类型
|
||||||
export interface PackageOption {
|
export interface PackageOption {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户价值类型
|
// 用户价值类型
|
||||||
export type ValueLevel = "all" | "high" | "medium" | "low";
|
export type ValueLevel = "all" | "high" | "medium" | "low";
|
||||||
|
|
||||||
// 状态类型
|
// 状态类型
|
||||||
export type UserStatus = "all" | "added" | "pending" | "failed" | "duplicate";
|
export type UserStatus = "all" | "added" | "pending" | "failed" | "duplicate";
|
||||||
|
|||||||
@@ -1,160 +1,158 @@
|
|||||||
import { useState, useEffect, useMemo } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
fetchTrafficPoolList,
|
fetchTrafficPoolList,
|
||||||
fetchDeviceOptions,
|
fetchDeviceOptions,
|
||||||
fetchPackageOptions,
|
fetchPackageOptions,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
import type {
|
import type {
|
||||||
TrafficPoolUser,
|
TrafficPoolUser,
|
||||||
DeviceOption,
|
DeviceOption,
|
||||||
PackageOption,
|
PackageOption,
|
||||||
ValueLevel,
|
ValueLevel,
|
||||||
UserStatus,
|
UserStatus,
|
||||||
} from "./data";
|
} from "./data";
|
||||||
import { Toast } from "antd-mobile";
|
import { Toast } from "antd-mobile";
|
||||||
|
|
||||||
export function useTrafficPoolListLogic() {
|
export function useTrafficPoolListLogic() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [list, setList] = useState<TrafficPoolUser[]>([]);
|
const [list, setList] = useState<TrafficPoolUser[]>([]);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize] = useState(10);
|
const [pageSize] = useState(10);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
// 筛选相关
|
// 筛选相关
|
||||||
const [showFilter, setShowFilter] = useState(false);
|
const [showFilter, setShowFilter] = useState(false);
|
||||||
const [deviceOptions, setDeviceOptions] = useState<DeviceOption[]>([]);
|
const [deviceOptions, setDeviceOptions] = useState<DeviceOption[]>([]);
|
||||||
const [packageOptions, setPackageOptions] = useState<PackageOption[]>([]);
|
const [packageOptions, setPackageOptions] = useState<PackageOption[]>([]);
|
||||||
const [deviceId, setDeviceId] = useState<string>("all");
|
const [deviceId, setDeviceId] = useState<string>("all");
|
||||||
const [packageId, setPackageId] = useState<string>("all");
|
const [packageId, setPackageId] = useState<string>("all");
|
||||||
const [valueLevel, setValueLevel] = useState<ValueLevel>("all");
|
const [valueLevel, setValueLevel] = useState<ValueLevel>("all");
|
||||||
const [userStatus, setUserStatus] = useState<UserStatus>("all");
|
const [userStatus, setUserStatus] = useState<UserStatus>("all");
|
||||||
|
|
||||||
// 批量相关
|
// 批量相关
|
||||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||||
const [batchModal, setBatchModal] = useState(false);
|
const [batchModal, setBatchModal] = useState(false);
|
||||||
const [batchTarget, setBatchTarget] = useState<string>("");
|
const [batchTarget, setBatchTarget] = useState<string>("");
|
||||||
|
|
||||||
// 数据分析
|
// 数据分析
|
||||||
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 addSuccessRate = total ? Math.round((added / total) * 100) : 0;
|
||||||
const failed = list.filter((u) => u.status === -1).length;
|
return { total, highValue, added, pending, failed, addSuccessRate };
|
||||||
const addSuccessRate = total ? Math.round((added / total) * 100) : 0;
|
}, [list]);
|
||||||
return { total, highValue, added, pending, failed, addSuccessRate };
|
|
||||||
}, [list]);
|
// 获取列表
|
||||||
|
const getList = async () => {
|
||||||
// 获取列表
|
setLoading(true);
|
||||||
const getList = async () => {
|
try {
|
||||||
setLoading(true);
|
const res = await fetchTrafficPoolList({
|
||||||
try {
|
page,
|
||||||
const res = await fetchTrafficPoolList({
|
pageSize,
|
||||||
page,
|
keyword: search,
|
||||||
pageSize,
|
// deviceId,
|
||||||
keyword: search,
|
// packageId,
|
||||||
// deviceId,
|
// valueLevel,
|
||||||
// packageId,
|
// userStatus,
|
||||||
// valueLevel,
|
});
|
||||||
// userStatus,
|
setList(res.list || []);
|
||||||
});
|
setTotal(res.total || 0);
|
||||||
setList(res.list || []);
|
} finally {
|
||||||
setTotal(res.total || 0);
|
setLoading(false);
|
||||||
} finally {
|
}
|
||||||
setLoading(false);
|
};
|
||||||
}
|
|
||||||
};
|
// 获取筛选项
|
||||||
|
useEffect(() => {
|
||||||
// 获取筛选项
|
fetchDeviceOptions().then(setDeviceOptions);
|
||||||
useEffect(() => {
|
fetchPackageOptions().then(setPackageOptions);
|
||||||
fetchDeviceOptions().then(setDeviceOptions);
|
}, []);
|
||||||
fetchPackageOptions().then(setPackageOptions);
|
|
||||||
}, []);
|
// 筛选条件变化时刷新列表
|
||||||
|
useEffect(() => {
|
||||||
// 筛选条件变化时刷新列表
|
getList();
|
||||||
useEffect(() => {
|
// eslint-disable-next-line
|
||||||
getList();
|
}, [page, search /*, deviceId, packageId, valueLevel, userStatus*/]);
|
||||||
// eslint-disable-next-line
|
|
||||||
}, [page, search /*, deviceId, packageId, valueLevel, userStatus*/]);
|
// 全选/反选
|
||||||
|
const handleSelectAll = (checked: boolean) => {
|
||||||
// 全选/反选
|
if (checked) {
|
||||||
const handleSelectAll = (checked: boolean) => {
|
setSelectedIds(list.map(item => item.id));
|
||||||
if (checked) {
|
} else {
|
||||||
setSelectedIds(list.map((item) => item.id));
|
setSelectedIds([]);
|
||||||
} else {
|
}
|
||||||
setSelectedIds([]);
|
};
|
||||||
}
|
// 单选
|
||||||
};
|
const handleSelect = (id: number, checked: boolean) => {
|
||||||
// 单选
|
setSelectedIds(prev =>
|
||||||
const handleSelect = (id: number, checked: boolean) => {
|
checked ? [...prev, id] : prev.filter(i => i !== id)
|
||||||
setSelectedIds((prev) =>
|
);
|
||||||
checked ? [...prev, id] : prev.filter((i) => i !== id)
|
};
|
||||||
);
|
|
||||||
};
|
// 批量加入分组/流量池
|
||||||
|
const handleBatchAdd = () => {
|
||||||
// 批量加入分组/流量池
|
if (!batchTarget) {
|
||||||
const handleBatchAdd = () => {
|
Toast.show({ content: "请选择目标分组", position: "top" });
|
||||||
if (!batchTarget) {
|
return;
|
||||||
Toast.show({ content: "请选择目标分组", position: "top" });
|
}
|
||||||
return;
|
// TODO: 调用后端批量接口,这里仅模拟
|
||||||
}
|
Toast.show({
|
||||||
// TODO: 调用后端批量接口,这里仅模拟
|
content: `已将${selectedIds.length}个用户加入${packageOptions.find(p => p.id === batchTarget)?.name || ""}`,
|
||||||
Toast.show({
|
position: "top",
|
||||||
content: `已将${selectedIds.length}个用户加入${packageOptions.find((p) => p.id === batchTarget)?.name || ""}`,
|
});
|
||||||
position: "top",
|
setBatchModal(false);
|
||||||
});
|
setSelectedIds([]);
|
||||||
setBatchModal(false);
|
setBatchTarget("");
|
||||||
setSelectedIds([]);
|
// 可刷新列表
|
||||||
setBatchTarget("");
|
};
|
||||||
// 可刷新列表
|
|
||||||
};
|
// 筛选重置
|
||||||
|
const resetFilter = () => {
|
||||||
// 筛选重置
|
setDeviceId("all");
|
||||||
const resetFilter = () => {
|
setPackageId("all");
|
||||||
setDeviceId("all");
|
setValueLevel("all");
|
||||||
setPackageId("all");
|
setUserStatus("all");
|
||||||
setValueLevel("all");
|
};
|
||||||
setUserStatus("all");
|
|
||||||
};
|
return {
|
||||||
|
loading,
|
||||||
return {
|
list,
|
||||||
loading,
|
page,
|
||||||
list,
|
setPage,
|
||||||
page,
|
pageSize,
|
||||||
setPage,
|
total,
|
||||||
pageSize,
|
search,
|
||||||
total,
|
setSearch,
|
||||||
search,
|
showFilter,
|
||||||
setSearch,
|
setShowFilter,
|
||||||
showFilter,
|
deviceOptions,
|
||||||
setShowFilter,
|
packageOptions,
|
||||||
deviceOptions,
|
deviceId,
|
||||||
packageOptions,
|
setDeviceId,
|
||||||
deviceId,
|
packageId,
|
||||||
setDeviceId,
|
setPackageId,
|
||||||
packageId,
|
valueLevel,
|
||||||
setPackageId,
|
setValueLevel,
|
||||||
valueLevel,
|
userStatus,
|
||||||
setValueLevel,
|
setUserStatus,
|
||||||
userStatus,
|
selectedIds,
|
||||||
setUserStatus,
|
setSelectedIds,
|
||||||
selectedIds,
|
handleSelectAll,
|
||||||
setSelectedIds,
|
handleSelect,
|
||||||
handleSelectAll,
|
batchModal,
|
||||||
handleSelect,
|
setBatchModal,
|
||||||
batchModal,
|
batchTarget,
|
||||||
setBatchModal,
|
setBatchTarget,
|
||||||
batchTarget,
|
handleBatchAdd,
|
||||||
setBatchTarget,
|
showStats,
|
||||||
handleBatchAdd,
|
setShowStats,
|
||||||
showStats,
|
stats,
|
||||||
setShowStats,
|
getList,
|
||||||
stats,
|
resetFilter,
|
||||||
getList,
|
};
|
||||||
resetFilter,
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,115 +1,115 @@
|
|||||||
.user-set-page {
|
.user-set-page {
|
||||||
background: #f7f8fa;
|
background: #f7f8fa;
|
||||||
}
|
}
|
||||||
.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;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 24px 20px 20px 20px;
|
padding: 24px 20px 20px 20px;
|
||||||
}
|
}
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
margin-right: 22px;
|
margin-right: 22px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.avatar-placeholder {
|
.avatar-placeholder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
background: #e6f7ff;
|
background: #e6f7ff;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
.info-list {
|
.info-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
.info-item {
|
.info-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.label {
|
.label {
|
||||||
color: #888;
|
color: #888;
|
||||||
min-width: 70px;
|
min-width: 70px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
.value {
|
.value {
|
||||||
color: #222;
|
color: #222;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
.avatar-upload {
|
.avatar-upload {
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 64px;
|
width: 64px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
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%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
.avatar-edit {
|
.avatar-edit {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
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;
|
||||||
padding: 3px 0 2px 0;
|
padding: 3px 0 2px 0;
|
||||||
border-radius: 0 0 32px 32px;
|
border-radius: 0 0 32px 32px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.avatar-upload:hover .avatar-edit {
|
.avatar-upload:hover .avatar-edit {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
.edit-input {
|
.edit-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #e5e6eb;
|
border: 1px solid #e5e6eb;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
background: #fafbfc;
|
background: #fafbfc;
|
||||||
}
|
}
|
||||||
.save-btn {
|
.save-btn {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,113 +1,113 @@
|
|||||||
import React, { useRef, useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import { useUserStore } from "@/store/module/user";
|
import { useUserStore } from "@/store/module/user";
|
||||||
import { Card, Button, Input, Toast } from "antd-mobile";
|
import { Card, Button, Input, Toast } from "antd-mobile";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
import NavCommon from "@/components/NavCommon";
|
import NavCommon from "@/components/NavCommon";
|
||||||
|
|
||||||
const UserSetting: React.FC = () => {
|
const UserSetting: React.FC = () => {
|
||||||
const { user, setUser } = useUserStore();
|
const { user, setUser } = useUserStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [nickname, setNickname] = useState(user?.username || "");
|
const [nickname, setNickname] = useState(user?.username || "");
|
||||||
const [avatar, setAvatar] = useState(user?.avatar || "");
|
const [avatar, setAvatar] = useState(user?.avatar || "");
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// 头像上传
|
// 头像上传
|
||||||
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存
|
// 保存
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!nickname.trim()) {
|
if (!nickname.trim()) {
|
||||||
Toast.show({ content: "昵称不能为空", position: "top" });
|
Toast.show({ content: "昵称不能为空", position: "top" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
setUser({ ...user, id: user.id, username: nickname, avatar });
|
setUser({ ...user, id: user.id, username: nickname, avatar });
|
||||||
Toast.show({ content: "保存成功", position: "top" });
|
Toast.show({ content: "保存成功", position: "top" });
|
||||||
navigate(-1);
|
navigate(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
header={<NavCommon title="用户信息修改" />}
|
header={<NavCommon title="用户信息修改" />}
|
||||||
footer={
|
footer={
|
||||||
<div className={style["save-btn"]}>
|
<div className={style["save-btn"]}>
|
||||||
<Button
|
<Button
|
||||||
block
|
block
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
loading={uploading}
|
loading={uploading}
|
||||||
>
|
>
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={style["user-set-page"]}>
|
<div className={style["user-set-page"]}>
|
||||||
<Card className={style["user-card"]}>
|
<Card className={style["user-card"]}>
|
||||||
<div className={style["user-info"]}>
|
<div className={style["user-info"]}>
|
||||||
<div className={style["avatar"]}>
|
<div className={style["avatar"]}>
|
||||||
<div
|
<div
|
||||||
className={style["avatar-upload"]}
|
className={style["avatar-upload"]}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
>
|
>
|
||||||
{avatar ? (
|
{avatar ? (
|
||||||
<img src={avatar} alt="头像" />
|
<img src={avatar} alt="头像" />
|
||||||
) : (
|
) : (
|
||||||
<div className={style["avatar-placeholder"]}>卡</div>
|
<div className={style["avatar-placeholder"]}>卡</div>
|
||||||
)}
|
)}
|
||||||
<div className={style["avatar-edit"]}>更换头像</div>
|
<div className={style["avatar-edit"]}>更换头像</div>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
style={{ display: "none" }}
|
style={{ display: "none" }}
|
||||||
onChange={handleAvatarChange}
|
onChange={handleAvatarChange}
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["info-list"]}>
|
<div className={style["info-list"]}>
|
||||||
<div className={style["info-item"]}>
|
<div className={style["info-item"]}>
|
||||||
<span className={style["label"]}>昵称</span>
|
<span className={style["label"]}>昵称</span>
|
||||||
<Input
|
<Input
|
||||||
className={style["edit-input"]}
|
className={style["edit-input"]}
|
||||||
value={nickname}
|
value={nickname}
|
||||||
onChange={setNickname}
|
onChange={setNickname}
|
||||||
maxLength={12}
|
maxLength={12}
|
||||||
placeholder="请输入昵称"
|
placeholder="请输入昵称"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["info-item"]}>
|
<div className={style["info-item"]}>
|
||||||
<span className={style["label"]}>手机号</span>
|
<span className={style["label"]}>手机号</span>
|
||||||
<span className={style["value"]}>{user?.phone || "-"}</span>
|
<span className={style["value"]}>{user?.phone || "-"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["info-item"]}>
|
<div className={style["info-item"]}>
|
||||||
<span className={style["label"]}>账号</span>
|
<span className={style["label"]}>账号</span>
|
||||||
<span className={style["value"]}>{user?.account || "-"}</span>
|
<span className={style["value"]}>{user?.account || "-"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["info-item"]}>
|
<div className={style["info-item"]}>
|
||||||
<span className={style["label"]}>角色</span>
|
<span className={style["label"]}>角色</span>
|
||||||
<span className={style["value"]}>
|
<span className={style["value"]}>
|
||||||
{user?.isAdmin === 1 ? "管理员" : "普通用户"}
|
{user?.isAdmin === 1 ? "管理员" : "普通用户"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UserSetting;
|
export default UserSetting;
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import request from "@/api/request";
|
import request from "@/api/request";
|
||||||
|
|
||||||
// 获取微信号详情
|
// 获取微信号详情
|
||||||
export function getWechatAccountDetail(id: string) {
|
export function getWechatAccountDetail(id: string) {
|
||||||
return request("/v1/wechats/getWechatInfo", { wechatId: id }, "GET");
|
return request("/v1/wechats/getWechatInfo", { wechatId: id }, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取微信号好友列表
|
// 获取微信号好友列表
|
||||||
export function getWechatFriends(params: {
|
export function getWechatFriends(params: {
|
||||||
wechatAccount: string;
|
wechatAccount: string;
|
||||||
page: number;
|
page: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
}) {
|
}) {
|
||||||
return request(
|
return request(
|
||||||
`/v1/wechats/${params.wechatAccount}/friends`,
|
`/v1/wechats/${params.wechatAccount}/friends`,
|
||||||
{
|
{
|
||||||
page: params.page,
|
page: params.page,
|
||||||
limit: params.limit,
|
limit: params.limit,
|
||||||
keyword: params.keyword,
|
keyword: params.keyword,
|
||||||
},
|
},
|
||||||
"GET"
|
"GET"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取微信好友详情
|
// 获取微信好友详情
|
||||||
export function getWechatFriendDetail(id: string) {
|
export function getWechatFriendDetail(id: string) {
|
||||||
return request("/v1/WechatFriend/detail", { id }, "GET");
|
return request("/v1/WechatFriend/detail", { id }, "GET");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,54 @@
|
|||||||
export interface WechatAccountSummary {
|
export interface WechatAccountSummary {
|
||||||
accountAge: string;
|
accountAge: string;
|
||||||
activityLevel: {
|
activityLevel: {
|
||||||
allTimes: number;
|
allTimes: number;
|
||||||
dayTimes: number;
|
dayTimes: number;
|
||||||
};
|
};
|
||||||
accountWeight: {
|
accountWeight: {
|
||||||
scope: number;
|
scope: number;
|
||||||
ageWeight: number;
|
ageWeight: number;
|
||||||
activityWeigth: number;
|
activityWeigth: number;
|
||||||
restrictWeight: number;
|
restrictWeight: number;
|
||||||
realNameWeight: number;
|
realNameWeight: number;
|
||||||
};
|
};
|
||||||
statistics: {
|
statistics: {
|
||||||
todayAdded: number;
|
todayAdded: number;
|
||||||
addLimit: number;
|
addLimit: number;
|
||||||
};
|
};
|
||||||
restrictions: {
|
restrictions: {
|
||||||
id: number;
|
id: number;
|
||||||
level: string;
|
level: string;
|
||||||
reason: string;
|
reason: string;
|
||||||
date: string;
|
date: string;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Friend {
|
export interface Friend {
|
||||||
id: string;
|
id: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
wechatId: string;
|
wechatId: string;
|
||||||
remark: string;
|
remark: string;
|
||||||
addTime: string;
|
addTime: string;
|
||||||
lastInteraction: string;
|
lastInteraction: string;
|
||||||
tags: Array<{
|
tags: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
color: string;
|
color: string;
|
||||||
}>;
|
}>;
|
||||||
region: string;
|
region: string;
|
||||||
source: string;
|
source: string;
|
||||||
notes: string;
|
notes: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WechatFriendDetail {
|
export interface WechatFriendDetail {
|
||||||
id: number;
|
id: number;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
region: string;
|
region: string;
|
||||||
wechatId: string;
|
wechatId: string;
|
||||||
addDate: string;
|
addDate: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
memo: string;
|
memo: string;
|
||||||
source: string;
|
source: string;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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"]}
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
import request from "@/api/request";
|
import request from "@/api/request";
|
||||||
|
|
||||||
// 获取微信号列表
|
// 获取微信号列表
|
||||||
export function getWechatAccounts(params: {
|
export function getWechatAccounts(params: {
|
||||||
page: number;
|
page: number;
|
||||||
page_size: number;
|
page_size: number;
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
}) {
|
}) {
|
||||||
return request("v1/wechats", params, "GET");
|
return request("v1/wechats", params, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取微信号详情
|
// 获取微信号详情
|
||||||
export function getWechatAccountDetail(id: string) {
|
export function getWechatAccountDetail(id: string) {
|
||||||
return request("v1/WechatAccount/detail", { id }, "GET");
|
return request("v1/WechatAccount/detail", { id }, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取微信号好友列表
|
// 获取微信号好友列表
|
||||||
export function getWechatFriends(params: {
|
export function getWechatFriends(params: {
|
||||||
wechatAccountKeyword: string;
|
wechatAccountKeyword: string;
|
||||||
pageIndex: number;
|
pageIndex: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
friendKeyword?: string;
|
friendKeyword?: string;
|
||||||
}) {
|
}) {
|
||||||
return request("v1/WechatFriend/friendlistData", params, "POST");
|
return request("v1/WechatFriend/friendlistData", params, "POST");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取微信好友详情
|
// 获取微信好友详情
|
||||||
export function getWechatFriendDetail(id: string) {
|
export function getWechatFriendDetail(id: string) {
|
||||||
return request("v1/WechatFriend/detail", { id }, "GET");
|
return request("v1/WechatFriend/detail", { id }, "GET");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,171 +1,171 @@
|
|||||||
.wechat-accounts-page {
|
.wechat-accounts-page {
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-title {
|
.nav-title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #222;
|
color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-list {
|
.card-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.avatar-wrapper {
|
.avatar-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 3px solid #e6f0fa;
|
border: 3px solid #e6f0fa;
|
||||||
box-shadow: 0 0 0 2px #1677ff33;
|
box-shadow: 0 0 0 2px #1677ff33;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
.status-dot-normal {
|
.status-dot-normal {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -2px;
|
right: -2px;
|
||||||
bottom: -2px;
|
bottom: -2px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
background: #52c41a;
|
background: #52c41a;
|
||||||
border: 2px solid #fff;
|
border: 2px solid #fff;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
.status-dot-abnormal {
|
.status-dot-abnormal {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -2px;
|
right: -2px;
|
||||||
bottom: -2px;
|
bottom: -2px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
background: #ff4d4f;
|
background: #ff4d4f;
|
||||||
border: 2px solid #fff;
|
border: 2px solid #fff;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
.header-info {
|
.header-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.nickname-row {
|
.nickname-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.nickname {
|
.nickname {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
max-width: 120px;
|
max-width: 120px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.status-label-normal {
|
.status-label-normal {
|
||||||
background: #e6fffb;
|
background: #e6fffb;
|
||||||
color: #13c2c2;
|
color: #13c2c2;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
.status-label-abnormal {
|
.status-label-abnormal {
|
||||||
background: #fff1f0;
|
background: #fff1f0;
|
||||||
color: #ff4d4f;
|
color: #ff4d4f;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
.wechat-id {
|
.wechat-id {
|
||||||
color: #888;
|
color: #888;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
.card-action {
|
.card-action {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
.card-body {
|
.card-body {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
.row-group {
|
.row-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.row-item {
|
.row-item {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #555;
|
color: #555;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
.strong {
|
.strong {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #222;
|
color: #222;
|
||||||
}
|
}
|
||||||
.strong-green {
|
.strong-green {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #52c41a;
|
color: #52c41a;
|
||||||
}
|
}
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
margin: 6px 0 8px 0;
|
margin: 6px 0 8px 0;
|
||||||
}
|
}
|
||||||
.progress-bg {
|
.progress-bg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.progress-fill {
|
.progress-fill {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
background: linear-gradient(90deg, #1677ff 0%, #69c0ff 100%);
|
background: linear-gradient(90deg, #1677ff 0%, #69c0ff 100%);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition: width 0.3s;
|
transition: width 0.3s;
|
||||||
}
|
}
|
||||||
.pagination {
|
.pagination {
|
||||||
margin: 16px 0 0 0;
|
margin: 16px 0 0 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.popup-content {
|
.popup-content {
|
||||||
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;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
}
|
}
|
||||||
.empty {
|
.empty {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #999;
|
color: #999;
|
||||||
padding: 48px 0 32px 0;
|
padding: 48px 0 32px 0;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
background: var(--primary-gradient);
|
background: var(--primary-gradient);
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: 0 2px 8px var(--primary-shadow);
|
box-shadow: 0 2px 8px var(--primary-shadow);
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: translateY(1px);
|
transform: translateY(1px);
|
||||||
box-shadow: 0 1px 4px var(--primary-shadow);
|
box-shadow: 0 1px 4px var(--primary-shadow);
|
||||||
@@ -132,11 +132,11 @@
|
|||||||
.scenario-item {
|
.scenario-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
@@ -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,34 +260,33 @@
|
|||||||
|
|
||||||
// 响应式设计
|
// 响应式设计
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
|
|
||||||
.scenario-card {
|
.scenario-card {
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
min-height: 70px;
|
min-height: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenario-icon {
|
.scenario-icon {
|
||||||
width: 46px;
|
width: 46px;
|
||||||
height: 46px;
|
height: 46px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenario-image {
|
.scenario-image {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenario-name {
|
.scenario-name {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-text {
|
.stat-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenario-growth {
|
.scenario-growth {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.growth-icon {
|
.growth-icon {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,32 +1,32 @@
|
|||||||
import request from "@/api/request";
|
import request from "@/api/request";
|
||||||
import { PlanDetail, PlanListResponse, ApiResponse } from "./data";
|
import { PlanDetail, PlanListResponse, ApiResponse } from "./data";
|
||||||
|
|
||||||
// ==================== 计划相关接口 ====================
|
// ==================== 计划相关接口 ====================
|
||||||
// 获取计划列表
|
// 获取计划列表
|
||||||
export function getPlanList(params: {
|
export function getPlanList(params: {
|
||||||
sceneId: string;
|
sceneId: string;
|
||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
}): Promise<PlanListResponse> {
|
}): Promise<PlanListResponse> {
|
||||||
return request(`/v1/plan/list`, params, "GET");
|
return request(`/v1/plan/list`, params, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取计划详情
|
// 获取计划详情
|
||||||
export function getPlanDetail(planId: string): Promise<PlanDetail> {
|
export function getPlanDetail(planId: string): Promise<PlanDetail> {
|
||||||
return request(`/v1/plan/detail`, { planId }, "GET");
|
return request(`/v1/plan/detail`, { planId }, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制计划
|
// 复制计划
|
||||||
export function copyPlan(planId: string): Promise<ApiResponse<any>> {
|
export function copyPlan(planId: string): Promise<ApiResponse<any>> {
|
||||||
return request(`/v1/plan/copy`, { planId }, "GET");
|
return request(`/v1/plan/copy`, { planId }, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除计划
|
// 删除计划
|
||||||
export function deletePlan(planId: string): Promise<ApiResponse<any>> {
|
export function deletePlan(planId: string): Promise<ApiResponse<any>> {
|
||||||
return request(`/v1/plan/delete`, { planId }, "DELETE");
|
return request(`/v1/plan/delete`, { planId }, "DELETE");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取小程序二维码
|
// 获取小程序二维码
|
||||||
export function getWxMinAppCode(planId: string): Promise<ApiResponse<string>> {
|
export function getWxMinAppCode(planId: string): Promise<ApiResponse<string>> {
|
||||||
return request(`/v1/plan/getWxMinAppCode`, { taskId: planId }, "GET");
|
return request(`/v1/plan/getWxMinAppCode`, { taskId: planId }, "GET");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,59 @@
|
|||||||
export interface Task {
|
export interface Task {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
status: number;
|
status: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
total_customers?: number;
|
total_customers?: number;
|
||||||
today_customers?: number;
|
today_customers?: number;
|
||||||
lastUpdated?: string;
|
lastUpdated?: string;
|
||||||
stats?: {
|
stats?: {
|
||||||
devices?: number;
|
devices?: number;
|
||||||
acquired?: number;
|
acquired?: number;
|
||||||
added?: number;
|
added?: number;
|
||||||
};
|
};
|
||||||
reqConf?: {
|
reqConf?: {
|
||||||
device?: string[];
|
device?: string[];
|
||||||
selectedDevices?: string[];
|
selectedDevices?: string[];
|
||||||
};
|
};
|
||||||
acquiredCount?: number;
|
acquiredCount?: number;
|
||||||
addedCount?: number;
|
addedCount?: number;
|
||||||
passRate?: number;
|
passRate?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiSettings {
|
export interface ApiSettings {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
webhookUrl: string;
|
webhookUrl: string;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API响应相关类型
|
// API响应相关类型
|
||||||
export interface TextUrl {
|
export interface TextUrl {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
originalString?: string;
|
originalString?: string;
|
||||||
sign?: string;
|
sign?: string;
|
||||||
fullUrl: string;
|
fullUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlanDetail {
|
export interface PlanDetail {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
scenario: number;
|
scenario: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
status: number;
|
status: number;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
textUrl: TextUrl;
|
textUrl: TextUrl;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiResponse<T> {
|
export interface ApiResponse<T> {
|
||||||
code: number;
|
code: number;
|
||||||
msg?: string;
|
msg?: string;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlanListResponse {
|
export interface PlanListResponse {
|
||||||
list: Task[];
|
list: Task[];
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,383 +1,382 @@
|
|||||||
.scenario-list-page {
|
.scenario-list-page {
|
||||||
padding:0 16px;
|
padding: 0 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 60vh;
|
height: 60vh;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-text {
|
.loading-text {
|
||||||
color: #666;
|
color: #666;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input-wrapper {
|
.search-input-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
.ant-input {
|
.ant-input {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plan-list {
|
||||||
.plan-list {
|
display: flex;
|
||||||
display: flex;
|
flex-direction: column;
|
||||||
flex-direction: column;
|
gap: 12px;
|
||||||
gap: 12px;
|
}
|
||||||
}
|
|
||||||
|
.plan-item {
|
||||||
.plan-item {
|
background: white;
|
||||||
background: white;
|
border-radius: 12px;
|
||||||
border-radius: 12px;
|
padding: 16px;
|
||||||
padding: 16px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
transition: all 0.2s ease;
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
&:hover {
|
||||||
&:hover {
|
transform: translateY(-2px);
|
||||||
transform: translateY(-2px);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
.plan-header {
|
||||||
.plan-header {
|
display: flex;
|
||||||
display: flex;
|
justify-content: space-between;
|
||||||
justify-content: space-between;
|
align-items: center;
|
||||||
align-items: center;
|
margin-bottom: 16px;
|
||||||
margin-bottom: 16px;
|
}
|
||||||
}
|
|
||||||
|
.plan-name {
|
||||||
.plan-name {
|
font-size: 16px;
|
||||||
font-size: 16px;
|
font-weight: 600;
|
||||||
font-weight: 600;
|
color: #333;
|
||||||
color: #333;
|
flex: 1;
|
||||||
flex: 1;
|
margin-right: 12px;
|
||||||
margin-right: 12px;
|
}
|
||||||
}
|
|
||||||
|
.plan-header-right {
|
||||||
.plan-header-right {
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
gap: 8px;
|
||||||
gap: 8px;
|
}
|
||||||
}
|
|
||||||
|
.more-btn {
|
||||||
.more-btn {
|
padding: 4px;
|
||||||
padding: 4px;
|
min-width: auto;
|
||||||
min-width: auto;
|
height: 28px;
|
||||||
height: 28px;
|
width: 28px;
|
||||||
width: 28px;
|
border-radius: 4px;
|
||||||
border-radius: 4px;
|
|
||||||
|
&:hover {
|
||||||
&:hover {
|
background-color: #f5f5f5;
|
||||||
background-color: #f5f5f5;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
.stats-grid {
|
||||||
.stats-grid {
|
display: grid;
|
||||||
display: grid;
|
grid-template-columns: 1fr 1fr;
|
||||||
grid-template-columns: 1fr 1fr;
|
gap: 12px;
|
||||||
gap: 12px;
|
margin-bottom: 16px;
|
||||||
margin-bottom: 16px;
|
}
|
||||||
}
|
|
||||||
|
.stat-item {
|
||||||
.stat-item {
|
background: #f8f9fa;
|
||||||
background: #f8f9fa;
|
border-radius: 8px;
|
||||||
border-radius: 8px;
|
padding: 12px;
|
||||||
padding: 12px;
|
text-align: center;
|
||||||
text-align: center;
|
border: 1px solid #e9ecef;
|
||||||
border: 1px solid #e9ecef;
|
}
|
||||||
}
|
|
||||||
|
.stat-label {
|
||||||
.stat-label {
|
font-size: 12px;
|
||||||
font-size: 12px;
|
color: #666;
|
||||||
color: #666;
|
margin-bottom: 4px;
|
||||||
margin-bottom: 4px;
|
font-weight: 500;
|
||||||
font-weight: 500;
|
}
|
||||||
}
|
|
||||||
|
.stat-value {
|
||||||
.stat-value {
|
font-size: 18px;
|
||||||
font-size: 18px;
|
font-weight: 600;
|
||||||
font-weight: 600;
|
color: #333;
|
||||||
color: #333;
|
line-height: 1.2;
|
||||||
line-height: 1.2;
|
}
|
||||||
}
|
|
||||||
|
.plan-footer {
|
||||||
.plan-footer {
|
border-top: 1px solid #f0f0f0;
|
||||||
border-top: 1px solid #f0f0f0;
|
padding-top: 12px;
|
||||||
padding-top: 12px;
|
}
|
||||||
}
|
|
||||||
|
.last-execution {
|
||||||
.last-execution {
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
gap: 6px;
|
||||||
gap: 6px;
|
font-size: 12px;
|
||||||
font-size: 12px;
|
color: #999;
|
||||||
color: #999;
|
|
||||||
|
svg {
|
||||||
svg {
|
font-size: 14px;
|
||||||
font-size: 14px;
|
color: #999;
|
||||||
color: #999;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
.empty-state {
|
||||||
.empty-state {
|
display: flex;
|
||||||
display: flex;
|
flex-direction: column;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
justify-content: center;
|
padding: 60px 20px;
|
||||||
padding: 60px 20px;
|
text-align: center;
|
||||||
text-align: center;
|
}
|
||||||
}
|
|
||||||
|
.empty-text {
|
||||||
.empty-text {
|
color: #999;
|
||||||
color: #999;
|
font-size: 14px;
|
||||||
font-size: 14px;
|
margin-bottom: 20px;
|
||||||
margin-bottom: 20px;
|
}
|
||||||
}
|
|
||||||
|
.create-first-btn {
|
||||||
.create-first-btn {
|
height: 40px;
|
||||||
height: 40px;
|
padding: 0 24px;
|
||||||
padding: 0 24px;
|
border-radius: 20px;
|
||||||
border-radius: 20px;
|
}
|
||||||
}
|
|
||||||
|
// 加载更多按钮样式
|
||||||
// 加载更多按钮样式
|
.load-more-container {
|
||||||
.load-more-container {
|
display: flex;
|
||||||
display: flex;
|
justify-content: center;
|
||||||
justify-content: center;
|
padding: 20px 0;
|
||||||
padding: 20px 0;
|
}
|
||||||
}
|
|
||||||
|
.load-more-btn {
|
||||||
.load-more-btn {
|
height: 44px;
|
||||||
height: 44px;
|
padding: 0 32px;
|
||||||
padding: 0 32px;
|
border-radius: 22px;
|
||||||
border-radius: 22px;
|
font-size: 16px;
|
||||||
font-size: 16px;
|
font-weight: 500;
|
||||||
font-weight: 500;
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
gap: 8px;
|
||||||
gap: 8px;
|
transition: all 0.2s ease;
|
||||||
transition: all 0.2s ease;
|
|
||||||
|
&:hover {
|
||||||
&:hover {
|
transform: translateY(-1px);
|
||||||
transform: translateY(-1px);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
}
|
||||||
}
|
|
||||||
|
&:active {
|
||||||
&:active {
|
transform: translateY(0);
|
||||||
transform: translateY(0);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// 没有更多数据提示样式
|
||||||
// 没有更多数据提示样式
|
.no-more-data {
|
||||||
.no-more-data {
|
display: flex;
|
||||||
display: flex;
|
justify-content: center;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
align-items: center;
|
padding: 20px 0;
|
||||||
padding: 20px 0;
|
color: #999;
|
||||||
color: #999;
|
font-size: 14px;
|
||||||
font-size: 14px;
|
|
||||||
|
span {
|
||||||
span {
|
position: relative;
|
||||||
position: relative;
|
padding: 0 20px;
|
||||||
padding: 0 20px;
|
|
||||||
|
&::before,
|
||||||
&::before,
|
&::after {
|
||||||
&::after {
|
content: "";
|
||||||
content: '';
|
position: absolute;
|
||||||
position: absolute;
|
top: 50%;
|
||||||
top: 50%;
|
width: 40px;
|
||||||
width: 40px;
|
height: 1px;
|
||||||
height: 1px;
|
background-color: #e0e0e0;
|
||||||
background-color: #e0e0e0;
|
}
|
||||||
}
|
|
||||||
|
&::before {
|
||||||
&::before {
|
left: -50px;
|
||||||
left: -50px;
|
}
|
||||||
}
|
|
||||||
|
&::after {
|
||||||
&::after {
|
right: -50px;
|
||||||
right: -50px;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
.action-menu-dialog {
|
||||||
.action-menu-dialog {
|
background: white;
|
||||||
background: white;
|
border-radius: 16px 16px 0 0;
|
||||||
border-radius: 16px 16px 0 0;
|
padding: 20px;
|
||||||
padding: 20px;
|
max-height: 60vh;
|
||||||
max-height: 60vh;
|
display: flex;
|
||||||
display: flex;
|
flex-direction: column;
|
||||||
flex-direction: column;
|
}
|
||||||
}
|
|
||||||
|
.action-menu-item {
|
||||||
.action-menu-item {
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
gap: 12px;
|
||||||
gap: 12px;
|
padding: 16px;
|
||||||
padding: 16px;
|
border-radius: 8px;
|
||||||
border-radius: 8px;
|
cursor: pointer;
|
||||||
cursor: pointer;
|
transition: background-color 0.2s ease;
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
|
&:hover {
|
||||||
&:hover {
|
background-color: #f5f5f5;
|
||||||
background-color: #f5f5f5;
|
}
|
||||||
}
|
|
||||||
|
&.danger {
|
||||||
&.danger {
|
color: #ff4d4f;
|
||||||
color: #ff4d4f;
|
|
||||||
|
&:hover {
|
||||||
&:hover {
|
background-color: #fff2f0;
|
||||||
background-color: #fff2f0;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
.action-icon {
|
||||||
.action-icon {
|
font-size: 16px;
|
||||||
font-size: 16px;
|
width: 20px;
|
||||||
width: 20px;
|
text-align: center;
|
||||||
text-align: center;
|
}
|
||||||
}
|
|
||||||
|
.action-text {
|
||||||
.action-text {
|
font-size: 16px;
|
||||||
font-size: 16px;
|
font-weight: 500;
|
||||||
font-weight: 500;
|
}
|
||||||
}
|
|
||||||
|
.dialog-header {
|
||||||
.dialog-header {
|
display: flex;
|
||||||
display: flex;
|
justify-content: space-between;
|
||||||
justify-content: space-between;
|
align-items: center;
|
||||||
align-items: center;
|
margin-bottom: 20px;
|
||||||
margin-bottom: 20px;
|
padding-bottom: 16px;
|
||||||
padding-bottom: 16px;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
|
h3 {
|
||||||
h3 {
|
margin: 0;
|
||||||
margin: 0;
|
font-size: 18px;
|
||||||
font-size: 18px;
|
font-weight: 600;
|
||||||
font-weight: 600;
|
color: #333;
|
||||||
color: #333;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
.dialog-content {
|
||||||
.dialog-content {
|
flex: 1;
|
||||||
flex: 1;
|
text-align: center;
|
||||||
text-align: center;
|
display: flex;
|
||||||
display: flex;
|
flex-direction: column;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
justify-content: center;
|
}
|
||||||
}
|
|
||||||
|
.qr-dialog {
|
||||||
.qr-dialog {
|
background: white;
|
||||||
background: white;
|
border-radius: 16px;
|
||||||
border-radius: 16px;
|
padding: 20px;
|
||||||
padding: 20px;
|
width: 100%;
|
||||||
width: 100%;
|
}
|
||||||
}
|
|
||||||
|
.qr-loading {
|
||||||
.qr-loading {
|
display: flex;
|
||||||
display: flex;
|
flex-direction: column;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
justify-content: center;
|
padding: 40px 20px;
|
||||||
padding: 40px 20px;
|
gap: 16px;
|
||||||
gap: 16px;
|
color: #666;
|
||||||
color: #666;
|
font-size: 14px;
|
||||||
font-size: 14px;
|
}
|
||||||
}
|
|
||||||
|
.qr-image {
|
||||||
.qr-image {
|
width: 100%;
|
||||||
width: 100%;
|
max-width: 200px;
|
||||||
max-width: 200px;
|
height: auto;
|
||||||
height: auto;
|
border-radius: 8px;
|
||||||
border-radius: 8px;
|
}
|
||||||
}
|
|
||||||
|
.qr-error {
|
||||||
.qr-error {
|
text-align: center;
|
||||||
text-align: center;
|
color: #ff4d4f;
|
||||||
color: #ff4d4f;
|
font-size: 14px;
|
||||||
font-size: 14px;
|
padding: 40px 20px;
|
||||||
padding: 40px 20px;
|
}
|
||||||
}
|
|
||||||
|
.qr-link-section {
|
||||||
.qr-link-section {
|
margin-top: 20px;
|
||||||
margin-top: 20px;
|
width: 100%;
|
||||||
width: 100%;
|
padding: 0 10px;
|
||||||
padding: 0 10px;
|
}
|
||||||
}
|
|
||||||
|
.link-label {
|
||||||
.link-label {
|
font-size: 14px;
|
||||||
font-size: 14px;
|
font-weight: 500;
|
||||||
font-weight: 500;
|
color: #333;
|
||||||
color: #333;
|
margin-bottom: 8px;
|
||||||
margin-bottom: 8px;
|
text-align: left;
|
||||||
text-align: left;
|
}
|
||||||
}
|
|
||||||
|
.link-input-wrapper {
|
||||||
.link-input-wrapper {
|
display: flex;
|
||||||
display: flex;
|
gap: 8px;
|
||||||
gap: 8px;
|
align-items: center;
|
||||||
align-items: center;
|
width: 100%;
|
||||||
width: 100%;
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
@media (max-width: 480px) {
|
flex-direction: column;
|
||||||
flex-direction: column;
|
gap: 12px;
|
||||||
gap: 12px;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
.link-input {
|
||||||
.link-input {
|
flex: 1;
|
||||||
flex: 1;
|
|
||||||
|
.ant-input {
|
||||||
.ant-input {
|
border-radius: 8px;
|
||||||
border-radius: 8px;
|
font-size: 12px;
|
||||||
font-size: 12px;
|
color: #666;
|
||||||
color: #666;
|
background-color: #f8f9fa;
|
||||||
background-color: #f8f9fa;
|
border: 1px solid #e9ecef;
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
|
&:focus {
|
||||||
&:focus {
|
border-color: #1890ff;
|
||||||
border-color: #1890ff;
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
@media (max-width: 480px) {
|
width: 100%;
|
||||||
width: 100%;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
.copy-button {
|
||||||
.copy-button {
|
height: 32px;
|
||||||
height: 32px;
|
padding: 0 12px;
|
||||||
padding: 0 12px;
|
border-radius: 8px;
|
||||||
border-radius: 8px;
|
font-size: 12px;
|
||||||
font-size: 12px;
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
gap: 4px;
|
||||||
gap: 4px;
|
white-space: nowrap;
|
||||||
white-space: nowrap;
|
flex-shrink: 0;
|
||||||
flex-shrink: 0;
|
|
||||||
|
.anticon {
|
||||||
.anticon {
|
font-size: 12px;
|
||||||
font-size: 12px;
|
}
|
||||||
}
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
@media (max-width: 480px) {
|
width: 100%;
|
||||||
width: 100%;
|
justify-content: center;
|
||||||
justify-content: center;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -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"] : ""}`}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,437 +1,437 @@
|
|||||||
import React, { useState, useMemo } from "react";
|
import React, { useState, useMemo } from "react";
|
||||||
import { Popup, Button, Toast, SpinLoading } from "antd-mobile";
|
import { Popup, Button, Toast, SpinLoading } from "antd-mobile";
|
||||||
import { Modal, Input, Tabs, Card, Tag, Space } from "antd";
|
import { Modal, Input, Tabs, Card, Tag, Space } from "antd";
|
||||||
import {
|
import {
|
||||||
CopyOutlined,
|
CopyOutlined,
|
||||||
CodeOutlined,
|
CodeOutlined,
|
||||||
BookOutlined,
|
BookOutlined,
|
||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
LinkOutlined,
|
LinkOutlined,
|
||||||
SafetyOutlined,
|
SafetyOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import style from "./planApi.module.scss";
|
import style from "./planApi.module.scss";
|
||||||
import { buildApiUrl } from "@/utils/apiUrl";
|
import { buildApiUrl } from "@/utils/apiUrl";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计划接口配置弹窗组件
|
* 计划接口配置弹窗组件
|
||||||
*
|
*
|
||||||
* 使用示例:
|
* 使用示例:
|
||||||
* ```tsx
|
* ```tsx
|
||||||
* const [showApiDialog, setShowApiDialog] = useState(false);
|
* const [showApiDialog, setShowApiDialog] = useState(false);
|
||||||
* const [apiSettings, setApiSettings] = useState({
|
* const [apiSettings, setApiSettings] = useState({
|
||||||
* apiKey: "your-api-key",
|
* apiKey: "your-api-key",
|
||||||
* webhookUrl: "https://api.example.com/webhook",
|
* webhookUrl: "https://api.example.com/webhook",
|
||||||
* taskId: "task-123"
|
* taskId: "task-123"
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
* <PlanApi
|
* <PlanApi
|
||||||
* visible={showApiDialog}
|
* visible={showApiDialog}
|
||||||
* onClose={() => setShowApiDialog(false)}
|
* onClose={() => setShowApiDialog(false)}
|
||||||
* apiKey={apiSettings.apiKey}
|
* apiKey={apiSettings.apiKey}
|
||||||
* webhookUrl={apiSettings.webhookUrl}
|
* webhookUrl={apiSettings.webhookUrl}
|
||||||
* taskId={apiSettings.taskId}
|
* taskId={apiSettings.taskId}
|
||||||
* />
|
* />
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* 特性:
|
* 特性:
|
||||||
* - 移动端使用 Popup,PC端使用 Modal
|
* - 移动端使用 Popup,PC端使用 Modal
|
||||||
* - 支持四个标签页:接口配置、快速测试、开发文档、代码示例
|
* - 支持四个标签页:接口配置、快速测试、开发文档、代码示例
|
||||||
* - 支持多种编程语言的代码示例
|
* - 支持多种编程语言的代码示例
|
||||||
* - 响应式设计,自适应不同屏幕尺寸
|
* - 响应式设计,自适应不同屏幕尺寸
|
||||||
* - 支持暗色主题
|
* - 支持暗色主题
|
||||||
* - 自动拼接API地址前缀
|
* - 自动拼接API地址前缀
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface PlanApiProps {
|
interface PlanApiProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
webhookUrl: string;
|
webhookUrl: string;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApiSettings {
|
interface ApiSettings {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
webhookUrl: string;
|
webhookUrl: string;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlanApi: React.FC<PlanApiProps> = ({
|
const PlanApi: React.FC<PlanApiProps> = ({
|
||||||
visible,
|
visible,
|
||||||
onClose,
|
onClose,
|
||||||
apiKey,
|
apiKey,
|
||||||
webhookUrl,
|
webhookUrl,
|
||||||
taskId,
|
taskId,
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState("config");
|
const [activeTab, setActiveTab] = useState("config");
|
||||||
const [activeLanguage, setActiveLanguage] = useState("javascript");
|
const [activeLanguage, setActiveLanguage] = useState("javascript");
|
||||||
|
|
||||||
// 处理webhook URL,确保包含完整的API地址
|
// 处理webhook URL,确保包含完整的API地址
|
||||||
const fullWebhookUrl = useMemo(() => {
|
const fullWebhookUrl = useMemo(() => {
|
||||||
return buildApiUrl(webhookUrl);
|
return buildApiUrl(webhookUrl);
|
||||||
}, [webhookUrl]);
|
}, [webhookUrl]);
|
||||||
|
|
||||||
// 生成测试URL
|
// 生成测试URL
|
||||||
const testUrl = useMemo(() => {
|
const testUrl = useMemo(() => {
|
||||||
if (!fullWebhookUrl) return "";
|
if (!fullWebhookUrl) return "";
|
||||||
return `${fullWebhookUrl}?name=测试客户&phone=13800138000&source=API测试`;
|
return `${fullWebhookUrl}?name=测试客户&phone=13800138000&source=API测试`;
|
||||||
}, [fullWebhookUrl]);
|
}, [fullWebhookUrl]);
|
||||||
|
|
||||||
// 检测是否为移动端
|
// 检测是否为移动端
|
||||||
const isMobile = window.innerWidth <= 768;
|
const isMobile = window.innerWidth <= 768;
|
||||||
|
|
||||||
const handleCopy = (text: string, type: string) => {
|
const handleCopy = (text: string, type: string) => {
|
||||||
navigator.clipboard.writeText(text);
|
navigator.clipboard.writeText(text);
|
||||||
Toast.show({
|
Toast.show({
|
||||||
content: `${type}已复制到剪贴板`,
|
content: `${type}已复制到剪贴板`,
|
||||||
position: "top",
|
position: "top",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTestInBrowser = () => {
|
const handleTestInBrowser = () => {
|
||||||
window.open(testUrl, "_blank");
|
window.open(testUrl, "_blank");
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderConfigTab = () => (
|
const renderConfigTab = () => (
|
||||||
<div className={style["config-content"]}>
|
<div className={style["config-content"]}>
|
||||||
{/* API密钥配置 */}
|
{/* API密钥配置 */}
|
||||||
<div className={style["config-section"]}>
|
<div className={style["config-section"]}>
|
||||||
<div className={style["section-header"]}>
|
<div className={style["section-header"]}>
|
||||||
<div className={style["section-title"]}>
|
<div className={style["section-title"]}>
|
||||||
<CheckCircleOutlined className={style["section-icon"]} />
|
<CheckCircleOutlined className={style["section-icon"]} />
|
||||||
API密钥
|
API密钥
|
||||||
</div>
|
</div>
|
||||||
<Tag color="green">安全认证</Tag>
|
<Tag color="green">安全认证</Tag>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["input-group"]}>
|
<div className={style["input-group"]}>
|
||||||
<Input value={apiKey} disabled className={style["api-input"]} />
|
<Input value={apiKey} disabled className={style["api-input"]} />
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => handleCopy(apiKey, "API密钥")}
|
onClick={() => handleCopy(apiKey, "API密钥")}
|
||||||
className={style["copy-btn"]}
|
className={style["copy-btn"]}
|
||||||
>
|
>
|
||||||
<CopyOutlined />
|
<CopyOutlined />
|
||||||
复制
|
复制
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["security-tip"]}>
|
<div className={style["security-tip"]}>
|
||||||
<strong>安全提示:</strong>
|
<strong>安全提示:</strong>
|
||||||
请妥善保管API密钥,不要在客户端代码中暴露。建议在服务器端使用该密钥。
|
请妥善保管API密钥,不要在客户端代码中暴露。建议在服务器端使用该密钥。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 接口地址配置 */}
|
{/* 接口地址配置 */}
|
||||||
<div className={style["config-section"]}>
|
<div className={style["config-section"]}>
|
||||||
<div className={style["section-header"]}>
|
<div className={style["section-header"]}>
|
||||||
<div className={style["section-title"]}>
|
<div className={style["section-title"]}>
|
||||||
<LinkOutlined className={style["section-icon"]} />
|
<LinkOutlined className={style["section-icon"]} />
|
||||||
接口地址
|
接口地址
|
||||||
</div>
|
</div>
|
||||||
<Tag color="blue">POST请求</Tag>
|
<Tag color="blue">POST请求</Tag>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["input-group"]}>
|
<div className={style["input-group"]}>
|
||||||
<Input
|
<Input
|
||||||
value={fullWebhookUrl}
|
value={fullWebhookUrl}
|
||||||
disabled
|
disabled
|
||||||
className={style["api-input"]}
|
className={style["api-input"]}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => handleCopy(fullWebhookUrl, "接口地址")}
|
onClick={() => handleCopy(fullWebhookUrl, "接口地址")}
|
||||||
className={style["copy-btn"]}
|
className={style["copy-btn"]}
|
||||||
>
|
>
|
||||||
<CopyOutlined />
|
<CopyOutlined />
|
||||||
复制
|
复制
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 参数说明 */}
|
{/* 参数说明 */}
|
||||||
<div className={style["params-grid"]}>
|
<div className={style["params-grid"]}>
|
||||||
<div className={style["param-section"]}>
|
<div className={style["param-section"]}>
|
||||||
<h4>必要参数</h4>
|
<h4>必要参数</h4>
|
||||||
<div className={style["param-list"]}>
|
<div className={style["param-list"]}>
|
||||||
<div>
|
<div>
|
||||||
<code>name</code> - 客户姓名
|
<code>name</code> - 客户姓名
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<code>phone</code> - 手机号码
|
<code>phone</code> - 手机号码
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["param-section"]}>
|
<div className={style["param-section"]}>
|
||||||
<h4>可选参数</h4>
|
<h4>可选参数</h4>
|
||||||
<div className={style["param-list"]}>
|
<div className={style["param-list"]}>
|
||||||
<div>
|
<div>
|
||||||
<code>source</code> - 来源标识
|
<code>source</code> - 来源标识
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<code>remark</code> - 备注信息
|
<code>remark</code> - 备注信息
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<code>tags</code> - 客户标签
|
<code>tags</code> - 客户标签
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderQuickTestTab = () => (
|
const renderQuickTestTab = () => (
|
||||||
<div className={style["test-content"]}>
|
<div className={style["test-content"]}>
|
||||||
<div className={style["test-section"]}>
|
<div className={style["test-section"]}>
|
||||||
<h3>快速测试URL</h3>
|
<h3>快速测试URL</h3>
|
||||||
<div className={style["input-group"]}>
|
<div className={style["input-group"]}>
|
||||||
<Input value={testUrl} disabled className={style["test-input"]} />
|
<Input value={testUrl} disabled className={style["test-input"]} />
|
||||||
</div>
|
</div>
|
||||||
<div className={style["test-buttons"]}>
|
<div className={style["test-buttons"]}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleCopy(testUrl, "测试URL")}
|
onClick={() => handleCopy(testUrl, "测试URL")}
|
||||||
className={style["test-btn"]}
|
className={style["test-btn"]}
|
||||||
>
|
>
|
||||||
<CopyOutlined />
|
<CopyOutlined />
|
||||||
复制测试URL
|
复制测试URL
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={handleTestInBrowser}
|
onClick={handleTestInBrowser}
|
||||||
className={style["test-btn"]}
|
className={style["test-btn"]}
|
||||||
>
|
>
|
||||||
在浏览器中测试
|
在浏览器中测试
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderDocsTab = () => (
|
const renderDocsTab = () => (
|
||||||
<div className={style["docs-content"]}>
|
<div className={style["docs-content"]}>
|
||||||
<div className={style["docs-grid"]}>
|
<div className={style["docs-grid"]}>
|
||||||
<Card className={style["doc-card"]}>
|
<Card className={style["doc-card"]}>
|
||||||
<div className={style["doc-icon"]}>
|
<div className={style["doc-icon"]}>
|
||||||
<BookOutlined />
|
<BookOutlined />
|
||||||
</div>
|
</div>
|
||||||
<h4>完整API文档</h4>
|
<h4>完整API文档</h4>
|
||||||
<p>详细的接口说明和参数文档</p>
|
<p>详细的接口说明和参数文档</p>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className={style["doc-card"]}>
|
<Card className={style["doc-card"]}>
|
||||||
<div className={style["doc-icon"]}>
|
<div className={style["doc-icon"]}>
|
||||||
<LinkOutlined />
|
<LinkOutlined />
|
||||||
</div>
|
</div>
|
||||||
<h4>集成指南</h4>
|
<h4>集成指南</h4>
|
||||||
<p>第三方平台集成教程</p>
|
<p>第三方平台集成教程</p>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderCodeTab = () => {
|
const renderCodeTab = () => {
|
||||||
const codeExamples = {
|
const codeExamples = {
|
||||||
javascript: `fetch('${fullWebhookUrl}', {
|
javascript: `fetch('${fullWebhookUrl}', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': 'Bearer ${apiKey}'
|
'Authorization': 'Bearer ${apiKey}'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: '张三',
|
name: '张三',
|
||||||
phone: '13800138000',
|
phone: '13800138000',
|
||||||
source: '官网表单',
|
source: '官网表单',
|
||||||
})
|
})
|
||||||
})`,
|
})`,
|
||||||
python: `import requests
|
python: `import requests
|
||||||
|
|
||||||
url = '${fullWebhookUrl}'
|
url = '${fullWebhookUrl}'
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': 'Bearer ${apiKey}'
|
'Authorization': 'Bearer ${apiKey}'
|
||||||
}
|
}
|
||||||
data = {
|
data = {
|
||||||
'name': '张三',
|
'name': '张三',
|
||||||
'phone': '13800138000',
|
'phone': '13800138000',
|
||||||
'source': '官网表单'
|
'source': '官网表单'
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(url, json=data, headers=headers)`,
|
response = requests.post(url, json=data, headers=headers)`,
|
||||||
php: `<?php
|
php: `<?php
|
||||||
$url = '${fullWebhookUrl}';
|
$url = '${fullWebhookUrl}';
|
||||||
$data = array(
|
$data = array(
|
||||||
'name' => '张三',
|
'name' => '张三',
|
||||||
'phone' => '13800138000',
|
'phone' => '13800138000',
|
||||||
'source' => '官网表单'
|
'source' => '官网表单'
|
||||||
);
|
);
|
||||||
|
|
||||||
$options = array(
|
$options = array(
|
||||||
'http' => array(
|
'http' => array(
|
||||||
'header' => "Content-type: application/json\\r\\nAuthorization: Bearer ${apiKey}\\r\\n",
|
'header' => "Content-type: application/json\\r\\nAuthorization: Bearer ${apiKey}\\r\\n",
|
||||||
'method' => 'POST',
|
'method' => 'POST',
|
||||||
'content' => json_encode($data)
|
'content' => json_encode($data)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
$context = stream_context_create($options);
|
$context = stream_context_create($options);
|
||||||
$result = file_get_contents($url, false, $context);`,
|
$result = file_get_contents($url, false, $context);`,
|
||||||
java: `import java.net.http.HttpClient;
|
java: `import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
String json = "{\\"name\\":\\"张三\\",\\"phone\\":\\"13800138000\\",\\"source\\":\\"官网表单\\"}";
|
String json = "{\\"name\\":\\"张三\\",\\"phone\\":\\"13800138000\\",\\"source\\":\\"官网表单\\"}";
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create("${fullWebhookUrl}"))
|
.uri(URI.create("${fullWebhookUrl}"))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("Authorization", "Bearer ${apiKey}")
|
.header("Authorization", "Bearer ${apiKey}")
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(json))
|
.POST(HttpRequest.BodyPublishers.ofString(json))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());`,
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());`,
|
||||||
};
|
};
|
||||||
|
|
||||||
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"]} ${
|
||||||
activeLanguage === lang ? style["active"] : ""
|
activeLanguage === lang ? style["active"] : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setActiveLanguage(lang)}
|
onClick={() => setActiveLanguage(lang)}
|
||||||
>
|
>
|
||||||
{lang.charAt(0).toUpperCase() + lang.slice(1)}
|
{lang.charAt(0).toUpperCase() + lang.slice(1)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className={style["code-block"]}>
|
<div className={style["code-block"]}>
|
||||||
<pre className={style["code"]}>
|
<pre className={style["code"]}>
|
||||||
<code>
|
<code>
|
||||||
{codeExamples[activeLanguage as keyof typeof codeExamples]}
|
{codeExamples[activeLanguage as keyof typeof codeExamples]}
|
||||||
</code>
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleCopy(
|
handleCopy(
|
||||||
codeExamples[activeLanguage as keyof typeof codeExamples],
|
codeExamples[activeLanguage as keyof typeof codeExamples],
|
||||||
"代码"
|
"代码"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className={style["copy-code-btn"]}
|
className={style["copy-code-btn"]}
|
||||||
>
|
>
|
||||||
<CopyOutlined />
|
<CopyOutlined />
|
||||||
复制代码
|
复制代码
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderContent = () => (
|
const renderContent = () => (
|
||||||
<div className={style["plan-api-dialog"]}>
|
<div className={style["plan-api-dialog"]}>
|
||||||
{/* 头部 */}
|
{/* 头部 */}
|
||||||
<div className={style["dialog-header"]}>
|
<div className={style["dialog-header"]}>
|
||||||
<div className={style["header-left"]}>
|
<div className={style["header-left"]}>
|
||||||
<CodeOutlined className={style["header-icon"]} />
|
<CodeOutlined className={style["header-icon"]} />
|
||||||
<div className={style["header-content"]}>
|
<div className={style["header-content"]}>
|
||||||
<h3>计划接口配置</h3>
|
<h3>计划接口配置</h3>
|
||||||
<p>
|
<p>
|
||||||
通过API接口直接导入客资到该获客计划,支持多种编程语言和第三方平台集成
|
通过API接口直接导入客资到该获客计划,支持多种编程语言和第三方平台集成
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button size="small" onClick={onClose} className={style["close-btn"]}>
|
<Button size="small" onClick={onClose} className={style["close-btn"]}>
|
||||||
×
|
×
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 导航标签 */}
|
{/* 导航标签 */}
|
||||||
<div className={style["nav-tabs"]}>
|
<div className={style["nav-tabs"]}>
|
||||||
<button
|
<button
|
||||||
className={`${style["nav-tab"]} ${activeTab === "config" ? style["active"] : ""}`}
|
className={`${style["nav-tab"]} ${activeTab === "config" ? style["active"] : ""}`}
|
||||||
onClick={() => setActiveTab("config")}
|
onClick={() => setActiveTab("config")}
|
||||||
>
|
>
|
||||||
<SettingOutlined />
|
<SettingOutlined />
|
||||||
接口配置
|
接口配置
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`${style["nav-tab"]} ${activeTab === "test" ? style["active"] : ""}`}
|
className={`${style["nav-tab"]} ${activeTab === "test" ? style["active"] : ""}`}
|
||||||
onClick={() => setActiveTab("test")}
|
onClick={() => setActiveTab("test")}
|
||||||
>
|
>
|
||||||
<ThunderboltOutlined />
|
<ThunderboltOutlined />
|
||||||
快速测试
|
快速测试
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`${style["nav-tab"]} ${activeTab === "docs" ? style["active"] : ""}`}
|
className={`${style["nav-tab"]} ${activeTab === "docs" ? style["active"] : ""}`}
|
||||||
onClick={() => setActiveTab("docs")}
|
onClick={() => setActiveTab("docs")}
|
||||||
>
|
>
|
||||||
<BookOutlined />
|
<BookOutlined />
|
||||||
开发文档
|
开发文档
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`${style["nav-tab"]} ${activeTab === "code" ? style["active"] : ""}`}
|
className={`${style["nav-tab"]} ${activeTab === "code" ? style["active"] : ""}`}
|
||||||
onClick={() => setActiveTab("code")}
|
onClick={() => setActiveTab("code")}
|
||||||
>
|
>
|
||||||
<CodeOutlined />
|
<CodeOutlined />
|
||||||
代码示例
|
代码示例
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 内容区域 */}
|
{/* 内容区域 */}
|
||||||
<div className={style["dialog-content"]}>
|
<div className={style["dialog-content"]}>
|
||||||
{activeTab === "config" && renderConfigTab()}
|
{activeTab === "config" && renderConfigTab()}
|
||||||
{activeTab === "test" && renderQuickTestTab()}
|
{activeTab === "test" && renderQuickTestTab()}
|
||||||
{activeTab === "docs" && renderDocsTab()}
|
{activeTab === "docs" && renderDocsTab()}
|
||||||
{activeTab === "code" && renderCodeTab()}
|
{activeTab === "code" && renderCodeTab()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部 */}
|
{/* 底部 */}
|
||||||
<div className={style["dialog-footer"]}>
|
<div className={style["dialog-footer"]}>
|
||||||
<div className={style["security-note"]}>
|
<div className={style["security-note"]}>
|
||||||
<SafetyOutlined />
|
<SafetyOutlined />
|
||||||
所有数据传输均采用HTTPS加密
|
所有数据传输均采用HTTPS加密
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className={style["complete-btn"]}
|
className={style["complete-btn"]}
|
||||||
>
|
>
|
||||||
完成配置
|
完成配置
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 移动端使用Popup
|
// 移动端使用Popup
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<Popup
|
<Popup
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onMaskClick={onClose}
|
onMaskClick={onClose}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
bodyStyle={{ height: "90vh" }}
|
bodyStyle={{ height: "90vh" }}
|
||||||
>
|
>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</Popup>
|
</Popup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PC端使用Modal
|
// PC端使用Modal
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={visible}
|
open={visible}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
footer={null}
|
footer={null}
|
||||||
width={800}
|
width={800}
|
||||||
centered
|
centered
|
||||||
className={style["plan-api-modal"]}
|
className={style["plan-api-modal"]}
|
||||||
>
|
>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PlanApi;
|
export default PlanApi;
|
||||||
|
|||||||
@@ -1,53 +1,53 @@
|
|||||||
import request from "@/api/request";
|
import request from "@/api/request";
|
||||||
// 获取场景类型列表
|
// 获取场景类型列表
|
||||||
export function getScenarioTypes() {
|
export function getScenarioTypes() {
|
||||||
return request("/v1/plan/scenes", undefined, "GET");
|
return request("/v1/plan/scenes", undefined, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建计划
|
// 创建计划
|
||||||
export function createPlan(data: any) {
|
export function createPlan(data: any) {
|
||||||
return request("/v1/plan/create", data, "POST");
|
return request("/v1/plan/create", data, "POST");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新计划
|
// 更新计划
|
||||||
export function updatePlan(planId: string, data: any) {
|
export function updatePlan(planId: string, data: any) {
|
||||||
return request(`/v1/scenarios/plans/${planId}`, data, "PUT");
|
return request(`/v1/scenarios/plans/${planId}`, data, "PUT");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取计划详情
|
// 获取计划详情
|
||||||
export function getPlanDetail(planId: string) {
|
export function getPlanDetail(planId: string) {
|
||||||
return request(`/v1/scenarios/plans/${planId}`, undefined, "GET");
|
return request(`/v1/scenarios/plans/${planId}`, undefined, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlanDetail 类型定义(可根据实际接口返回结构补充字段)
|
// PlanDetail 类型定义(可根据实际接口返回结构补充字段)
|
||||||
export interface PlanDetail {
|
export interface PlanDetail {
|
||||||
name: string;
|
name: string;
|
||||||
scenario: number;
|
scenario: number;
|
||||||
posters: any[];
|
posters: any[];
|
||||||
device: string[];
|
device: string[];
|
||||||
remarkType: string;
|
remarkType: string;
|
||||||
greeting: string;
|
greeting: string;
|
||||||
addInterval: number;
|
addInterval: number;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
sceneId: string | number;
|
sceneId: string | number;
|
||||||
remarkFormat: string;
|
remarkFormat: string;
|
||||||
addFriendInterval: number;
|
addFriendInterval: number;
|
||||||
// 其它字段可扩展
|
// 其它字段可扩展
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兼容旧代码的接口命名
|
// 兼容旧代码的接口命名
|
||||||
export function getPlanScenes() {
|
export function getPlanScenes() {
|
||||||
return getScenarioTypes();
|
return getScenarioTypes();
|
||||||
}
|
}
|
||||||
export function createScenarioPlan(data: any) {
|
export function createScenarioPlan(data: any) {
|
||||||
return createPlan(data);
|
return createPlan(data);
|
||||||
}
|
}
|
||||||
export function fetchPlanDetail(planId: string) {
|
export function fetchPlanDetail(planId: string) {
|
||||||
return getPlanDetail(planId);
|
return getPlanDetail(planId);
|
||||||
}
|
}
|
||||||
export function updateScenarioPlan(planId: string, data: any) {
|
export function updateScenarioPlan(planId: string, data: any) {
|
||||||
return updatePlan(planId, data);
|
return updatePlan(planId, data);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user