feat: 本次提交更新内容如下
样式暂时可以了
This commit is contained in:
199
nkebao/package-lock.json
generated
199
nkebao/package-lock.json
generated
@@ -72,6 +72,7 @@
|
|||||||
"sonner": "^1.7.4",
|
"sonner": "^1.7.4",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"tdesign-mobile-react": "^0.16.0",
|
||||||
"vaul": "^0.9.6",
|
"vaul": "^0.9.6",
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
@@ -3620,6 +3621,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@popperjs/core": {
|
||||||
|
"version": "2.11.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
|
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/popperjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/number": {
|
"node_modules/@radix-ui/number": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz",
|
||||||
@@ -6376,6 +6387,24 @@
|
|||||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/@use-gesture/core": {
|
||||||
|
"version": "10.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
|
||||||
|
"integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@use-gesture/react": {
|
||||||
|
"version": "10.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
|
||||||
|
"integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@use-gesture/core": "10.3.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@webassemblyjs/ast": {
|
"node_modules/@webassemblyjs/ast": {
|
||||||
"version": "1.14.1",
|
"version": "1.14.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.14.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.14.1.tgz",
|
||||||
@@ -6658,6 +6687,30 @@
|
|||||||
"node": ">= 6.0.0"
|
"node": ">= 6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ahooks": {
|
||||||
|
"version": "3.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ahooks/-/ahooks-3.9.0.tgz",
|
||||||
|
"integrity": "sha512-r20/C38aFyU3Zqp3620gkdLnxmQhnmWORB3eGGTDlM4i/fOc0GUvM+f2oleMzEu7b3+pHXyzz+FB6ojxsUdYdw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.21.0",
|
||||||
|
"dayjs": "^1.9.1",
|
||||||
|
"intersection-observer": "^0.12.0",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"react-fast-compare": "^3.2.2",
|
||||||
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
|
"screenfull": "^5.0.0",
|
||||||
|
"tslib": "^2.4.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
|
||||||
@@ -7914,6 +7967,12 @@
|
|||||||
"url": "https://polar.sh/cva"
|
"url": "https://polar.sh/cva"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/classnames": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/clean-css": {
|
"node_modules/clean-css": {
|
||||||
"version": "5.3.3",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmmirror.com/clean-css/-/clean-css-5.3.3.tgz",
|
"resolved": "https://registry.npmmirror.com/clean-css/-/clean-css-5.3.3.tgz",
|
||||||
@@ -9179,6 +9238,12 @@
|
|||||||
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
|
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
|
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
|
||||||
@@ -9488,6 +9553,16 @@
|
|||||||
"utila": "~0.4"
|
"utila": "~0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-helpers": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.8.7",
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dom-serializer": {
|
"node_modules/dom-serializer": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
"resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
||||||
@@ -11739,6 +11814,21 @@
|
|||||||
"he": "bin/he"
|
"he": "bin/he"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hoist-non-react-statics": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"react-is": "^16.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hoist-non-react-statics/node_modules/react-is": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/hoopy": {
|
"node_modules/hoopy": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmmirror.com/hoopy/-/hoopy-0.1.4.tgz",
|
"resolved": "https://registry.npmmirror.com/hoopy/-/hoopy-0.1.4.tgz",
|
||||||
@@ -12183,6 +12273,12 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/intersection-observer": {
|
||||||
|
"version": "0.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz",
|
||||||
|
"integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
|
||||||
@@ -13805,6 +13901,15 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-cookie": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -14125,6 +14230,12 @@
|
|||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash-es": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.debounce": {
|
"node_modules/lodash.debounce": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
"resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||||
@@ -16954,6 +17065,12 @@
|
|||||||
"integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==",
|
"integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-fast-compare": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react-hook-form": {
|
"node_modules/react-hook-form": {
|
||||||
"version": "7.59.0",
|
"version": "7.59.0",
|
||||||
"resolved": "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.59.0.tgz",
|
"resolved": "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.59.0.tgz",
|
||||||
@@ -17209,6 +17326,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-transition-group": {
|
||||||
|
"version": "4.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
|
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"dom-helpers": "^5.0.1",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.6.0",
|
||||||
|
"react-dom": ">=16.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -17498,6 +17631,12 @@
|
|||||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/resize-observer-polyfill": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.10",
|
"version": "1.22.10",
|
||||||
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz",
|
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz",
|
||||||
@@ -17934,6 +18073,18 @@
|
|||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/screenfull": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/select-hose": {
|
"node_modules/select-hose": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/select-hose/-/select-hose-2.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/select-hose/-/select-hose-2.0.0.tgz",
|
||||||
@@ -18321,6 +18472,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/smoothscroll-polyfill": {
|
||||||
|
"version": "0.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/smoothscroll-polyfill/-/smoothscroll-polyfill-0.4.4.tgz",
|
||||||
|
"integrity": "sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/sockjs": {
|
"node_modules/sockjs": {
|
||||||
"version": "0.3.24",
|
"version": "0.3.24",
|
||||||
"resolved": "https://registry.npmmirror.com/sockjs/-/sockjs-0.3.24.tgz",
|
"resolved": "https://registry.npmmirror.com/sockjs/-/sockjs-0.3.24.tgz",
|
||||||
@@ -19280,6 +19437,42 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tdesign-icons-react": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tdesign-icons-react/-/tdesign-icons-react-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-Gpl1Kxkb8+YXYjMsSW5W3kWy/drVB/huFLIHWhmJdUnEwUAW0Il+nDyb/BsRi51jLnalBwjYbOELEggH2qp6FQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.16.5",
|
||||||
|
"classnames": "^2.2.6"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.13.1",
|
||||||
|
"react-dom": ">=16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tdesign-mobile-react": {
|
||||||
|
"version": "0.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tdesign-mobile-react/-/tdesign-mobile-react-0.16.0.tgz",
|
||||||
|
"integrity": "sha512-b5oI3Fk/Fpi64tH/d2+2wvQ5b6wKiRUGX4DBayIXL9LAqqxk2L2LxM8bXvq2rV3WdVg4scM38A/o8F54mzONXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"@use-gesture/react": "^10.2.10",
|
||||||
|
"ahooks": "^3.8.5",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"react-transition-group": "^4.4.2",
|
||||||
|
"smoothscroll-polyfill": "^0.4.4",
|
||||||
|
"tdesign-icons-react": "^0.5.0",
|
||||||
|
"tinycolor2": "^1.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"react-dom": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/temp-dir": {
|
"node_modules/temp-dir": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/temp-dir/-/temp-dir-2.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/temp-dir/-/temp-dir-2.0.0.tgz",
|
||||||
@@ -19452,6 +19645,12 @@
|
|||||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tinycolor2": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tmpl": {
|
"node_modules/tmpl": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmmirror.com/tmpl/-/tmpl-1.0.5.tgz",
|
"resolved": "https://registry.npmmirror.com/tmpl/-/tmpl-1.0.5.tgz",
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
"date-fns": "latest",
|
"date-fns": "latest",
|
||||||
"embla-carousel-react": "8.5.1",
|
"embla-carousel-react": "8.5.1",
|
||||||
"input-otp": "1.4.1",
|
"input-otp": "1.4.1",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.525.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-day-picker": "latest",
|
"react-day-picker": "latest",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@@ -67,6 +67,7 @@
|
|||||||
"sonner": "^1.7.4",
|
"sonner": "^1.7.4",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"tdesign-mobile-react": "^0.16.0",
|
||||||
"vaul": "^0.9.6",
|
"vaul": "^0.9.6",
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import WechatAccountDetail from './pages/wechat-accounts/WechatAccountDetail';
|
|||||||
import Workspace from './pages/workspace/Workspace';
|
import Workspace from './pages/workspace/Workspace';
|
||||||
import AutoLike from './pages/workspace/auto-like/AutoLike';
|
import AutoLike from './pages/workspace/auto-like/AutoLike';
|
||||||
import NewAutoLike from './pages/workspace/auto-like/NewAutoLike';
|
import NewAutoLike from './pages/workspace/auto-like/NewAutoLike';
|
||||||
|
import AutoLikeDetail from './pages/workspace/auto-like/AutoLikeDetail';
|
||||||
import AutoGroup from './pages/workspace/auto-group/AutoGroup';
|
import AutoGroup from './pages/workspace/auto-group/AutoGroup';
|
||||||
import AutoGroupDetail from './pages/workspace/auto-group/Detail';
|
import AutoGroupDetail from './pages/workspace/auto-group/Detail';
|
||||||
import GroupPush from './pages/workspace/group-push/GroupPush';
|
import GroupPush from './pages/workspace/group-push/GroupPush';
|
||||||
@@ -58,6 +59,7 @@ function App() {
|
|||||||
<Route path="/workspace" element={<Workspace />} />
|
<Route path="/workspace" element={<Workspace />} />
|
||||||
<Route path="/workspace/auto-like" element={<AutoLike />} />
|
<Route path="/workspace/auto-like" element={<AutoLike />} />
|
||||||
<Route path="/workspace/auto-like/new" element={<NewAutoLike />} />
|
<Route path="/workspace/auto-like/new" element={<NewAutoLike />} />
|
||||||
|
<Route path="/workspace/auto-like/:id" element={<AutoLikeDetail />} />
|
||||||
<Route path="/workspace/auto-group" element={<AutoGroup />} />
|
<Route path="/workspace/auto-group" element={<AutoGroup />} />
|
||||||
<Route path="/workspace/auto-group/:id" element={<AutoGroupDetail />} />
|
<Route path="/workspace/auto-group/:id" element={<AutoGroupDetail />} />
|
||||||
<Route path="/workspace/group-push" element={<GroupPush />} />
|
<Route path="/workspace/group-push" element={<GroupPush />} />
|
||||||
|
|||||||
@@ -1,35 +1,90 @@
|
|||||||
import { get, post } from './request';
|
import { get, post, del } from './request';
|
||||||
|
import {
|
||||||
export interface LikeTask {
|
LikeTask,
|
||||||
id: string;
|
CreateLikeTaskData,
|
||||||
name: string;
|
UpdateLikeTaskData,
|
||||||
status: 'running' | 'paused';
|
LikeRecord,
|
||||||
deviceCount: number;
|
ApiResponse,
|
||||||
targetGroup: string;
|
PaginatedResponse
|
||||||
likeCount: number;
|
} from '@/types/auto-like';
|
||||||
lastLikeTime: string;
|
|
||||||
createTime: string;
|
|
||||||
creator: string;
|
|
||||||
likeInterval: number;
|
|
||||||
maxLikesPerDay: number;
|
|
||||||
timeRange: { start: string; end: string };
|
|
||||||
contentTypes: string[];
|
|
||||||
targetTags: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 获取自动点赞任务列表
|
||||||
export async function fetchAutoLikeTasks(): Promise<LikeTask[]> {
|
export async function fetchAutoLikeTasks(): Promise<LikeTask[]> {
|
||||||
const res = await get('/api/workbench/auto-like/list');
|
try {
|
||||||
return res.data?.list || [];
|
const res = await get<ApiResponse<PaginatedResponse<LikeTask>>>('/v1/workbench/list?type=1&page=1&limit=100');
|
||||||
|
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
return res.data.list || [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取自动点赞任务失败:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteAutoLikeTask(id: string) {
|
// 获取单个任务详情
|
||||||
return post('/api/workbench/auto-like/delete', { id });
|
export async function fetchAutoLikeTaskDetail(id: string): Promise<LikeTask | null> {
|
||||||
|
try {
|
||||||
|
const res = await get<ApiResponse<LikeTask>>(`/v1/workbench/detail/${id}`);
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取任务详情失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toggleAutoLikeTask(id: string, status: string) {
|
// 创建自动点赞任务
|
||||||
return post('/api/workbench/auto-like/toggle', { id, status });
|
export async function createAutoLikeTask(data: CreateLikeTaskData): Promise<ApiResponse> {
|
||||||
|
return post('/v1/workbench/create', {
|
||||||
|
...data,
|
||||||
|
type: 1 // 自动点赞类型
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function copyAutoLikeTask(id: string) {
|
// 更新自动点赞任务
|
||||||
return post('/api/workbench/auto-like/copy', { id });
|
export async function updateAutoLikeTask(data: UpdateLikeTaskData): Promise<ApiResponse> {
|
||||||
}
|
return post('/v1/workbench/update', {
|
||||||
|
...data,
|
||||||
|
type: 1 // 自动点赞类型
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除自动点赞任务
|
||||||
|
export async function deleteAutoLikeTask(id: string): Promise<ApiResponse> {
|
||||||
|
return del('/v1/workbench/delete', { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换任务状态
|
||||||
|
export async function toggleAutoLikeTask(id: string, status: string): Promise<ApiResponse> {
|
||||||
|
return post('/v1/workbench/update-status', { id, status });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制自动点赞任务
|
||||||
|
export async function copyAutoLikeTask(id: string): Promise<ApiResponse> {
|
||||||
|
return post('/v1/workbench/copy', { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取点赞记录
|
||||||
|
export async function fetchLikeRecords(
|
||||||
|
workbenchId: string,
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 20
|
||||||
|
): Promise<PaginatedResponse<LikeRecord>> {
|
||||||
|
try {
|
||||||
|
const res = await get<ApiResponse<PaginatedResponse<LikeRecord>>>(`/v1/workbench/like-records?workbenchId=${workbenchId}&page=${page}&limit=${limit}`);
|
||||||
|
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
return { list: [], total: 0, page, limit };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取点赞记录失败:', error);
|
||||||
|
return { list: [], total: 0, page, limit };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { LikeTask, LikeRecord, CreateLikeTaskData };
|
||||||
@@ -1,69 +1,108 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useCallback } from "react";
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import {
|
import {
|
||||||
Plus,
|
|
||||||
Search,
|
|
||||||
RefreshCw,
|
|
||||||
MoreVertical,
|
|
||||||
Clock,
|
|
||||||
Edit,
|
|
||||||
Trash2,
|
|
||||||
Eye,
|
|
||||||
Copy,
|
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
|
MoreVertical,
|
||||||
|
Eye,
|
||||||
|
Edit,
|
||||||
|
Copy,
|
||||||
|
Trash2,
|
||||||
|
Clock,
|
||||||
|
Plus,
|
||||||
|
Filter,
|
||||||
|
Search,
|
||||||
|
RefreshCw,
|
||||||
Settings,
|
Settings,
|
||||||
Calendar,
|
Calendar,
|
||||||
Users,
|
Users,
|
||||||
ThumbsUp,
|
ThumbsUp,
|
||||||
} from 'lucide-react';
|
} from "lucide-react";
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from "@/components/ui/card";
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import { useToast } from "@/components/ui/toast";
|
||||||
DropdownMenu,
|
import { Progress } from "@/components/ui/progress";
|
||||||
DropdownMenuContent,
|
import { fetchAutoLikeTasks, deleteAutoLikeTask, toggleAutoLikeTask, copyAutoLikeTask, LikeTask } from '@/api/autoLike';
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
type CardMenuProps = {
|
||||||
} from '@/components/ui/dropdown-menu';
|
onView: () => void;
|
||||||
import Layout from '@/components/Layout';
|
onEdit: () => void;
|
||||||
import PageHeader from '@/components/PageHeader';
|
onCopy: () => void;
|
||||||
import BottomNav from '@/components/BottomNav';
|
onDelete: () => void;
|
||||||
import { useToast } from '@/components/ui/toast';
|
};
|
||||||
import '@/components/Layout.css';
|
|
||||||
import {
|
function CardMenu({ onView, onEdit, onCopy, onDelete }: CardMenuProps) {
|
||||||
fetchAutoLikeTasks,
|
const [open, setOpen] = React.useState(false);
|
||||||
deleteAutoLikeTask,
|
const menuRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
toggleAutoLikeTask,
|
|
||||||
copyAutoLikeTask,
|
React.useEffect(() => {
|
||||||
LikeTask,
|
function handleClickOutside(event: MouseEvent) {
|
||||||
} from '@/api/autoLike';
|
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (open) document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative" }}>
|
||||||
|
<button onClick={() => setOpen((v) => !v)} style={{ background: "none", border: "none", padding: 0, margin: 0, cursor: "pointer" }}>
|
||||||
|
<MoreVertical className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: 0,
|
||||||
|
top: 28,
|
||||||
|
background: "#fff",
|
||||||
|
borderRadius: 8,
|
||||||
|
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
|
||||||
|
zIndex: 100,
|
||||||
|
minWidth: 120,
|
||||||
|
padding: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div onClick={() => { onView(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}>
|
||||||
|
<Eye className="h-4 w-4 mr-2" />查看
|
||||||
|
</div>
|
||||||
|
<div onClick={() => { onEdit(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}>
|
||||||
|
<Edit className="h-4 w-4 mr-2" />编辑
|
||||||
|
</div>
|
||||||
|
<div onClick={() => { onCopy(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}>
|
||||||
|
<Copy className="h-4 w-4 mr-2" />复制
|
||||||
|
</div>
|
||||||
|
<div onClick={() => { onDelete(); setOpen(false); }} style={{ padding: 8, cursor: "pointer", display: "flex", alignItems: "center", borderRadius: 6, fontSize: 14, gap: 6, color: "#e53e3e", transition: "background .2s" }} onMouseOver={e => (e.currentTarget as HTMLDivElement).style.background="#f5f5f5"} onMouseOut={e => (e.currentTarget as HTMLDivElement).style.background=""}>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />删除
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function AutoLike() {
|
export default function AutoLike() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [expandedTaskId, setExpandedTaskId] = useState<string | null>(null);
|
const [expandedTaskId, setExpandedTaskId] = useState<string | null>(null);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [tasks, setTasks] = React.useState<LikeTask[]>([]);
|
||||||
const [tasks, setTasks] = useState<LikeTask[]>([]);
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
// 获取任务列表
|
|
||||||
const fetchTasks = useCallback(async () => {
|
const fetchTasks = useCallback(async () => {
|
||||||
setLoading(true);
|
|
||||||
try {
|
try {
|
||||||
const list = await fetchAutoLikeTasks();
|
const list = await fetchAutoLikeTasks();
|
||||||
setTasks(list);
|
setTasks(list);
|
||||||
} catch {
|
} catch (error) {
|
||||||
toast({ title: '获取任务失败', variant: 'destructive' });
|
toast({ title: "获取任务失败", variant: "destructive" });
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
}, [toast]);
|
}, [toast]);
|
||||||
|
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
fetchTasks();
|
fetchTasks();
|
||||||
}, [fetchTasks]);
|
}, [fetchTasks]);
|
||||||
|
|
||||||
@@ -72,13 +111,17 @@ export default function AutoLike() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
if (!window.confirm('确定要删除该任务吗?')) return;
|
if (!window.confirm("确定要删除该任务吗?")) return;
|
||||||
try {
|
try {
|
||||||
await deleteAutoLikeTask(id);
|
const response = await deleteAutoLikeTask(id);
|
||||||
toast({ title: '删除成功' });
|
if (response.code === 200) {
|
||||||
fetchTasks();
|
toast({ title: "删除成功" });
|
||||||
} catch {
|
fetchTasks();
|
||||||
toast({ title: '删除失败', variant: 'destructive' });
|
} else {
|
||||||
|
toast({ title: "删除失败", description: response.msg || "请稍后重试", variant: "destructive" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast({ title: "删除失败", description: "请稍后重试", variant: "destructive" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,188 +135,110 @@ export default function AutoLike() {
|
|||||||
|
|
||||||
const handleCopy = async (id: string) => {
|
const handleCopy = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
await copyAutoLikeTask(id);
|
const response = await copyAutoLikeTask(id);
|
||||||
toast({ title: '复制成功' });
|
if (response.code === 200) {
|
||||||
fetchTasks();
|
toast({ title: "复制成功" });
|
||||||
} catch {
|
fetchTasks();
|
||||||
toast({ title: '复制失败', variant: 'destructive' });
|
} else {
|
||||||
|
toast({ title: "复制失败", description: response.msg || "请稍后重试", variant: "destructive" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast({ title: "复制失败", description: "请稍后重试", variant: "destructive" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleTaskStatus = async (id: string, status: string) => {
|
const toggleTaskStatus = async (id: string, status: string) => {
|
||||||
try {
|
try {
|
||||||
await toggleAutoLikeTask(id, status);
|
// status: 'running' -> 2(要关闭),'paused' -> 1(要开启)
|
||||||
toast({ title: '操作成功' });
|
const newStatus = status === "running" ? 2 : 1;
|
||||||
fetchTasks();
|
const response = await toggleAutoLikeTask(id, String(newStatus));
|
||||||
} catch {
|
if (response.code === 200) {
|
||||||
toast({ title: '操作失败', variant: 'destructive' });
|
toast({ title: "操作成功" });
|
||||||
|
fetchTasks();
|
||||||
|
} else {
|
||||||
|
toast({ title: "操作失败", description: response.msg || "请稍后重试", variant: "destructive" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast({ title: "操作失败", description: "请稍后重试", variant: "destructive" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateNew = () => {
|
const handleCreateNew = () => {
|
||||||
navigate('/workspace/auto-like/new');
|
navigate("/workspace/auto-like/new");
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredTasks = tasks.filter((task) =>
|
const filteredTasks = tasks.filter((task) =>
|
||||||
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'running':
|
|
||||||
return 'bg-green-100 text-green-800';
|
|
||||||
case 'paused':
|
|
||||||
return 'bg-gray-100 text-gray-800';
|
|
||||||
default:
|
|
||||||
return 'bg-gray-100 text-gray-800';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusText = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'running':
|
|
||||||
return '进行中';
|
|
||||||
case 'paused':
|
|
||||||
return '已暂停';
|
|
||||||
default:
|
|
||||||
return '未知';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<div className="flex-1 bg-gray-50 min-h-screen pb-20">
|
||||||
header={
|
<header className="sticky top-0 z-10 bg-white border-b">
|
||||||
<PageHeader
|
<div className="flex items-center justify-between p-4">
|
||||||
title="自动点赞"
|
<div className="flex items-center space-x-3">
|
||||||
defaultBackPath="/workspace"
|
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||||
rightContent={
|
<ChevronDown className="h-5 w-5" />
|
||||||
<Button onClick={handleCreateNew}>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
新建任务
|
|
||||||
</Button>
|
</Button>
|
||||||
}
|
<h1 className="text-lg font-medium">自动点赞</h1>
|
||||||
/>
|
</div>
|
||||||
}
|
<Button onClick={handleCreateNew}>
|
||||||
footer={<BottomNav />}
|
<Plus className="h-4 w-4 mr-2" />新建任务
|
||||||
>
|
</Button>
|
||||||
<div className="bg-gray-50 min-h-screen pb-20">
|
</div>
|
||||||
|
</header>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{/* 搜索和筛选 */}
|
|
||||||
<Card className="p-4 mb-4">
|
<Card className="p-4 mb-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||||
<Input
|
<Input placeholder="搜索任务名称" className="pl-9" value={searchTerm} onChange={e => setSearchTerm(e.target.value)} />
|
||||||
placeholder="搜索任务名称"
|
|
||||||
className="pl-9"
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/* 移除筛选按钮 */}
|
|
||||||
{/* <Button variant="outline" size="icon">
|
|
||||||
<Filter className="h-4 w-4" />
|
|
||||||
</Button> */}
|
|
||||||
<Button variant="outline" size="icon" onClick={fetchTasks}>
|
<Button variant="outline" size="icon" onClick={fetchTasks}>
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 任务列表 */}
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{loading ? (
|
{filteredTasks.map((task) => (
|
||||||
<Card className="p-8 text-center">
|
|
||||||
<ThumbsUp className="h-12 w-12 text-gray-300 mx-auto mb-3" />
|
|
||||||
<p className="text-gray-500 text-lg font-medium mb-2">加载中...</p>
|
|
||||||
<p className="text-gray-400 text-sm mb-4">请稍候,正在获取任务列表</p>
|
|
||||||
</Card>
|
|
||||||
) : filteredTasks.length === 0 ? (
|
|
||||||
<Card className="p-8 text-center">
|
|
||||||
<ThumbsUp className="h-12 w-12 text-gray-300 mx-auto mb-3" />
|
|
||||||
<p className="text-gray-500 text-lg font-medium mb-2">暂无点赞任务</p>
|
|
||||||
<p className="text-gray-400 text-sm mb-4">创建您的第一个自动点赞任务</p>
|
|
||||||
<Button onClick={handleCreateNew}>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
创建第一个任务
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
filteredTasks.map((task) => (
|
|
||||||
<Card key={task.id} className="p-4">
|
<Card key={task.id} className="p-4">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<h3 className="font-medium">{task.name}</h3>
|
<h3 className="font-medium">{task.name}</h3>
|
||||||
<Badge className={getStatusColor(task.status)}>
|
<Badge variant={task.status === "running" ? "success" : "secondary"}>
|
||||||
{getStatusText(task.status)}
|
{task.status === "running" ? "进行中" : "已暂停"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch
|
<Switch checked={task.status === "running"} onCheckedChange={() => toggleTaskStatus(task.id, task.status)} />
|
||||||
checked={task.status === 'running'}
|
<CardMenu
|
||||||
onCheckedChange={() => toggleTaskStatus(task.id, task.status === 'running' ? 'paused' : 'running')}
|
onView={() => handleView(task.id)}
|
||||||
/>
|
onEdit={() => handleEdit(task.id)}
|
||||||
<DropdownMenu>
|
onCopy={() => handleCopy(task.id)}
|
||||||
<DropdownMenuTrigger asChild>
|
onDelete={() => handleDelete(task.id)}
|
||||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
/>
|
||||||
<MoreVertical className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem onClick={() => handleView(task.id)}>
|
|
||||||
<Eye className="h-4 w-4 mr-2" />
|
|
||||||
查看
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => handleEdit(task.id)}>
|
|
||||||
<Edit className="h-4 w-4 mr-2" />
|
|
||||||
编辑
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => handleCopy(task.id)}>
|
|
||||||
<Copy className="h-4 w-4 mr-2" />
|
|
||||||
复制
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => handleDelete(task.id)}>
|
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
|
||||||
删除
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
<div>执行设备:{task.deviceCount} 个</div>
|
<div>执行设备:{task.deviceCount} 个</div>
|
||||||
<div>目标人群:{task.targetGroup}</div>
|
<div>目标人群:{task.targetGroup}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
<div>已点赞:{task.likeCount} 次</div>
|
<div>已点赞:{task.likeCount} 次</div>
|
||||||
<div>创建人:{task.creator}</div>
|
<div>创建人:{task.creator}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
|
||||||
<div className="flex items-center justify-between text-xs text-gray-500 border-t pt-4">
|
<div className="flex items-center">
|
||||||
<div className="flex items-center">
|
<Clock className="w-4 h-4 mr-1" />上次点赞:{task.lastLikeTime}
|
||||||
<Clock className="w-4 h-4 mr-1" />
|
</div>
|
||||||
上次点赞:{task.lastLikeTime}
|
<div className="flex items-center">
|
||||||
</div>
|
<span>创建时间:{task.createTime}</span>
|
||||||
<div className="flex items-center">
|
<Button variant="ghost" size="sm" className="ml-2 p-0 h-6 w-6" onClick={() => toggleExpand(task.id)}>
|
||||||
<span>创建时间:{task.createTime}</span>
|
{expandedTaskId === task.id ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||||
<Button
|
</Button>
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="ml-2 p-0 h-6 w-6"
|
|
||||||
onClick={() => toggleExpand(task.id)}
|
|
||||||
>
|
|
||||||
{expandedTaskId === task.id ? (
|
|
||||||
<ChevronUp className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{expandedTaskId === task.id && (
|
{expandedTaskId === task.id && (
|
||||||
<div className="mt-4 pt-4 border-t border-dashed">
|
<div className="mt-4 pt-4 border-t border-dashed">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
@@ -285,21 +250,20 @@ export default function AutoLike() {
|
|||||||
<div className="space-y-2 pl-7">
|
<div className="space-y-2 pl-7">
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-gray-500">点赞间隔:</span>
|
<span className="text-gray-500">点赞间隔:</span>
|
||||||
<span>{task.likeInterval} 秒</span>
|
<span>{task.likeInterval} 秒</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-gray-500">每日最大点赞数:</span>
|
<span className="text-gray-500">每日最大点赞数:</span>
|
||||||
<span>{task.maxLikesPerDay} 次</span>
|
<span>{task.maxLikesPerDay} 次</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-gray-500">执行时间段:</span>
|
<span className="text-gray-500">执行时间段:</span>
|
||||||
<span>
|
<span>
|
||||||
{task.timeRange.start} - {task.timeRange.end}
|
{task.timeRange.start} - {task.timeRange.end}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Users className="h-5 w-5 mr-2 text-gray-500" />
|
<Users className="h-5 w-5 mr-2 text-gray-500" />
|
||||||
@@ -307,15 +271,14 @@ export default function AutoLike() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 pl-7">
|
<div className="space-y-2 pl-7">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{task.targetTags.map((tag) => (
|
{task.targetTags.map((tag) => (
|
||||||
<Badge key={tag} variant="outline" className="bg-gray-50">
|
<Badge key={tag} variant="outline" className="bg-gray-50">
|
||||||
{tag}
|
{tag}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<ThumbsUp className="h-5 w-5 mr-2 text-gray-500" />
|
<ThumbsUp className="h-5 w-5 mr-2 text-gray-500" />
|
||||||
@@ -323,42 +286,39 @@ export default function AutoLike() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 pl-7">
|
<div className="space-y-2 pl-7">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{task.contentTypes.map((type) => (
|
{task.contentTypes.map((type) => (
|
||||||
<Badge key={type} variant="outline" className="bg-gray-50">
|
<Badge key={type} variant="outline" className="bg-gray-50">
|
||||||
{type === 'text' ? '文字' : type === 'image' ? '图片' : '视频'}
|
{type === "text" ? "文字" : type === "image" ? "图片" : "视频"}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="flex items-center">
|
||||||
<div className="flex items-center">
|
<Calendar className="h-5 w-5 mr-2 text-gray-500" />
|
||||||
<Calendar className="h-5 w-5 mr-2 text-gray-500" />
|
<h4 className="font-medium">执行进度</h4>
|
||||||
<h4 className="font-medium">执行进度</h4>
|
</div>
|
||||||
</div>
|
<div className="space-y-2 pl-7">
|
||||||
<div className="space-y-2 pl-7">
|
<div className="flex justify-between text-sm mb-1">
|
||||||
<div className="flex justify-between text-sm mb-1">
|
<span className="text-gray-500">今日已点赞:</span>
|
||||||
<span className="text-gray-500">今日已点赞:</span>
|
<span>
|
||||||
<span>
|
{task.likeCount} / {task.maxLikesPerDay}
|
||||||
{task.likeCount} / {task.maxLikesPerDay}
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
<Progress
|
||||||
<Progress
|
value={(task.likeCount / task.maxLikesPerDay) * 100}
|
||||||
value={(task.likeCount / task.maxLikesPerDay) * 100}
|
className="h-2"
|
||||||
className="h-2"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
))
|
))}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
452
nkebao/src/pages/workspace/auto-like/AutoLikeDetail.tsx
Normal file
452
nkebao/src/pages/workspace/auto-like/AutoLikeDetail.tsx
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Edit,
|
||||||
|
Trash2,
|
||||||
|
Copy,
|
||||||
|
ThumbsUp,
|
||||||
|
Settings,
|
||||||
|
Calendar,
|
||||||
|
Eye,
|
||||||
|
RefreshCw,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import Layout from '@/components/Layout';
|
||||||
|
import PageHeader from '@/components/PageHeader';
|
||||||
|
import BottomNav from '@/components/BottomNav';
|
||||||
|
import { useToast } from '@/components/ui/toast';
|
||||||
|
import '@/components/Layout.css';
|
||||||
|
import {
|
||||||
|
fetchAutoLikeTaskDetail,
|
||||||
|
toggleAutoLikeTask,
|
||||||
|
deleteAutoLikeTask,
|
||||||
|
copyAutoLikeTask,
|
||||||
|
fetchLikeRecords,
|
||||||
|
LikeTask,
|
||||||
|
LikeRecord,
|
||||||
|
} from '@/api/autoLike';
|
||||||
|
|
||||||
|
export default function AutoLikeDetail() {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [task, setTask] = useState<LikeTask | null>(null);
|
||||||
|
const [records, setRecords] = useState<LikeRecord[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [recordsLoading, setRecordsLoading] = useState(false);
|
||||||
|
|
||||||
|
const fetchTaskDetail = useCallback(async () => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const taskData = await fetchAutoLikeTaskDetail(id);
|
||||||
|
if (taskData) {
|
||||||
|
setTask(taskData);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: '任务不存在',
|
||||||
|
description: '该任务可能已被删除',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
navigate('/workspace/auto-like');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取任务详情失败:', error);
|
||||||
|
toast({
|
||||||
|
title: '获取失败',
|
||||||
|
description: '请稍后重试',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [id, toast, navigate]);
|
||||||
|
|
||||||
|
const fetchRecords = useCallback(async () => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
setRecordsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetchLikeRecords(id, 1, 20);
|
||||||
|
setRecords(response.list || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取点赞记录失败:', error);
|
||||||
|
} finally {
|
||||||
|
setRecordsLoading(false);
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
fetchTaskDetail();
|
||||||
|
fetchRecords();
|
||||||
|
}
|
||||||
|
}, [id, fetchTaskDetail, fetchRecords]);
|
||||||
|
|
||||||
|
const handleToggleStatus = async () => {
|
||||||
|
if (!task) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newStatus = task.status === 'running' ? 'paused' : 'running';
|
||||||
|
const response = await toggleAutoLikeTask(task.id, newStatus);
|
||||||
|
|
||||||
|
if (response.code === 200) {
|
||||||
|
setTask(prev => prev ? { ...prev, status: newStatus } : null);
|
||||||
|
toast({ title: '操作成功' });
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: '操作失败',
|
||||||
|
description: response.msg || '请稍后重试',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('操作失败:', error);
|
||||||
|
toast({
|
||||||
|
title: '操作失败',
|
||||||
|
description: '请稍后重试',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!task || !window.confirm('确定要删除该任务吗?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await deleteAutoLikeTask(task.id);
|
||||||
|
if (response.code === 200) {
|
||||||
|
toast({ title: '删除成功' });
|
||||||
|
navigate('/workspace/auto-like');
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: '删除失败',
|
||||||
|
description: response.msg || '请稍后重试',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
toast({
|
||||||
|
title: '删除失败',
|
||||||
|
description: '请稍后重试',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
if (!task) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await copyAutoLikeTask(task.id);
|
||||||
|
if (response.code === 200) {
|
||||||
|
toast({ title: '复制成功' });
|
||||||
|
navigate('/workspace/auto-like');
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: '复制失败',
|
||||||
|
description: response.msg || '请稍后重试',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制失败:', error);
|
||||||
|
toast({
|
||||||
|
title: '复制失败',
|
||||||
|
description: '请稍后重试',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'running':
|
||||||
|
return 'bg-green-100 text-green-800';
|
||||||
|
case 'paused':
|
||||||
|
return 'bg-gray-100 text-gray-800';
|
||||||
|
case 'completed':
|
||||||
|
return 'bg-blue-100 text-blue-800';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'running':
|
||||||
|
return '进行中';
|
||||||
|
case 'paused':
|
||||||
|
return '已暂停';
|
||||||
|
case 'completed':
|
||||||
|
return '已完成';
|
||||||
|
default:
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
header={<PageHeader title="任务详情" defaultBackPath="/workspace/auto-like" />}
|
||||||
|
footer={<BottomNav />}
|
||||||
|
>
|
||||||
|
<div className="bg-gray-50 min-h-screen pb-20">
|
||||||
|
<div className="p-4">
|
||||||
|
<Card className="p-8 text-center">
|
||||||
|
<RefreshCw className="h-12 w-12 text-gray-300 mx-auto mb-3 animate-spin" />
|
||||||
|
<p className="text-gray-500 text-lg font-medium mb-2">加载中...</p>
|
||||||
|
<p className="text-gray-400 text-sm">请稍候,正在获取任务详情</p>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
header={
|
||||||
|
<PageHeader
|
||||||
|
title="任务详情"
|
||||||
|
defaultBackPath="/workspace/auto-like"
|
||||||
|
rightContent={
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<Settings className="h-4 w-4 mr-2" />
|
||||||
|
操作
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => navigate(`/workspace/auto-like/${task.id}/edit`)}>
|
||||||
|
<Edit className="h-4 w-4 mr-2" />
|
||||||
|
编辑
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={handleCopy}>
|
||||||
|
<Copy className="h-4 w-4 mr-2" />
|
||||||
|
复制
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={handleDelete}>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
删除
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
footer={<BottomNav />}
|
||||||
|
>
|
||||||
|
<div className="bg-gray-50 min-h-screen pb-20">
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{/* 任务基本信息 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="flex items-center">
|
||||||
|
<ThumbsUp className="h-5 w-5 mr-2" />
|
||||||
|
{task.name}
|
||||||
|
</CardTitle>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Badge className={getStatusColor(task.status)}>
|
||||||
|
{getStatusText(task.status)}
|
||||||
|
</Badge>
|
||||||
|
<Switch
|
||||||
|
checked={task.status === 'running'}
|
||||||
|
onCheckedChange={handleToggleStatus}
|
||||||
|
disabled={task.status === 'completed'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="text-gray-500">创建时间</div>
|
||||||
|
<div className="font-medium">{task.createTime}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="text-gray-500">创建人</div>
|
||||||
|
<div className="font-medium">{task.creator}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="text-gray-500">执行设备</div>
|
||||||
|
<div className="font-medium">{task.deviceCount} 个</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="text-gray-500">目标人群</div>
|
||||||
|
<div className="font-medium">{task.targetGroup}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 执行进度 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center">
|
||||||
|
<Calendar className="h-5 w-5 mr-2" />
|
||||||
|
执行进度
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-500">今日已点赞</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{task.todayLikeCount} / {task.maxLikesPerDay}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
value={(task.todayLikeCount / task.maxLikesPerDay) * 100}
|
||||||
|
className="h-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<div className="text-gray-500">总点赞数</div>
|
||||||
|
<div className="font-medium">{task.totalLikeCount} 次</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-gray-500">上次点赞</div>
|
||||||
|
<div className="font-medium">{task.lastLikeTime}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 任务配置 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center">
|
||||||
|
<Settings className="h-5 w-5 mr-2" />
|
||||||
|
任务配置
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="text-gray-500">点赞间隔</div>
|
||||||
|
<div className="font-medium">{task.likeInterval} 秒</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="text-gray-500">每日最大点赞数</div>
|
||||||
|
<div className="font-medium">{task.maxLikesPerDay} 次</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="text-gray-500">单个好友最大点赞数</div>
|
||||||
|
<div className="font-medium">{task.friendMaxLikes} 次</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="text-gray-500">执行时间段</div>
|
||||||
|
<div className="font-medium">
|
||||||
|
{task.timeRange.start} - {task.timeRange.end}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm text-gray-500">内容类型</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{task.contentTypes.map((type) => (
|
||||||
|
<Badge key={type} variant="outline">
|
||||||
|
{type === 'text' ? '文字' : type === 'image' ? '图片' : type === 'video' ? '视频' : '链接'}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{task.targetTags.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm text-gray-500">目标标签</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{task.targetTags.map((tag) => (
|
||||||
|
<Badge key={tag} variant="outline">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{task.enableFriendTags && task.friendTags && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm text-gray-500">好友标签</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="outline">{task.friendTags}</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 点赞记录 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="flex items-center">
|
||||||
|
<Eye className="h-5 w-5 mr-2" />
|
||||||
|
点赞记录
|
||||||
|
</CardTitle>
|
||||||
|
<Button variant="outline" size="sm" onClick={fetchRecords}>
|
||||||
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{recordsLoading ? (
|
||||||
|
<div className="text-center py-4">
|
||||||
|
<RefreshCw className="h-6 w-6 text-gray-400 mx-auto animate-spin" />
|
||||||
|
<p className="text-gray-500 text-sm mt-2">加载中...</p>
|
||||||
|
</div>
|
||||||
|
) : records.length === 0 ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<ThumbsUp className="h-12 w-12 text-gray-300 mx-auto mb-3" />
|
||||||
|
<p className="text-gray-500">暂无点赞记录</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{records.slice(0, 10).map((record) => (
|
||||||
|
<div key={record.id} className="flex items-center space-x-3 p-3 border rounded-lg">
|
||||||
|
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||||
|
<ThumbsUp className="h-4 w-4 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-sm font-medium truncate">
|
||||||
|
{record.friendName}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 truncate">
|
||||||
|
{record.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{record.likeTime}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,8 @@ import PageHeader from '@/components/PageHeader';
|
|||||||
import BottomNav from '@/components/BottomNav';
|
import BottomNav from '@/components/BottomNav';
|
||||||
import { useToast } from '@/components/ui/toast';
|
import { useToast } from '@/components/ui/toast';
|
||||||
import '@/components/Layout.css';
|
import '@/components/Layout.css';
|
||||||
|
import { createAutoLikeTask, CreateLikeTaskData } from '@/api/autoLike';
|
||||||
|
import { ContentType } from '@/types/auto-like';
|
||||||
|
|
||||||
interface Device {
|
interface Device {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -33,20 +35,17 @@ export default function NewAutoLike() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [currentStep, setCurrentStep] = useState(1);
|
const [currentStep, setCurrentStep] = useState(1);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState<CreateLikeTaskData>({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
interval: 30,
|
||||||
likeInterval: 30,
|
maxLikes: 100,
|
||||||
maxLikesPerDay: 100,
|
|
||||||
friendMaxLikes: 10,
|
friendMaxLikes: 10,
|
||||||
startTime: '09:00',
|
startTime: '09:00',
|
||||||
endTime: '18:00',
|
endTime: '18:00',
|
||||||
contentTypes: ['text', 'image'],
|
contentTypes: ['text', 'image'],
|
||||||
includeKeywords: '',
|
devices: [],
|
||||||
excludeKeywords: '',
|
friends: [],
|
||||||
selectedDevices: [] as string[],
|
targetTags: [],
|
||||||
selectedGroups: [] as string[],
|
|
||||||
targetTags: [] as string[],
|
|
||||||
enableFriendTags: false,
|
enableFriendTags: false,
|
||||||
friendTags: '',
|
friendTags: '',
|
||||||
});
|
});
|
||||||
@@ -70,48 +69,48 @@ export default function NewAutoLike() {
|
|||||||
'VIP', '高价值', '活跃', '互动', '潜在', '新客户', '男性', '女性', '青年', '中年', '高收入', '中收入'
|
'VIP', '高价值', '活跃', '互动', '潜在', '新客户', '男性', '女性', '青年', '中年', '高收入', '中收入'
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleInputChange = (field: string, value: any) => {
|
const handleInputChange = (field: keyof CreateLikeTaskData, value: any) => {
|
||||||
setFormData(prev => ({ ...prev, [field]: value }));
|
setFormData((prev: CreateLikeTaskData) => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeviceToggle = (deviceId: string) => {
|
const handleDeviceToggle = (deviceId: string) => {
|
||||||
setFormData(prev => ({
|
setFormData((prev: CreateLikeTaskData) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selectedDevices: prev.selectedDevices.includes(deviceId)
|
devices: prev.devices.includes(deviceId)
|
||||||
? prev.selectedDevices.filter(id => id !== deviceId)
|
? prev.devices.filter((id: string) => id !== deviceId)
|
||||||
: [...prev.selectedDevices, deviceId]
|
: [...prev.devices, deviceId]
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGroupToggle = (groupId: string) => {
|
const handleGroupToggle = (groupId: string) => {
|
||||||
setFormData(prev => ({
|
setFormData((prev: CreateLikeTaskData) => ({
|
||||||
...prev,
|
...prev,
|
||||||
selectedGroups: prev.selectedGroups.includes(groupId)
|
friends: prev.friends?.includes(groupId)
|
||||||
? prev.selectedGroups.filter(id => id !== groupId)
|
? prev.friends.filter((id: string) => id !== groupId)
|
||||||
: [...prev.selectedGroups, groupId]
|
: [...(prev.friends || []), groupId]
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTagToggle = (tag: string) => {
|
const handleTagToggle = (tag: string) => {
|
||||||
setFormData(prev => ({
|
setFormData((prev: CreateLikeTaskData) => ({
|
||||||
...prev,
|
...prev,
|
||||||
targetTags: prev.targetTags.includes(tag)
|
targetTags: prev.targetTags.includes(tag)
|
||||||
? prev.targetTags.filter(t => t !== tag)
|
? prev.targetTags.filter((t: string) => t !== tag)
|
||||||
: [...prev.targetTags, tag]
|
: [...prev.targetTags, tag]
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContentTypeToggle = (type: string) => {
|
const handleContentTypeToggle = (type: ContentType) => {
|
||||||
setFormData(prev => ({
|
setFormData((prev: CreateLikeTaskData) => ({
|
||||||
...prev,
|
...prev,
|
||||||
contentTypes: prev.contentTypes.includes(type)
|
contentTypes: prev.contentTypes.includes(type)
|
||||||
? prev.contentTypes.filter(t => t !== type)
|
? prev.contentTypes.filter((t: ContentType) => t !== type)
|
||||||
: [...prev.contentTypes, type]
|
: [...prev.contentTypes, type]
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
if (currentStep === 1 && (!formData.name || formData.selectedDevices.length === 0)) {
|
if (currentStep === 1 && (!formData.name || formData.devices.length === 0)) {
|
||||||
toast({
|
toast({
|
||||||
title: '请完善信息',
|
title: '请完善信息',
|
||||||
description: '请填写任务名称并选择至少一个设备',
|
description: '请填写任务名称并选择至少一个设备',
|
||||||
@@ -119,7 +118,7 @@ export default function NewAutoLike() {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentStep === 2 && formData.selectedGroups.length === 0) {
|
if (currentStep === 2 && (!formData.friends || formData.friends.length === 0)) {
|
||||||
toast({
|
toast({
|
||||||
title: '请选择目标人群',
|
title: '请选择目标人群',
|
||||||
description: '请至少选择一个目标人群',
|
description: '请至少选择一个目标人群',
|
||||||
@@ -136,16 +135,23 @@ export default function NewAutoLike() {
|
|||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
// 模拟API调用
|
const response = await createAutoLikeTask(formData);
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
toast({
|
if (response.code === 200) {
|
||||||
title: '创建成功',
|
toast({
|
||||||
description: '自动点赞任务已创建',
|
title: '创建成功',
|
||||||
});
|
description: '自动点赞任务已创建',
|
||||||
|
});
|
||||||
navigate('/workspace/auto-like');
|
navigate('/workspace/auto-like');
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: '创建失败',
|
||||||
|
description: response.msg || '请稍后重试',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('创建失败:', error);
|
||||||
toast({
|
toast({
|
||||||
title: '创建失败',
|
title: '创建失败',
|
||||||
description: '请检查网络连接后重试',
|
description: '请检查网络连接后重试',
|
||||||
@@ -200,30 +206,21 @@ export default function NewAutoLike() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 步骤1: 基本设置 */}
|
{/* 步骤内容 */}
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>任务信息</CardTitle>
|
<CardTitle>任务基本信息</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="name">任务名称</Label>
|
<Label htmlFor="name">任务名称</Label>
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
placeholder="请输入任务名称"
|
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||||
/>
|
placeholder="请输入任务名称"
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="description">任务描述</Label>
|
|
||||||
<Input
|
|
||||||
id="description"
|
|
||||||
placeholder="请输入任务描述(可选)"
|
|
||||||
value={formData.description}
|
|
||||||
onChange={(e) => handleInputChange('description', e.target.value)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -231,36 +228,33 @@ export default function NewAutoLike() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>执行设备</CardTitle>
|
<CardTitle>选择执行设备</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
{devices.map((device) => (
|
{devices.map((device) => (
|
||||||
<div
|
<div
|
||||||
key={device.id}
|
key={device.id}
|
||||||
className={`flex items-center justify-between p-3 rounded-lg border cursor-pointer ${
|
className={`flex items-center p-3 border rounded-lg cursor-pointer transition-colors ${
|
||||||
formData.selectedDevices.includes(device.id)
|
formData.devices.includes(device.id)
|
||||||
? 'border-blue-500 bg-blue-50'
|
? 'border-blue-500 bg-blue-50'
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleDeviceToggle(device.id)}
|
onClick={() => handleDeviceToggle(device.id)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className={`w-2 h-2 rounded-full ${
|
|
||||||
device.status === 'online' ? 'bg-green-500' : 'bg-gray-400'
|
|
||||||
}`} />
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">{device.name}</div>
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
{device.status === 'online' ? '在线' : '离线'} ·
|
|
||||||
最后活跃: {device.lastActive}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={formData.selectedDevices.includes(device.id)}
|
checked={formData.devices.includes(device.id)}
|
||||||
onChange={() => handleDeviceToggle(device.id)}
|
onChange={() => handleDeviceToggle(device.id)}
|
||||||
/>
|
/>
|
||||||
|
<div className="ml-3 flex-1">
|
||||||
|
<div className="font-medium">{device.name}</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
状态: {device.status === 'online' ? '在线' : '离线'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`w-2 h-2 rounded-full ${
|
||||||
|
device.status === 'online' ? 'bg-green-500' : 'bg-gray-400'
|
||||||
|
}`} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -269,42 +263,34 @@ export default function NewAutoLike() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 步骤2: 目标人群 */}
|
|
||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>目标人群</CardTitle>
|
<CardTitle>选择目标人群</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
{targetGroups.map((group) => (
|
{targetGroups.map((group) => (
|
||||||
<div
|
<div
|
||||||
key={group.id}
|
key={group.id}
|
||||||
className={`flex items-center justify-between p-3 rounded-lg border cursor-pointer ${
|
className={`flex items-center p-3 border rounded-lg cursor-pointer transition-colors ${
|
||||||
formData.selectedGroups.includes(group.id)
|
formData.friends?.includes(group.id)
|
||||||
? 'border-blue-500 bg-blue-50'
|
? 'border-blue-500 bg-blue-50'
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleGroupToggle(group.id)}
|
onClick={() => handleGroupToggle(group.id)}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<div className="font-medium">{group.name}</div>
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
{group.count} 个好友
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-1 mt-1">
|
|
||||||
{group.tags.map((tag) => (
|
|
||||||
<Badge key={tag} variant="outline" className="text-xs">
|
|
||||||
{tag}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={formData.selectedGroups.includes(group.id)}
|
checked={formData.friends?.includes(group.id) || false}
|
||||||
onChange={() => handleGroupToggle(group.id)}
|
onChange={() => handleGroupToggle(group.id)}
|
||||||
/>
|
/>
|
||||||
|
<div className="ml-3 flex-1">
|
||||||
|
<div className="font-medium">{group.name}</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
人数: {group.count} | 标签: {group.tags.join(', ')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -313,14 +299,14 @@ export default function NewAutoLike() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>标签筛选</CardTitle>
|
<CardTitle>选择标签</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{availableTags.map((tag) => (
|
{availableTags.map((tag) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={tag}
|
key={tag}
|
||||||
variant={formData.targetTags.includes(tag) ? 'default' : 'outline'}
|
variant={formData.targetTags.includes(tag) ? "default" : "outline"}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => handleTagToggle(tag)}
|
onClick={() => handleTagToggle(tag)}
|
||||||
>
|
>
|
||||||
@@ -333,7 +319,6 @@ export default function NewAutoLike() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 步骤3: 高级设置 */}
|
|
||||||
{currentStep === 3 && (
|
{currentStep === 3 && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
@@ -344,38 +329,45 @@ export default function NewAutoLike() {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="interval">点赞间隔(秒)</Label>
|
<Label htmlFor="interval">点赞间隔(秒)</Label>
|
||||||
<Input
|
<input
|
||||||
id="interval"
|
id="interval"
|
||||||
value={formData.likeInterval.toString()}
|
type="number"
|
||||||
onChange={(e) => handleInputChange('likeInterval', parseInt(e.target.value))}
|
className="w-full border rounded px-3 py-2 text-sm"
|
||||||
|
value={formData.interval}
|
||||||
|
onChange={(e) => handleInputChange('interval', parseInt(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="maxLikes">每日最大点赞数</Label>
|
<Label htmlFor="maxLikes">每日最大点赞数</Label>
|
||||||
<Input
|
<input
|
||||||
id="maxLikes"
|
id="maxLikes"
|
||||||
value={formData.maxLikesPerDay.toString()}
|
type="number"
|
||||||
onChange={(e) => handleInputChange('maxLikesPerDay', parseInt(e.target.value))}
|
className="w-full border rounded px-3 py-2 text-sm"
|
||||||
|
value={formData.maxLikes}
|
||||||
|
onChange={(e) => handleInputChange('maxLikes', parseInt(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="friendMaxLikes">单个好友每日最大点赞数</Label>
|
<Label htmlFor="friendMaxLikes">单个好友每日最大点赞数</Label>
|
||||||
<Input
|
<input
|
||||||
id="friendMaxLikes"
|
id="friendMaxLikes"
|
||||||
value={formData.friendMaxLikes.toString()}
|
type="number"
|
||||||
|
className="w-full border rounded px-3 py-2 text-sm"
|
||||||
|
value={formData.friendMaxLikes}
|
||||||
onChange={(e) => handleInputChange('friendMaxLikes', parseInt(e.target.value))}
|
onChange={(e) => handleInputChange('friendMaxLikes', parseInt(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="startTime">开始时间</Label>
|
<Label htmlFor="startTime">开始时间</Label>
|
||||||
<input
|
<input
|
||||||
id="startTime"
|
id="startTime"
|
||||||
type="time"
|
type="time"
|
||||||
|
className="w-full border rounded px-3 py-2 text-sm"
|
||||||
value={formData.startTime}
|
value={formData.startTime}
|
||||||
onChange={(e) => handleInputChange('startTime', e.target.value)}
|
onChange={(e) => handleInputChange('startTime', e.target.value)}
|
||||||
className="flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -383,138 +375,54 @@ export default function NewAutoLike() {
|
|||||||
<input
|
<input
|
||||||
id="endTime"
|
id="endTime"
|
||||||
type="time"
|
type="time"
|
||||||
|
className="w-full border rounded px-3 py-2 text-sm"
|
||||||
value={formData.endTime}
|
value={formData.endTime}
|
||||||
onChange={(e) => handleInputChange('endTime', e.target.value)}
|
onChange={(e) => handleInputChange('endTime', e.target.value)}
|
||||||
className="flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>内容筛选</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex flex-wrap gap-2 mb-4">
|
|
||||||
{[
|
|
||||||
{ value: 'text', label: '文字' },
|
|
||||||
{ value: 'image', label: '图片' },
|
|
||||||
{ value: 'video', label: '视频' },
|
|
||||||
{ value: 'link', label: '链接' },
|
|
||||||
].map((type) => (
|
|
||||||
<Badge
|
|
||||||
key={type.value}
|
|
||||||
variant={formData.contentTypes.includes(type.value) ? 'default' : 'outline'}
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => handleContentTypeToggle(type.value)}
|
|
||||||
>
|
|
||||||
{type.label}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="mb-2">
|
|
||||||
<Label htmlFor="includeKeywords">包含关键词</Label>
|
|
||||||
<textarea
|
|
||||||
id="includeKeywords"
|
|
||||||
className="w-full border rounded p-2 text-sm"
|
|
||||||
placeholder="多个关键词用逗号或换行分隔"
|
|
||||||
value={formData.includeKeywords}
|
|
||||||
onChange={e => handleInputChange('includeKeywords', e.target.value)}
|
|
||||||
rows={2}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="excludeKeywords">排除关键词</Label>
|
<Label>内容类型</Label>
|
||||||
<textarea
|
<div className="flex flex-wrap gap-2 mt-2">
|
||||||
id="excludeKeywords"
|
{(['text', 'image', 'video', 'link'] as ContentType[]).map((type) => (
|
||||||
className="w-full border rounded p-2 text-sm"
|
<Badge
|
||||||
placeholder="多个关键词用逗号或换行分隔"
|
key={type}
|
||||||
value={formData.excludeKeywords}
|
variant={formData.contentTypes.includes(type) ? "default" : "outline"}
|
||||||
onChange={e => handleInputChange('excludeKeywords', e.target.value)}
|
className="cursor-pointer"
|
||||||
rows={2}
|
onClick={() => handleContentTypeToggle(type)}
|
||||||
/>
|
>
|
||||||
</div>
|
{type === 'text' ? '文字' : type === 'image' ? '图片' : type === 'video' ? '视频' : '链接'}
|
||||||
</CardContent>
|
</Badge>
|
||||||
</Card>
|
))}
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>任务预览</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-2 text-sm">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-500">任务名称:</span>
|
|
||||||
<span>{formData.name}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-500">执行设备:</span>
|
|
||||||
<span>{formData.selectedDevices.length} 个</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-500">目标人群:</span>
|
|
||||||
<span>{formData.selectedGroups.length} 个</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-500">点赞间隔:</span>
|
|
||||||
<span>{formData.likeInterval} 秒</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-500">每日上限:</span>
|
|
||||||
<span>{formData.maxLikesPerDay} 次</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-500">单好友上限:</span>
|
|
||||||
<span>{formData.friendMaxLikes} 次</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-500">执行时间:</span>
|
|
||||||
<span>{formData.startTime} - {formData.endTime}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-500">内容类型:</span>
|
|
||||||
<span>{formData.contentTypes.map(t => ({text:'文字',image:'图片',video:'视频',link:'链接'}[t])).join('、')}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-500">包含关键词:</span>
|
|
||||||
<span>{formData.includeKeywords}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-500">排除关键词:</span>
|
|
||||||
<span>{formData.excludeKeywords}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
<div className="flex items-center space-x-2">
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>点赞后自动打标签</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex items-center mb-2">
|
|
||||||
<Switch
|
<Switch
|
||||||
checked={formData.enableFriendTags}
|
checked={formData.enableFriendTags}
|
||||||
onCheckedChange={v => handleInputChange('enableFriendTags', v)}
|
onCheckedChange={(checked) => handleInputChange('enableFriendTags', checked)}
|
||||||
className="mr-2"
|
|
||||||
/>
|
/>
|
||||||
<span className="text-sm">启用后,点赞后会自动为好友打上指定标签</span>
|
<Label htmlFor="enableFriendTags">启用好友标签</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formData.enableFriendTags && (
|
{formData.enableFriendTags && (
|
||||||
<Input
|
<div>
|
||||||
placeholder="请输入标签,多个标签用逗号分隔"
|
<Label htmlFor="friendTags">好友标签</Label>
|
||||||
value={formData.friendTags}
|
<Input
|
||||||
onChange={e => handleInputChange('friendTags', e.target.value)}
|
id="friendTags"
|
||||||
/>
|
value={formData.friendTags}
|
||||||
|
onChange={(e) => handleInputChange('friendTags', e.target.value)}
|
||||||
|
placeholder="请输入要添加的标签"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 底部按钮 */}
|
{/* 操作按钮 */}
|
||||||
<div className="flex justify-between mt-6">
|
<div className="flex justify-between mt-6">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -523,6 +431,7 @@ export default function NewAutoLike() {
|
|||||||
>
|
>
|
||||||
上一步
|
上一步
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
{currentStep < 3 ? (
|
{currentStep < 3 ? (
|
||||||
<Button onClick={handleNext}>
|
<Button onClick={handleNext}>
|
||||||
|
|||||||
117
nkebao/src/types/auto-like.ts
Normal file
117
nkebao/src/types/auto-like.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
// 自动点赞任务状态
|
||||||
|
export type LikeTaskStatus = 'running' | 'paused' | 'completed';
|
||||||
|
|
||||||
|
// 内容类型
|
||||||
|
export type ContentType = 'text' | 'image' | 'video' | 'link';
|
||||||
|
|
||||||
|
// 设备信息
|
||||||
|
export interface Device {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: 'online' | 'offline';
|
||||||
|
lastActive: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 好友信息
|
||||||
|
export interface Friend {
|
||||||
|
id: string;
|
||||||
|
nickname: string;
|
||||||
|
wechatId: string;
|
||||||
|
avatar: string;
|
||||||
|
tags: string[];
|
||||||
|
region: string;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点赞记录
|
||||||
|
export interface LikeRecord {
|
||||||
|
id: string;
|
||||||
|
workbenchId: string;
|
||||||
|
momentsId: string;
|
||||||
|
snsId: string;
|
||||||
|
wechatAccountId: string;
|
||||||
|
wechatFriendId: string;
|
||||||
|
likeTime: string;
|
||||||
|
content: string;
|
||||||
|
resUrls: string;
|
||||||
|
momentTime: string;
|
||||||
|
userName: string;
|
||||||
|
operatorName: string;
|
||||||
|
operatorAvatar: string;
|
||||||
|
friendName: string;
|
||||||
|
friendAvatar: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动点赞任务
|
||||||
|
export interface LikeTask {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: LikeTaskStatus;
|
||||||
|
deviceCount: number;
|
||||||
|
targetGroup: string;
|
||||||
|
likeCount: number;
|
||||||
|
lastLikeTime: string;
|
||||||
|
createTime: string;
|
||||||
|
creator: string;
|
||||||
|
likeInterval: number;
|
||||||
|
maxLikesPerDay: number;
|
||||||
|
timeRange: { start: string; end: string };
|
||||||
|
contentTypes: ContentType[];
|
||||||
|
targetTags: string[];
|
||||||
|
devices: string[];
|
||||||
|
friends: string[];
|
||||||
|
friendMaxLikes: number;
|
||||||
|
friendTags: string;
|
||||||
|
enableFriendTags: boolean;
|
||||||
|
todayLikeCount: number;
|
||||||
|
totalLikeCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建任务数据
|
||||||
|
export interface CreateLikeTaskData {
|
||||||
|
name: string;
|
||||||
|
interval: number;
|
||||||
|
maxLikes: number;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
contentTypes: ContentType[];
|
||||||
|
devices: string[];
|
||||||
|
friends?: string[];
|
||||||
|
friendMaxLikes: number;
|
||||||
|
friendTags?: string;
|
||||||
|
enableFriendTags: boolean;
|
||||||
|
targetTags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务数据
|
||||||
|
export interface UpdateLikeTaskData extends CreateLikeTaskData {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任务配置
|
||||||
|
export interface TaskConfig {
|
||||||
|
interval: number;
|
||||||
|
maxLikes: number;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
contentTypes: ContentType[];
|
||||||
|
devices: string[];
|
||||||
|
friends: string[];
|
||||||
|
friendMaxLikes: number;
|
||||||
|
friendTags: string;
|
||||||
|
enableFriendTags: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 响应格式
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
list: T[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
23793
nkebao/yarn.lock
23793
nkebao/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user