diff --git a/nkebao/.eslintrc.js b/nkebao/.eslintrc.js index c947e090..7aba7b74 100644 --- a/nkebao/.eslintrc.js +++ b/nkebao/.eslintrc.js @@ -25,6 +25,8 @@ module.exports = { 'react/react-in-jsx-scope': 'off', '@typescript-eslint/no-unused-vars': 'warn', 'react/prop-types': 'off', + 'linebreak-style': ['error', 'unix'], + 'eol-last': ['error', 'always'], }, settings: { react: { diff --git a/nkebao/.prettierrc b/nkebao/.prettierrc new file mode 100644 index 00000000..9b38df22 --- /dev/null +++ b/nkebao/.prettierrc @@ -0,0 +1,13 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "endOfLine": "lf", + "bracketSpacing": true, + "arrowParens": "avoid", + "jsxSingleQuote": false, + "quoteProps": "as-needed" +} \ No newline at end of file diff --git a/nkebao/.vscode/settings.json b/nkebao/.vscode/settings.json new file mode 100644 index 00000000..7a1a7c34 --- /dev/null +++ b/nkebao/.vscode/settings.json @@ -0,0 +1,32 @@ +{ + "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" + }, + "[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" + } +} \ No newline at end of file diff --git a/nkebao/package.json b/nkebao/package.json index 8866db69..31765e9d 100644 --- a/nkebao/package.json +++ b/nkebao/package.json @@ -37,8 +37,11 @@ }, "scripts": { "dev": "vite", - "build": "vite build", + "build": "tsc && vite build", "preview": "vite preview", - "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix" + "lint": "eslint src --ext .js,.jsx,.ts,.tsx --fix", + "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,scss,css}\"", + "lint:check": "eslint src --ext .js,.jsx,.ts,.tsx", + "format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx,json,scss,css}\"" } } diff --git a/nkebao/src/App.tsx b/nkebao/src/App.tsx index 9138ddb5..bfee5515 100644 --- a/nkebao/src/App.tsx +++ b/nkebao/src/App.tsx @@ -1,11 +1,11 @@ -import React from "react"; -import AppRouter from "@/router"; -function App() { - return ( - <> - - - ); -} - -export default App; +import React from "react"; +import AppRouter from "@/router"; +function App() { + return ( + <> + + + ); +} + +export default App; diff --git a/nkebao/src/api/devices.ts b/nkebao/src/api/devices.ts index b39c7683..efadbc91 100644 --- a/nkebao/src/api/devices.ts +++ b/nkebao/src/api/devices.ts @@ -1,44 +1,44 @@ -import request from "./request"; - -// 获取设备列表 -export const fetchDeviceList = (params: { - page?: number; - limit?: number; - keyword?: string; -}) => request("/v1/devices", params, "GET"); - -// 获取设备详情 -export const fetchDeviceDetail = (id: string | number) => - request(`/v1/devices/${id}`); - -// 获取设备关联微信账号 -export const fetchDeviceRelatedAccounts = (id: string | number) => - request(`/v1/wechats/related-device/${id}`); - -// 获取设备操作日志 -export const fetchDeviceHandleLogs = ( - id: string | number, - page = 1, - limit = 10 -) => request(`/v1/devices/${id}/handle-logs`, { page, limit }, "GET"); - -// 更新设备任务配置 -export const updateDeviceTaskConfig = (config: { - deviceId: string | number; - autoAddFriend?: boolean; - autoReply?: boolean; - momentsSync?: boolean; - aiChat?: boolean; -}) => request("/v1/devices/task-config", config, "POST"); - -// 删除设备 -export const deleteDevice = (id: number) => - request(`/v1/devices/${id}`, undefined, "DELETE"); - -// 获取设备二维码 -export const fetchDeviceQRCode = (accountId: string) => - request("/v1/api/device/add", { accountId }, "POST"); - -// 通过IMEI添加设备 -export const addDeviceByImei = (imei: string, name: string) => - request("/v1/api/device/add-by-imei", { imei, name }, "POST"); +import request from "./request"; + +// 获取设备列表 +export const fetchDeviceList = (params: { + page?: number; + limit?: number; + keyword?: string; +}) => request("/v1/devices", params, "GET"); + +// 获取设备详情 +export const fetchDeviceDetail = (id: string | number) => + request(`/v1/devices/${id}`); + +// 获取设备关联微信账号 +export const fetchDeviceRelatedAccounts = (id: string | number) => + request(`/v1/wechats/related-device/${id}`); + +// 获取设备操作日志 +export const fetchDeviceHandleLogs = ( + id: string | number, + page = 1, + limit = 10 +) => request(`/v1/devices/${id}/handle-logs`, { page, limit }, "GET"); + +// 更新设备任务配置 +export const updateDeviceTaskConfig = (config: { + deviceId: string | number; + autoAddFriend?: boolean; + autoReply?: boolean; + momentsSync?: boolean; + aiChat?: boolean; +}) => request("/v1/devices/task-config", config, "POST"); + +// 删除设备 +export const deleteDevice = (id: number) => + request(`/v1/devices/${id}`, undefined, "DELETE"); + +// 获取设备二维码 +export const fetchDeviceQRCode = (accountId: string) => + request("/v1/api/device/add", { accountId }, "POST"); + +// 通过IMEI添加设备 +export const addDeviceByImei = (imei: string, name: string) => + request("/v1/api/device/add-by-imei", { imei, name }, "POST"); diff --git a/nkebao/src/api/request.ts b/nkebao/src/api/request.ts index 1e57fac9..9b0cacdb 100644 --- a/nkebao/src/api/request.ts +++ b/nkebao/src/api/request.ts @@ -1,84 +1,90 @@ -import axios, { - AxiosInstance, - AxiosRequestConfig, - Method, - AxiosResponse, -} from "axios"; -import { Toast } from "antd-mobile"; - -const DEFAULT_DEBOUNCE_GAP = 1000; -const debounceMap = new Map(); - -const instance: AxiosInstance = axios.create({ - baseURL: (import.meta as any).env?.VITE_API_BASE_URL || "/api", - timeout: 20000, - headers: { - "Content-Type": "application/json", - }, -}); - -instance.interceptors.request.use((config) => { - const token = localStorage.getItem("token"); - if (token) { - config.headers = config.headers || {}; - config.headers["Authorization"] = `Bearer ${token}`; - } - return config; -}); - -instance.interceptors.response.use( - (res: AxiosResponse) => { - const { code, success, msg } = res.data || {}; - if (code === 200 || success) { - return res.data.data ?? res.data; - } - Toast.show({ content: msg || "接口错误", position: "top" }); - if (code === 401) { - localStorage.removeItem("token"); - const currentPath = window.location.pathname + window.location.search; - if (currentPath === "/login") { - window.location.href = "/login"; - } else { - window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`; - } - } - return Promise.reject(msg || "接口错误"); - }, - (err) => { - Toast.show({ content: err.message || "网络异常", position: "top" }); - return Promise.reject(err); - } -); - -export function request( - url: string, - data?: any, - method: Method = "GET", - config?: AxiosRequestConfig, - debounceGap?: number -): Promise { - const gap = - typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP; - const key = `${method}_${url}_${JSON.stringify(data)}`; - const now = Date.now(); - const last = debounceMap.get(key) || 0; - if (gap > 0 && now - last < gap) { - // Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' }); - return Promise.reject("请求过于频繁,请稍后再试"); - } - debounceMap.set(key, now); - - const axiosConfig: AxiosRequestConfig = { - url, - method, - ...config, - }; - if (method.toUpperCase() === "GET") { - axiosConfig.params = data; - } else { - axiosConfig.data = data; - } - return instance(axiosConfig); -} - -export default request; +import axios, { + AxiosInstance, + AxiosRequestConfig, + Method, + AxiosResponse, +} from "axios"; +import { Toast } from "antd-mobile"; + +const DEFAULT_DEBOUNCE_GAP = 1000; +const debounceMap = new Map(); + +const instance: AxiosInstance = axios.create({ + baseURL: (import.meta as any).env?.VITE_API_BASE_URL || "/api", + timeout: 20000, + headers: { + "Content-Type": "application/json", + }, +}); + +instance.interceptors.request.use(config => { + const token = localStorage.getItem("token"); + if (token) { + config.headers = config.headers || {}; + config.headers["Authorization"] = `Bearer ${token}`; + } + return config; +}); + +instance.interceptors.response.use( + (res: AxiosResponse) => { + const { code, success, msg } = res.data || {}; + if (code === 200 || success) { + return res.data.data ?? res.data; + } + Toast.show({ content: msg || "接口错误", position: "top" }); + if (code === 401) { + localStorage.removeItem("token"); + const currentPath = window.location.pathname + window.location.search; + if (currentPath === "/login") { + window.location.href = "/login"; + } else { + window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`; + } + } + return Promise.reject(msg || "接口错误"); + }, + err => { + Toast.show({ content: err.message || "网络异常", position: "top" }); + return Promise.reject(err); + } +); + +export function request( + url: string, + data?: any, + method: Method = "GET", + config?: AxiosRequestConfig, + debounceGap?: number +): Promise { + const gap = + typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP; + const key = `${method}_${url}_${JSON.stringify(data)}`; + const now = Date.now(); + const last = debounceMap.get(key) || 0; + if (gap > 0 && now - last < gap) { + // Toast.show({ content: '请求过于频繁,请稍后再试', position: 'top' }); + return Promise.reject("请求过于频繁,请稍后再试"); + } + debounceMap.set(key, now); + + const axiosConfig: AxiosRequestConfig = { + url, + method, + ...config, + }; + + // 如果是FormData,不设置Content-Type,让浏览器自动设置 + if (data instanceof FormData) { + delete axiosConfig.headers?.["Content-Type"]; + } + + if (method.toUpperCase() === "GET") { + axiosConfig.params = data; + } else { + axiosConfig.data = data; + } + return instance(axiosConfig); +} + +export default request; diff --git a/nkebao/src/components/AccountSelection/api.ts b/nkebao/src/components/AccountSelection/api.ts index 1aeb5785..bf069cb8 100644 --- a/nkebao/src/components/AccountSelection/api.ts +++ b/nkebao/src/components/AccountSelection/api.ts @@ -1,10 +1,10 @@ -import request from "@/api/request"; - -// 获取好友列表 -export function getAccountList(params: { - page: number; - limit: number; - keyword?: string; -}) { - return request("/v1/workbench/account-list", params, "GET"); -} +import request from "@/api/request"; + +// 获取好友列表 +export function getAccountList(params: { + page: number; + limit: number; + keyword?: string; +}) { + return request("/v1/workbench/account-list", params, "GET"); +} diff --git a/nkebao/src/components/AccountSelection/index.tsx b/nkebao/src/components/AccountSelection/index.tsx index 4fd68280..be8023b8 100644 --- a/nkebao/src/components/AccountSelection/index.tsx +++ b/nkebao/src/components/AccountSelection/index.tsx @@ -114,7 +114,7 @@ export default function AccountSelection({ // 渲染和过滤都依赖内部accountsList const filteredAccounts = accountsList.filter( - (acc) => + acc => acc.userName.includes(searchQuery) || acc.realName.includes(searchQuery) || acc.departmentName.includes(searchQuery) @@ -125,7 +125,7 @@ export default function AccountSelection({ if (readonly) return; const uniqueValue = [...new Set(value)]; const newSelected = uniqueValue.includes(accountId) - ? uniqueValue.filter((id) => id !== accountId) + ? uniqueValue.filter(id => id !== accountId) : [...uniqueValue, accountId]; onChange(newSelected); }; @@ -140,10 +140,10 @@ export default function AccountSelection({ // 获取已选账号详细信息 - 去重处理 const uniqueValue = [...new Set(value)]; const selectedAccountObjs = [ - ...accountsList.filter((acc) => uniqueValue.includes(acc.id)), + ...accountsList.filter(acc => uniqueValue.includes(acc.id)), ...uniqueValue - .filter((id) => !accountsList.some((acc) => acc.id === id)) - .map((id) => ({ + .filter(id => !accountsList.some(acc => acc.id === id)) + .map(id => ({ id, userName: String(id), realName: "", @@ -155,7 +155,7 @@ export default function AccountSelection({ const handleRemoveAccount = (id: number) => { if (readonly) return; const uniqueValue = [...new Set(value)]; - onChange(uniqueValue.filter((d) => d !== id)); + onChange(uniqueValue.filter(d => d !== id)); }; // 确认选择 @@ -200,7 +200,7 @@ export default function AccountSelection({ background: "#fff", }} > - {selectedAccountObjs.map((acc) => ( + {selectedAccountObjs.map(acc => (
) : filteredAccounts.length > 0 ? (
- {filteredAccounts.map((acc) => ( + {filteredAccounts.map(acc => (
+ ); +}; + +export default StepIndicator; diff --git a/nkebao/src/components/Upload/README.md b/nkebao/src/components/Upload/README.md new file mode 100644 index 00000000..7c05cfaa --- /dev/null +++ b/nkebao/src/components/Upload/README.md @@ -0,0 +1,75 @@ +# Upload 上传组件 + +基于 antd Upload 组件封装,简化了 API 请求和文件验证逻辑。 + +## 功能特性 + +- ✅ 自动处理文件上传 API 请求 +- ✅ 文件类型和大小验证 +- ✅ 支持编辑和新增场景 +- ✅ 支持单文件和多文件上传 +- ✅ 上传状态显示 +- ✅ 文件删除功能 + +## 使用方法 + +### 图片上传组件 (UploadComponent) + +```tsx +import UploadComponent from '@/components/Upload'; + +// 单图片上传 + setImageUrl(urls[0] || "")} + count={1} + accept="image/*" +/> + +// 多图片上传 + +``` + +### 视频上传组件 (VideoUpload) + +```tsx +import VideoUpload from "@/components/Upload/VideoUpload"; + +; +``` + +## Props + +### UploadComponent + +| 参数 | 说明 | 类型 | 默认值 | +| --------- | -------------- | --------------------------------------- | ---------------- | +| value | 文件 URL 数组 | `string[]` | `[]` | +| onChange | 文件变化回调 | `(urls: string[]) => void` | - | +| count | 最大上传数量 | `number` | `9` | +| accept | 接受的文件类型 | `string` | `"image/*"` | +| listType | 列表类型 | `"text" \| "picture" \| "picture-card"` | `"picture-card"` | +| disabled | 是否禁用 | `boolean` | `false` | +| className | 自定义类名 | `string` | - | + +### VideoUpload + +| 参数 | 说明 | 类型 | 默认值 | +| --------- | ------------ | ----------------------- | ------- | +| value | 视频 URL | `string` | `""` | +| onChange | 视频变化回调 | `(url: string) => void` | - | +| disabled | 是否禁用 | `boolean` | `false` | +| className | 自定义类名 | `string` | - | + +## 注意事项 + +1. 组件内部使用 `/v1/attachment/upload` 接口进行文件上传 +2. 图片文件大小限制为 5MB,视频文件大小限制为 50MB +3. 支持编辑场景,传入 `value` 时会自动显示已上传的文件 +4. 文件上传成功后会自动调用 `onChange` 回调 diff --git a/nkebao/src/components/Upload/VideoUpload.tsx b/nkebao/src/components/Upload/VideoUpload.tsx new file mode 100644 index 00000000..eb8b86fb --- /dev/null +++ b/nkebao/src/components/Upload/VideoUpload.tsx @@ -0,0 +1,144 @@ +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 request from "@/api/request"; +import style from "./index.module.scss"; + +interface VideoUploadProps { + value?: string; + onChange?: (url: string) => void; + disabled?: boolean; + className?: string; +} + +const VideoUpload: React.FC = ({ + value = "", + onChange, + disabled = false, + className, +}) => { + const [loading, setLoading] = useState(false); + const [fileList, setFileList] = useState([]); + + // 将value转换为fileList格式 + React.useEffect(() => { + if (value) { + const file: UploadFile = { + uid: "-1", + name: "video", + status: "done", + url: value, + }; + 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 false; // 阻止自动上传 + }; + + // 处理文件变化 + const handleChange: UploadProps["onChange"] = info => { + if (info.file.status === "uploading") { + setLoading(true); + return; + } + if (info.file.status === "done") { + setLoading(false); + // 更新fileList + setFileList([info.file]); + // 调用onChange + onChange?.(info.file.url || ""); + } + }; + + // 自定义上传请求 + const customRequest: UploadProps["customRequest"] = async ({ + file, + onSuccess, + onError, + }) => { + try { + setLoading(true); + const formData = new FormData(); + formData.append("file", file as File); + + const response = await request("/v1/attachment/upload", formData, "POST"); + + if (response) { + const uploadedUrl = + typeof response === "string" ? response : response.url || response; + onSuccess?.(uploadedUrl); + } else { + throw new Error("上传失败"); + } + } catch (error) { + console.error("上传失败:", error); + onError?.(error as Error); + message.error("上传失败,请重试"); + } finally { + setLoading(false); + } + }; + + // 删除文件 + const handleRemove = () => { + setFileList([]); + onChange?.(""); + return true; + }; + + const uploadButton = ( +
+ {loading ? ( +
+ +
上传中...
+
+ ) : ( + <> + +
上传视频
+ + )} +
+ ); + + return ( +
+ + {fileList.length >= 1 ? null : uploadButton} + +
+ ); +}; + +export default VideoUpload; diff --git a/nkebao/src/components/Upload/index.module.scss b/nkebao/src/components/Upload/index.module.scss new file mode 100644 index 00000000..54a4c006 --- /dev/null +++ b/nkebao/src/components/Upload/index.module.scss @@ -0,0 +1,75 @@ +.upload-container { + width: 100%; +} + +.upload-button { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100px; + border: 1px dashed #d9d9d9; + border-radius: 6px; + background: #fafafa; + cursor: pointer; + transition: all 0.3s; + + &:hover { + border-color: #1677ff; + background: #f0f8ff; + } +} + +.upload-icon { + font-size: 24px; + color: #999; + margin-bottom: 8px; +} + +.upload-text { + font-size: 14px; + color: #666; +} + +.uploading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + + .upload-icon { + color: #1677ff; + animation: spin 1s linear infinite; + } + + .upload-text { + color: #1677ff; + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +// 覆盖antd默认样式 +:global { + .ant-upload-list-picture-card { + .ant-upload-list-item { + width: 100px; + height: 100px; + } + } + + .ant-upload-select-picture-card { + width: 100px; + height: 100px; + } +} diff --git a/nkebao/src/components/Upload/index.tsx b/nkebao/src/components/Upload/index.tsx new file mode 100644 index 00000000..004163fe --- /dev/null +++ b/nkebao/src/components/Upload/index.tsx @@ -0,0 +1,164 @@ +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 request from "@/api/request"; +import style from "./index.module.scss"; + +interface UploadComponentProps { + value?: string[]; + onChange?: (urls: string[]) => void; + count?: number; // 最大上传数量 + accept?: string; // 文件类型 + listType?: "text" | "picture" | "picture-card"; + disabled?: boolean; + className?: string; +} + +const UploadComponent: React.FC = ({ + value = [], + onChange, + count = 9, + accept = "image/*", + listType = "picture-card", + disabled = false, + className, +}) => { + const [loading, setLoading] = useState(false); + const [fileList, setFileList] = useState([]); + + // 将value转换为fileList格式 + React.useEffect(() => { + if (value && value.length > 0) { + const files = value.map((url, index) => ({ + uid: `-${index}`, + name: `file-${index}`, + status: "done" as const, + url, + })); + setFileList(files); + } else { + setFileList([]); + } + }, [value]); + + // 文件验证 + const beforeUpload = (file: File) => { + const isValidType = file.type.startsWith(accept.replace("*", "")); + if (!isValidType) { + message.error(`只能上传${accept}格式的文件!`); + return false; + } + + const isLt5M = file.size / 1024 / 1024 < 5; + if (!isLt5M) { + message.error("文件大小不能超过5MB!"); + return false; + } + + return false; // 阻止自动上传 + }; + + // 处理文件变化 + const handleChange: UploadProps["onChange"] = info => { + if (info.file.status === "uploading") { + setLoading(true); + return; + } + if (info.file.status === "done") { + setLoading(false); + // 更新fileList + const newFileList = [...fileList]; + const fileIndex = newFileList.findIndex(f => f.uid === info.file.uid); + if (fileIndex > -1) { + newFileList[fileIndex] = info.file; + } else { + newFileList.push(info.file); + } + setFileList(newFileList); + + // 调用onChange + const urls = newFileList.map(f => f.url).filter(Boolean) as string[]; + onChange?.(urls); + } + }; + + // 自定义上传请求 + const customRequest: UploadProps["customRequest"] = async ({ + file, + onSuccess, + onError, + onProgress, + }) => { + try { + setLoading(true); + const formData = new FormData(); + formData.append("file", file as File); + + const response = await request("/v1/attachment/upload", formData, "POST"); + + if (response) { + const uploadedUrl = + typeof response === "string" ? response : response.url || response; + onSuccess?.(uploadedUrl); + } else { + throw new Error("上传失败"); + } + } catch (error) { + console.error("上传失败:", error); + onError?.(error as Error); + message.error("上传失败,请重试"); + } finally { + setLoading(false); + } + }; + + // 删除文件 + const handleRemove = (file: UploadFile) => { + const newFileList = fileList.filter(f => f.uid !== file.uid); + setFileList(newFileList); + const urls = newFileList.map(f => f.url).filter(Boolean) as string[]; + onChange?.(urls); + return true; + }; + + const uploadButton = ( +
+ {loading ? ( +
+ +
上传中...
+
+ ) : ( + <> + +
+ {count > 1 ? "上传文件" : "上传文件"} +
+ + )} +
+ ); + + return ( +
+ 1} + fileList={fileList} + accept={accept} + listType={listType} + showUploadList={true} + disabled={disabled || loading} + beforeUpload={beforeUpload} + customRequest={customRequest} + onChange={handleChange} + onRemove={handleRemove} + > + {fileList.length >= count ? null : uploadButton} + +
+ ); +}; + +export default UploadComponent; diff --git a/nkebao/src/main.tsx b/nkebao/src/main.tsx index 62b34a6c..f4ae337c 100644 --- a/nkebao/src/main.tsx +++ b/nkebao/src/main.tsx @@ -1,8 +1,8 @@ -import React from "react"; -import { createRoot } from "react-dom/client"; -import App from "./App"; -import "./styles/global.scss"; -// import VConsole from "vconsole"; -// new VConsole(); -const root = createRoot(document.getElementById("root")!); -root.render(); +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; +import "./styles/global.scss"; +// import VConsole from "vconsole"; +// new VConsole(); +const root = createRoot(document.getElementById("root")!); +root.render(); diff --git a/nkebao/src/pages/login/api.ts b/nkebao/src/pages/login/api.ts index 191b14f7..617d0bed 100644 --- a/nkebao/src/pages/login/api.ts +++ b/nkebao/src/pages/login/api.ts @@ -1,53 +1,53 @@ -import request from '@/api/request'; -export interface LoginParams { - phone: string; - password?: string; - verificationCode?: string; -} - -export interface LoginResponse { - code: number; - msg: string; - data: { - token: string; - token_expired: string; - member: { - id: string; - name: string; - phone: string; - s2_accountId: string; - avatar?: string; - email?: string; - }; - }; -} - -export interface SendCodeResponse { - code: number; - msg: string; -} - -// 密码登录 -export function loginWithPassword(params:any) { - return request('/v1/auth/login', params, 'POST'); -} - -// 验证码登录 -export function loginWithCode(params:any) { - return request('/v1/auth/login-code', params, 'POST'); -} - -// 发送验证码 -export function sendVerificationCode(params:any) { - return request('/v1/auth/code',params, 'POST'); -} - -// 退出登录 -export function logout() { - return request('/v1/auth/logout', {}, 'POST'); -} - -// 获取用户信息 -export function getUserInfo() { - return request('/v1/auth/user-info', {}, 'GET'); -} \ No newline at end of file +import request from "@/api/request"; +export interface LoginParams { + phone: string; + password?: string; + verificationCode?: string; +} + +export interface LoginResponse { + code: number; + msg: string; + data: { + token: string; + token_expired: string; + member: { + id: string; + name: string; + phone: string; + s2_accountId: string; + avatar?: string; + email?: string; + }; + }; +} + +export interface SendCodeResponse { + code: number; + msg: string; +} + +// 密码登录 +export function loginWithPassword(params: any) { + return request("/v1/auth/login", params, "POST"); +} + +// 验证码登录 +export function loginWithCode(params: any) { + return request("/v1/auth/login-code", params, "POST"); +} + +// 发送验证码 +export function sendVerificationCode(params: any) { + return request("/v1/auth/code", params, "POST"); +} + +// 退出登录 +export function logout() { + return request("/v1/auth/logout", {}, "POST"); +} + +// 获取用户信息 +export function getUserInfo() { + return request("/v1/auth/user-info", {}, "GET"); +} diff --git a/nkebao/src/pages/login/login.module.scss b/nkebao/src/pages/login/login.module.scss index a13baa52..28a3a722 100644 --- a/nkebao/src/pages/login/login.module.scss +++ b/nkebao/src/pages/login/login.module.scss @@ -1,439 +1,440 @@ -.login-page { - min-height: 100vh; - background: var(--primary-gradient); - display: flex; - align-items: center; - justify-content: center; - padding: 15px; - position: relative; - overflow: hidden; -} - -// 背景装饰 -.bg-decoration { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - pointer-events: none; - z-index: 0; -} - -.bg-circle { - position: absolute; - border-radius: 50%; - background: rgba(255, 255, 255, 0.1); - animation: float 6s ease-in-out infinite; - - &:nth-child(1) { - width: 200px; - height: 200px; - top: -100px; - right: -100px; - animation-delay: 0s; - } - - &:nth-child(2) { - width: 150px; - height: 150px; - bottom: -75px; - left: -75px; - animation-delay: 2s; - } - - &:nth-child(3) { - width: 100px; - height: 100px; - top: 50%; - right: 10%; - animation-delay: 4s; - } -} - -@keyframes float { - 0%, 100% { - transform: translateY(0px) rotate(0deg); - } - 50% { - transform: translateY(-20px) rotate(180deg); - } -} - -.login-container { - width: 100%; - max-width: 420px; - background: #ffffff; - backdrop-filter: blur(20px); - border-radius: 24px; - padding: 24px 20px; - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); - position: relative; - z-index: 1; - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.login-header { - text-align: center; - margin-bottom: 24px; -} - -.logo-section { - display: flex; - align-items: center; - justify-content: center; - gap: 12px; - margin-bottom: 16px; -} - -.logo-icon { - width: 40px; - height: 40px; - background: var(--primary-gradient); - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - color: white; - font-size: 20px; - box-shadow: 0 6px 12px var(--primary-shadow); -} - -.app-name { - font-size: 24px; - font-weight: 800; - background: var(--primary-gradient); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - margin: 0; -} - -.subtitle { - font-size: 13px; - color: #666; - margin: 0; -} - -.form-container { - margin-bottom: 20px; -} - -// 标签页样式 -.tab-container { - display: flex; - background: #f8f9fa; - border-radius: 10px; - padding: 3px; - margin-bottom: 24px; - position: relative; -} - -.tab-item { - flex: 1; - text-align: center; - padding: 10px 12px; - font-size: 13px; - font-weight: 500; - color: #666; - cursor: pointer; - border-radius: 7px; - transition: all 0.3s ease; - position: relative; - z-index: 2; - - &.active { - color: var(--primary-color); - font-weight: 600; - background: white; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); - } -} - -.tab-indicator { - display: none; // 隐藏分割线指示器 -} - -// 表单样式 -.login-form { - :global(.adm-form) { - --adm-font-size-main: 14px; - } -} - -.input-group { - margin-bottom: 18px; -} - -.input-label { - display: block; - font-size: 13px; - font-weight: 600; - color: #333; - margin-bottom: 6px; -} - -.input-wrapper { - position: relative; - display: flex; - align-items: center; - background: #f8f9fa; - border: 2px solid transparent; - border-radius: 10px; - transition: all 0.3s ease; - - &:focus-within { - border-color: var(--primary-color); - background: white; - box-shadow: 0 0 0 3px var(--primary-shadow-light); - } -} - -.input-prefix { - padding: 0 12px; - color: #666; - font-size: 13px; - font-weight: 500; - border-right: 1px solid #e5e5e5; -} - -.phone-input, -.password-input, -.code-input { - flex: 1; - border: none !important; - background: transparent !important; - padding: 12px 14px !important; - font-size: 15px !important; - color: #333 !important; - - &::placeholder { - color: #999; - } - - &:focus { - box-shadow: none !important; - } -} - -.eye-icon { - padding: 0 12px; - color: #666; - cursor: pointer; - transition: color 0.3s ease; - - &:hover { - color: var(--primary-color); - } -} - -.send-code-btn { - padding: 6px 12px; - margin-right: 6px; - background: var(--primary-gradient); - color: white; - border: none; - border-radius: 6px; - font-size: 11px; - font-weight: 500; - cursor: pointer; - transition: all 0.3s ease; - - &:hover:not(.disabled) { - transform: translateY(-1px); - box-shadow: 0 3px 8px var(--primary-shadow); - } - - &.disabled { - background: #e5e5e5; - color: #999; - cursor: not-allowed; - transform: none; - box-shadow: none; - } -} - -.agreement-section { - margin-bottom: 24px; -} - -.agreement-checkbox { - display: flex; - align-items: center; - gap: 6px; - font-size: 12px; - color: #666; - line-height: 1.3; - white-space: nowrap; - - :global(.adm-checkbox) { - margin-top: 0; - flex-shrink: 0; - transform: scale(0.8); - } -} - -.agreement-text { - flex: 1; - display: flex; - align-items: center; - flex-wrap: nowrap; - white-space: nowrap; - overflow: visible; - text-overflow: clip; - font-size: 13px; -} - -.agreement-link { - color: var(--primary-color); - cursor: pointer; - text-decoration: none; - white-space: nowrap; - font-size: 11px; - - &:hover { - text-decoration: underline; - } -} - -.login-btn { - height: 46px; - font-size: 15px; - font-weight: 600; - border-radius: 10px; - background: var(--primary-gradient); - border: none; - box-shadow: 0 6px 12px var(--primary-shadow); - transition: all 0.3s ease; - - &:hover:not(:disabled) { - transform: translateY(-1px); - box-shadow: 0 8px 16px var(--primary-shadow-dark); - } - - &:disabled { - background: #e5e5e5; - color: #999; - transform: none; - box-shadow: none; - } -} - -.divider { - position: relative; - text-align: center; - margin: 24px 0; - - &::before { - content: ''; - position: absolute; - top: 50%; - left: 0; - right: 0; - height: 1px; - background: #e5e5e5; - } - - span { - background: rgba(255, 255, 255, 0.95); - padding: 0 12px; - color: #999; - font-size: 11px; - font-weight: 500; - } -} - -.third-party-login { - display: flex; - justify-content: center; - gap: 20px; -} - -.third-party-item { - display: flex; - flex-direction: column; - align-items: center; - gap: 6px; - cursor: pointer; - padding: 12px; - border-radius: 10px; - transition: all 0.3s ease; - - &:hover { - background: #f8f9fa; - transform: translateY(-1px); - } - - span { - font-size: 11px; - color: #666; - font-weight: 500; - } -} - -.wechat-icon, -.apple-icon { - width: 36px; - height: 36px; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - color: white; - font-size: 18px; - transition: all 0.3s ease; -} - -.wechat-icon { - 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); - } - - svg { - width: 20px; - height: 20px; - } -} - -.apple-icon { - 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); - } - - svg { - width: 20px; - height: 20px; - } -} - -// 响应式设计 -@media (max-width: 480px) { - .login-container { - padding: 24px 20px; - margin: 0 12px; - } - - .app-name { - font-size: 22px; - } - - .third-party-login { - gap: 16px; - } - - .third-party-item { - padding: 10px; - } - - .wechat-icon, - .apple-icon { - width: 32px; - height: 32px; - } -} +.login-page { + min-height: 100vh; + background: var(--primary-gradient); + display: flex; + align-items: center; + justify-content: center; + padding: 15px; + position: relative; + overflow: hidden; +} + +// 背景装饰 +.bg-decoration { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + z-index: 0; +} + +.bg-circle { + position: absolute; + border-radius: 50%; + background: rgba(255, 255, 255, 0.1); + animation: float 6s ease-in-out infinite; + + &:nth-child(1) { + width: 200px; + height: 200px; + top: -100px; + right: -100px; + animation-delay: 0s; + } + + &:nth-child(2) { + width: 150px; + height: 150px; + bottom: -75px; + left: -75px; + animation-delay: 2s; + } + + &:nth-child(3) { + width: 100px; + height: 100px; + top: 50%; + right: 10%; + animation-delay: 4s; + } +} + +@keyframes float { + 0%, + 100% { + transform: translateY(0px) rotate(0deg); + } + 50% { + transform: translateY(-20px) rotate(180deg); + } +} + +.login-container { + width: 100%; + max-width: 420px; + background: #ffffff; + backdrop-filter: blur(20px); + border-radius: 24px; + padding: 24px 20px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + position: relative; + z-index: 1; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.login-header { + text-align: center; + margin-bottom: 24px; +} + +.logo-section { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin-bottom: 16px; +} + +.logo-icon { + width: 40px; + height: 40px; + background: var(--primary-gradient); + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 20px; + box-shadow: 0 6px 12px var(--primary-shadow); +} + +.app-name { + font-size: 24px; + font-weight: 800; + background: var(--primary-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin: 0; +} + +.subtitle { + font-size: 13px; + color: #666; + margin: 0; +} + +.form-container { + margin-bottom: 20px; +} + +// 标签页样式 +.tab-container { + display: flex; + background: #f8f9fa; + border-radius: 10px; + padding: 3px; + margin-bottom: 24px; + position: relative; +} + +.tab-item { + flex: 1; + text-align: center; + padding: 10px 12px; + font-size: 13px; + font-weight: 500; + color: #666; + cursor: pointer; + border-radius: 7px; + transition: all 0.3s ease; + position: relative; + z-index: 2; + + &.active { + color: var(--primary-color); + font-weight: 600; + background: white; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); + } +} + +.tab-indicator { + display: none; // 隐藏分割线指示器 +} + +// 表单样式 +.login-form { + :global(.adm-form) { + --adm-font-size-main: 14px; + } +} + +.input-group { + margin-bottom: 18px; +} + +.input-label { + display: block; + font-size: 13px; + font-weight: 600; + color: #333; + margin-bottom: 6px; +} + +.input-wrapper { + position: relative; + display: flex; + align-items: center; + background: #f8f9fa; + border: 2px solid transparent; + border-radius: 10px; + transition: all 0.3s ease; + + &:focus-within { + border-color: var(--primary-color); + background: white; + box-shadow: 0 0 0 3px var(--primary-shadow-light); + } +} + +.input-prefix { + padding: 0 12px; + color: #666; + font-size: 13px; + font-weight: 500; + border-right: 1px solid #e5e5e5; +} + +.phone-input, +.password-input, +.code-input { + flex: 1; + border: none !important; + background: transparent !important; + padding: 12px 14px !important; + font-size: 15px !important; + color: #333 !important; + + &::placeholder { + color: #999; + } + + &:focus { + box-shadow: none !important; + } +} + +.eye-icon { + padding: 0 12px; + color: #666; + cursor: pointer; + transition: color 0.3s ease; + + &:hover { + color: var(--primary-color); + } +} + +.send-code-btn { + padding: 6px 12px; + margin-right: 6px; + background: var(--primary-gradient); + color: white; + border: none; + border-radius: 6px; + font-size: 11px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + + &:hover:not(.disabled) { + transform: translateY(-1px); + box-shadow: 0 3px 8px var(--primary-shadow); + } + + &.disabled { + background: #e5e5e5; + color: #999; + cursor: not-allowed; + transform: none; + box-shadow: none; + } +} + +.agreement-section { + margin-bottom: 24px; +} + +.agreement-checkbox { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: #666; + line-height: 1.3; + white-space: nowrap; + + :global(.adm-checkbox) { + margin-top: 0; + flex-shrink: 0; + transform: scale(0.8); + } +} + +.agreement-text { + flex: 1; + display: flex; + align-items: center; + flex-wrap: nowrap; + white-space: nowrap; + overflow: visible; + text-overflow: clip; + font-size: 13px; +} + +.agreement-link { + color: var(--primary-color); + cursor: pointer; + text-decoration: none; + white-space: nowrap; + font-size: 11px; + + &:hover { + text-decoration: underline; + } +} + +.login-btn { + height: 46px; + font-size: 15px; + font-weight: 600; + border-radius: 10px; + background: var(--primary-gradient); + border: none; + box-shadow: 0 6px 12px var(--primary-shadow); + transition: all 0.3s ease; + + &:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 8px 16px var(--primary-shadow-dark); + } + + &:disabled { + background: #e5e5e5; + color: #999; + transform: none; + box-shadow: none; + } +} + +.divider { + position: relative; + text-align: center; + margin: 24px 0; + + &::before { + content: ""; + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 1px; + background: #e5e5e5; + } + + span { + background: rgba(255, 255, 255, 0.95); + padding: 0 12px; + color: #999; + font-size: 11px; + font-weight: 500; + } +} + +.third-party-login { + display: flex; + justify-content: center; + gap: 20px; +} + +.third-party-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + cursor: pointer; + padding: 12px; + border-radius: 10px; + transition: all 0.3s ease; + + &:hover { + background: #f8f9fa; + transform: translateY(-1px); + } + + span { + font-size: 11px; + color: #666; + font-weight: 500; + } +} + +.wechat-icon, +.apple-icon { + width: 36px; + height: 36px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 18px; + transition: all 0.3s ease; +} + +.wechat-icon { + 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); + } + + svg { + width: 20px; + height: 20px; + } +} + +.apple-icon { + 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); + } + + svg { + width: 20px; + height: 20px; + } +} + +// 响应式设计 +@media (max-width: 480px) { + .login-container { + padding: 24px 20px; + margin: 0 12px; + } + + .app-name { + font-size: 22px; + } + + .third-party-login { + gap: 16px; + } + + .third-party-item { + padding: 10px; + } + + .wechat-icon, + .apple-icon { + width: 32px; + height: 32px; + } +} diff --git a/nkebao/src/pages/mobile/component-test/index.tsx b/nkebao/src/pages/mobile/component-test/index.tsx index acb05ebe..1334cda6 100644 --- a/nkebao/src/pages/mobile/component-test/index.tsx +++ b/nkebao/src/pages/mobile/component-test/index.tsx @@ -1,160 +1,160 @@ -import React, { useState } from "react"; -import { NavBar, Tabs } from "antd-mobile"; -import { ArrowLeftOutlined } from "@ant-design/icons"; -import { useNavigate } from "react-router-dom"; -import Layout from "@/components/Layout/Layout"; -import NavCommon from "@/components/NavCommon"; -import DeviceSelection from "@/components/DeviceSelection"; -import FriendSelection from "@/components/FriendSelection"; -import GroupSelection from "@/components/GroupSelection"; -import ContentLibrarySelection from "@/components/ContentLibrarySelection"; -import AccountSelection from "@/components/AccountSelection"; - -const ComponentTest: React.FC = () => { - const navigate = useNavigate(); - const [activeTab, setActiveTab] = useState("devices"); - - // 设备选择状态 - const [selectedDevices, setSelectedDevices] = useState([]); - - // 好友选择状态 - const [selectedFriends, setSelectedFriends] = useState([]); - - // 群组选择状态 - const [selectedGroups, setSelectedGroups] = useState([]); - - // 内容库选择状态 - const [selectedLibraries, setSelectedLibraries] = useState([]); - - const [selectedAccounts, setSelectedAccounts] = useState([]); - - return ( - }> -
- - -
-

DeviceSelection 组件测试

- -
- 已选设备: {selectedDevices.length} 个 -
- 设备ID: {selectedDevices.join(", ") || "无"} -
-
-
- - -
-

FriendSelection 组件测试

- -
- 已选好友: {selectedFriends.length} 个 -
- 好友ID: {selectedFriends.join(", ") || "无"} -
-
-
- - -
-

GroupSelection 组件测试

- -
- 已选群组: {selectedGroups.length} 个 -
- 群组ID: {selectedGroups.join(", ") || "无"} -
-
-
- - -
-

- ContentLibrarySelection 组件测试 -

- -
- 已选内容库: {selectedLibraries.length} 个 -
- 内容库ID:{" "} - {selectedLibraries.join(", ") || "无"} -
-
-
- - -
-

AccountSelection 组件测试

- -
- 已选账号: - {selectedAccounts.length > 0 - ? selectedAccounts.join(", ") - : "无"} -
-
-
-
-
-
- ); -}; - -export default ComponentTest; +import React, { useState } from "react"; +import { NavBar, Tabs } from "antd-mobile"; +import { ArrowLeftOutlined } from "@ant-design/icons"; +import { useNavigate } from "react-router-dom"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; +import DeviceSelection from "@/components/DeviceSelection"; +import FriendSelection from "@/components/FriendSelection"; +import GroupSelection from "@/components/GroupSelection"; +import ContentLibrarySelection from "@/components/ContentLibrarySelection"; +import AccountSelection from "@/components/AccountSelection"; + +const ComponentTest: React.FC = () => { + const navigate = useNavigate(); + const [activeTab, setActiveTab] = useState("devices"); + + // 设备选择状态 + const [selectedDevices, setSelectedDevices] = useState([]); + + // 好友选择状态 + const [selectedFriends, setSelectedFriends] = useState([]); + + // 群组选择状态 + const [selectedGroups, setSelectedGroups] = useState([]); + + // 内容库选择状态 + const [selectedLibraries, setSelectedLibraries] = useState([]); + + const [selectedAccounts, setSelectedAccounts] = useState([]); + + return ( + }> +
+ + +
+

DeviceSelection 组件测试

+ +
+ 已选设备: {selectedDevices.length} 个 +
+ 设备ID: {selectedDevices.join(", ") || "无"} +
+
+
+ + +
+

FriendSelection 组件测试

+ +
+ 已选好友: {selectedFriends.length} 个 +
+ 好友ID: {selectedFriends.join(", ") || "无"} +
+
+
+ + +
+

GroupSelection 组件测试

+ +
+ 已选群组: {selectedGroups.length} 个 +
+ 群组ID: {selectedGroups.join(", ") || "无"} +
+
+
+ + +
+

+ ContentLibrarySelection 组件测试 +

+ +
+ 已选内容库: {selectedLibraries.length} 个 +
+ 内容库ID:{" "} + {selectedLibraries.join(", ") || "无"} +
+
+
+ + +
+

AccountSelection 组件测试

+ +
+ 已选账号: + {selectedAccounts.length > 0 + ? selectedAccounts.join(", ") + : "无"} +
+
+
+
+
+
+ ); +}; + +export default ComponentTest; diff --git a/nkebao/src/pages/mobile/content/form/data.ts b/nkebao/src/pages/mobile/content/form/data.ts index d0270043..3f3f3425 100644 --- a/nkebao/src/pages/mobile/content/form/data.ts +++ b/nkebao/src/pages/mobile/content/form/data.ts @@ -1,61 +1,61 @@ -// 内容库表单数据类型定义 -export interface ContentLibrary { - id: string; - name: string; - sourceType: number; // 1=微信好友, 2=聊天群 - creatorName?: string; - updateTime: string; - status: number; // 0=未启用, 1=已启用 - itemCount?: number; - createTime: string; - sourceFriends?: string[]; - sourceGroups?: string[]; - keywordInclude?: string[]; - keywordExclude?: string[]; - aiPrompt?: string; - timeEnabled?: number; - timeStart?: string; - timeEnd?: string; - selectedFriends?: any[]; - selectedGroups?: any[]; - selectedGroupMembers?: WechatGroupMember[]; -} - -// 微信群成员 -export interface WechatGroupMember { - id: string; - nickname: string; - wechatId: string; - avatar: string; - gender?: "male" | "female"; - role?: "owner" | "admin" | "member"; - joinTime?: string; -} - -// API 响应类型 -export interface ApiResponse { - code: number; - msg: string; - data: T; -} - -// 创建内容库参数 -export interface CreateContentLibraryParams { - name: string; - sourceType: number; - sourceFriends?: string[]; - sourceGroups?: string[]; - keywordInclude?: string[]; - keywordExclude?: string[]; - aiPrompt?: string; - timeEnabled?: number; - timeStart?: string; - timeEnd?: string; -} - -// 更新内容库参数 -export interface UpdateContentLibraryParams - extends Partial { - id: string; - status?: number; -} +// 内容库表单数据类型定义 +export interface ContentLibrary { + id: string; + name: string; + sourceType: number; // 1=微信好友, 2=聊天群 + creatorName?: string; + updateTime: string; + status: number; // 0=未启用, 1=已启用 + itemCount?: number; + createTime: string; + sourceFriends?: string[]; + sourceGroups?: string[]; + keywordInclude?: string[]; + keywordExclude?: string[]; + aiPrompt?: string; + timeEnabled?: number; + timeStart?: string; + timeEnd?: string; + selectedFriends?: any[]; + selectedGroups?: any[]; + selectedGroupMembers?: WechatGroupMember[]; +} + +// 微信群成员 +export interface WechatGroupMember { + id: string; + nickname: string; + wechatId: string; + avatar: string; + gender?: "male" | "female"; + role?: "owner" | "admin" | "member"; + joinTime?: string; +} + +// API 响应类型 +export interface ApiResponse { + code: number; + msg: string; + data: T; +} + +// 创建内容库参数 +export interface CreateContentLibraryParams { + name: string; + sourceType: number; + sourceFriends?: string[]; + sourceGroups?: string[]; + keywordInclude?: string[]; + keywordExclude?: string[]; + aiPrompt?: string; + timeEnabled?: number; + timeStart?: string; + timeEnd?: string; +} + +// 更新内容库参数 +export interface UpdateContentLibraryParams + extends Partial { + id: string; + status?: number; +} diff --git a/nkebao/src/pages/mobile/content/form/index.module.scss b/nkebao/src/pages/mobile/content/form/index.module.scss index 0ee6be9d..b2f49942 100644 --- a/nkebao/src/pages/mobile/content/form/index.module.scss +++ b/nkebao/src/pages/mobile/content/form/index.module.scss @@ -1,138 +1,140 @@ -.form-page { - background: #f7f8fa; - padding: 16px; -} - -.form-main { - max-width: 420px; - margin: 0 auto; - padding: 16px 0 0 0; -} - -.form-section { - margin-bottom: 18px; -} - -.form-card { - border-radius: 16px; - box-shadow: 0 4px 24px rgba(0,0,0,0.06); - padding: 24px 18px 18px 18px; - background: #fff; -} - -.form-label { - font-weight: 600; - font-size: 16px; - color: #222; - display: block; - margin-bottom: 6px; -} - -.section-title { - font-size: 16px; - font-weight: 700; - color: #222; - margin-top: 28px; - margin-bottom: 12px; - letter-spacing: 0.5px; -} - -.section-block { - padding: 12px 0 8px 0; - border-bottom: 1px solid #f0f0f0; - margin-bottom: 8px; -} - -.tabs-bar { - .adm-tabs-header { - background: #f7f8fa; - border-radius: 8px; - margin-bottom: 8px; - } - .adm-tabs-tab { - font-size: 15px; - font-weight: 500; - padding: 8px 0; - } -} - -.collapse { - margin-top: 12px; - .adm-collapse-panel-content { - padding-bottom: 8px; - background: #f8fafc; - border-radius: 10px; - padding: 18px 14px 10px 14px; - margin-top: 2px; - box-shadow: 0 2px 8px rgba(0,0,0,0.03); - } - .form-section { - margin-bottom: 22px; - } - .form-label { - font-size: 15px; - font-weight: 500; - margin-bottom: 4px; - color: #333; - } - .adm-input { - min-height: 42px; - font-size: 15px; - border-radius: 7px; - margin-bottom: 2px; - } -} - -.ai-row, .section-block { - display: flex; - align-items: center; - gap: 12px; -} - -.ai-desc { - color: #888; - font-size: 13px; - flex: 1; -} - -.date-row, .section-block { - display: flex; - gap: 12px; - align-items: center; -} - -.adm-input { - min-height: 44px; - font-size: 15px; - border-radius: 8px; -} - -.submit-btn { - margin-top: 32px; - height: 48px !important; - border-radius: 10px !important; - font-size: 17px; - font-weight: 600; - letter-spacing: 1px; -} - -@media (max-width: 600px) { - .form-main { - max-width: 100vw; - padding: 0; - } - .form-card { - border-radius: 0; - box-shadow: none; - padding: 16px 6px 12px 6px; - } - .section-title { - font-size: 15px; - margin-top: 22px; - margin-bottom: 8px; - } - .submit-btn { - height: 44px !important; - font-size: 15px; - } -} +.form-page { + background: #f7f8fa; + padding: 16px; +} + +.form-main { + max-width: 420px; + margin: 0 auto; + padding: 16px 0 0 0; +} + +.form-section { + margin-bottom: 18px; +} + +.form-card { + border-radius: 16px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06); + padding: 24px 18px 18px 18px; + background: #fff; +} + +.form-label { + font-weight: 600; + font-size: 16px; + color: #222; + display: block; + margin-bottom: 6px; +} + +.section-title { + font-size: 16px; + font-weight: 700; + color: #222; + margin-top: 28px; + margin-bottom: 12px; + letter-spacing: 0.5px; +} + +.section-block { + padding: 12px 0 8px 0; + border-bottom: 1px solid #f0f0f0; + margin-bottom: 8px; +} + +.tabs-bar { + .adm-tabs-header { + background: #f7f8fa; + border-radius: 8px; + margin-bottom: 8px; + } + .adm-tabs-tab { + font-size: 15px; + font-weight: 500; + padding: 8px 0; + } +} + +.collapse { + margin-top: 12px; + .adm-collapse-panel-content { + padding-bottom: 8px; + background: #f8fafc; + border-radius: 10px; + padding: 18px 14px 10px 14px; + margin-top: 2px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03); + } + .form-section { + margin-bottom: 22px; + } + .form-label { + font-size: 15px; + font-weight: 500; + margin-bottom: 4px; + color: #333; + } + .adm-input { + min-height: 42px; + font-size: 15px; + border-radius: 7px; + margin-bottom: 2px; + } +} + +.ai-row, +.section-block { + display: flex; + align-items: center; + gap: 12px; +} + +.ai-desc { + color: #888; + font-size: 13px; + flex: 1; +} + +.date-row, +.section-block { + display: flex; + gap: 12px; + align-items: center; +} + +.adm-input { + min-height: 44px; + font-size: 15px; + border-radius: 8px; +} + +.submit-btn { + margin-top: 32px; + height: 48px !important; + border-radius: 10px !important; + font-size: 17px; + font-weight: 600; + letter-spacing: 1px; +} + +@media (max-width: 600px) { + .form-main { + max-width: 100vw; + padding: 0; + } + .form-card { + border-radius: 0; + box-shadow: none; + padding: 16px 6px 12px 6px; + } + .section-title { + font-size: 15px; + margin-top: 22px; + margin-bottom: 8px; + } + .submit-btn { + height: 44px !important; + font-size: 15px; + } +} diff --git a/nkebao/src/pages/mobile/content/form/index.tsx b/nkebao/src/pages/mobile/content/form/index.tsx index ff099ed5..432b5ece 100644 --- a/nkebao/src/pages/mobile/content/form/index.tsx +++ b/nkebao/src/pages/mobile/content/form/index.tsx @@ -48,7 +48,7 @@ export default function ContentForm() { if (isEdit && id) { setLoading(true); getContentLibraryDetail(id) - .then((data) => { + .then(data => { setName(data.name || ""); setSourceType(data.sourceType === 1 ? "friends" : "groups"); setSelectedFriends(data.sourceFriends || []); @@ -66,7 +66,7 @@ export default function ContentForm() { end ? new Date(end) : null, ]); }) - .catch((e) => { + .catch(e => { Toast.show({ content: e?.message || "获取详情失败", position: "top", @@ -92,11 +92,11 @@ export default function ContentForm() { groupMembers: {}, keywordInclude: keywordsInclude .split(/,|,|\n|\s+/) - .map((s) => s.trim()) + .map(s => s.trim()) .filter(Boolean), keywordExclude: keywordsExclude .split(/,|,|\n|\s+/) - .map((s) => s.trim()) + .map(s => s.trim()) .filter(Boolean), aiPrompt, timeEnabled: dateRange[0] || dateRange[1] ? 1 : 0, @@ -148,7 +148,7 @@ export default function ContentForm() {
e.preventDefault()} + onSubmit={e => e.preventDefault()} autoComplete="off" >
@@ -159,7 +159,7 @@ export default function ContentForm() { setName(e.target.value)} + onChange={e => setName(e.target.value)} className={style["input"]} />
@@ -168,7 +168,7 @@ export default function ContentForm() {
setSourceType(key as "friends" | "groups")} + onChange={key => setSourceType(key as "friends" | "groups")} className={style["tabs-bar"]} > @@ -201,7 +201,7 @@ export default function ContentForm() {