完整新闻信息管理

This commit is contained in:
2025-10-06 01:32:16 +08:00
parent 1e6c2e88a1
commit 3d7240565e
25 changed files with 2692 additions and 463 deletions

View File

@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"axios": "^1.12.2",
"echarts": "^6.0.0",
"element-plus": "^2.11.4",
"pinia": "^3.0.3",
@@ -1068,6 +1069,21 @@
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/birpc": {
"version": "2.6.1",
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.6.1.tgz",
@@ -1095,6 +1111,18 @@
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
"dev": true
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz",
@@ -1117,6 +1145,17 @@
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
"dev": true
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-3.0.5.tgz",
@@ -1141,6 +1180,14 @@
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.18.tgz",
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA=="
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.1.tgz",
@@ -1150,6 +1197,19 @@
"node": ">=8"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/echarts": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.0.0.tgz",
@@ -1200,6 +1260,47 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
@@ -1250,6 +1351,40 @@
"node": ">=8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
@@ -1264,6 +1399,60 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
@@ -1273,6 +1462,42 @@
"node": ">=8"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
@@ -1616,6 +1841,14 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
@@ -1648,6 +1881,25 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
@@ -1776,6 +2028,11 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/quill": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/quill/-/quill-2.0.3.tgz",
@@ -3121,6 +3378,21 @@
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "1.12.2",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"requires": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"birpc": {
"version": "2.6.1",
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.6.1.tgz",
@@ -3142,6 +3414,15 @@
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
"dev": true
},
"call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"requires": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
}
},
"chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz",
@@ -3158,6 +3439,14 @@
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
"dev": true
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-3.0.5.tgz",
@@ -3176,12 +3465,27 @@
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.18.tgz",
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA=="
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"detect-libc": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.1.tgz",
"integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==",
"dev": true
},
"dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"requires": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
}
},
"echarts": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.0.0.tgz",
@@ -3225,6 +3529,35 @@
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
},
"es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
},
"es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
},
"es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"requires": {
"es-errors": "^1.3.0"
}
},
"es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"requires": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
}
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
@@ -3262,6 +3595,23 @@
"to-regex-range": "^5.0.1"
}
},
"follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="
},
"form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
}
},
"fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
@@ -3269,12 +3619,69 @@
"dev": true,
"optional": true
},
"function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
},
"get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"requires": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
}
},
"get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"requires": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
}
},
"gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
},
"has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"requires": {
"has-symbols": "^1.0.3"
}
},
"hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"requires": {
"function-bind": "^1.1.2"
}
},
"hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
@@ -3446,6 +3853,11 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
},
"memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
@@ -3471,6 +3883,19 @@
}
}
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"requires": {
"mime-db": "1.52.0"
}
},
"mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
@@ -3554,6 +3979,11 @@
"source-map-js": "^1.2.1"
}
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"quill": {
"version": "2.0.3",
"resolved": "https://registry.npmmirror.com/quill/-/quill-2.0.3.tgz",

View File

@@ -10,6 +10,7 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"axios": "^1.12.2",
"echarts": "^6.0.0",
"element-plus": "^2.11.4",
"pinia": "^3.0.3",

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped>
</style>

View File

@@ -1,198 +1,403 @@
<template>
<div class="news-manage">
<!-- 查询表单 -->
<el-form :model="searchForm" label-width="100px" class="search-form">
<el-form-item label="新闻标题">
<el-input v-model="searchForm.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="副标题">
<el-input v-model="searchForm.subtitle" placeholder="请输入副标题" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<el-config-provider :locale="zhCn">
<!-- 主容器 -->
<div class="p-6 bg-white rounded-lg shadow-md min-h-screen pb-24" v-loading="loading">
<!-- 新闻表格 -->
<el-table :data="filteredNewsList" border style="width: 100%" class="news-table">
<el-table-column prop="title" label="标题" />
<el-table-column prop="subtitle" label="副标题" />
<el-table-column prop="cover" label="封面" width="120">
<template #default="scope">
<!-- 封面图片展示模拟URL -->
<img
v-if="scope.row.cover"
:src="scope.row.cover"
style="width: 100px; height: auto; object-fit: cover;"
alt="封面"
/>
<span v-else>无封面</span>
</template>
</el-table-column>
<el-table-column prop="content" label="文章内容" width="300" show-overflow-tooltip />
<el-table-column label="操作">
<template #default="scope">
<el-button type="text" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 头部区域 -->
<div class="flex justify-between items-center mb-6">
<span class="text-2xl font-bold text-gray-700">文章管理列表</span>
<el-button type="primary" :icon="Plus" @click="handleCreate">新建文章</el-button>
</div>
<!-- 编辑弹窗 -->
<el-dialog title="编辑新闻" v-model="dialogVisible" width="60%">
<el-form :model="currentNews" label-width="100px">
<el-form-item label="标题">
<el-input v-model="currentNews.title" />
</el-form-item>
<el-form-item label="副标题">
<el-input v-model="currentNews.subtitle" />
</el-form-item>
<el-form-item label="封面地址">
<el-input
v-model="currentNews.cover"
placeholder="请输入封面图片URLhttps://xxx.jpg"
<!-- 文章表格 -->
<el-table :data="tableData" style="width: 100%" row-key="id">
<el-table-column prop="id" label="ID" width="80" sortable fixed></el-table-column>
<el-table-column label="封面" width="180">
<template #default="scope">
<el-image style="width: 120px; height: 70px; border-radius: 6px;" :src="scope.row.cover" :preview-src-list="[scope.row.cover]" fit="cover" :preview-teleported="true" hide-on-click-modal>
<template #error>
<div class="flex items-center justify-center w-full h-full bg-gray-100 text-gray-500">加载失败</div>
</template>
</el-image>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip>
<template #default="scope">
<span class="font-semibold">{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column label="文章内容" min-width="300">
<template #default="scope">
<div class="content-preview" :title="scope.row.content">
{{ scope.row.content.length > 100 ? `${scope.row.content.slice(0, 100)}...` : scope.row.content }}
</div>
<el-button size="mini" type="text" class="mt-1 text-blue-600" @click="handleViewContent(scope.row)">查看详情</el-button>
</template>
</el-table-column>
<el-table-column prop="topic" label="主题" width="120"></el-table-column>
<el-table-column prop="excerpt" label="摘要" min-width="250" show-overflow-tooltip></el-table-column>
<el-table-column prop="create_at" label="创建时间" width="180" sortable></el-table-column>
<el-table-column prop="update_at" label="更新时间" width="180" sortable></el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<div class="action-buttons">
<el-button size="small" type="primary" :icon="Edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- Content 详情弹窗 -->
<el-dialog v-model="contentDialogVisible" title="文章内容详情" :width="`800px`" :before-close="handleCloseDialog">
<el-card class="p-4">
<h3 class="text-xl font-bold text-gray-800 mb-4">{{ currentArticle.title }}</h3>
<div class="content-full text-gray-700 leading-relaxed whitespace-pre-wrap">
{{ currentArticle.content || "当前文章暂无内容" }}
</div>
</el-card>
</el-dialog>
</div>
<!-- 分页器容器 -->
<div class="fixed bottom-0 right-0 w-full p-4 bg-white shadow-[0_-2px_5px_rgba(0,0,0,0.05)] flex justify-end z-10 border-t border-gray-200">
<el-pagination class="custom-pagination" background layout="total, sizes, prev, pager, next, jumper" :total="total" v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 20, 50, 100]" @size-change="handleSizeChange" @current-change="handleCurrentChange"></el-pagination>
</div>
<!-- ==================================================== -->
<!-- 🔥 文章编辑/新建抽屉 (已修改) 🔥 -->
<!-- ==================================================== -->
<el-drawer v-model="drawerVisible" :title="isEditMode ? '编辑文章' : '发表新文章'" direction="rtl" size="60%" :before-close="handleDrawerClose" destroy-on-close>
<div class="publish-form-container">
<!-- 1. 文章标题输入 -->
<div class="form-group title-group">
<label class="form-label">文章标题</label>
<el-input v-model="form.title" placeholder="请输入文章标题" clearable :disabled="isSubmitting" />
</div>
<!-- 2. 专题选择 (仅在新建时显示) -->
<div v-if="!isEditMode" class="form-group topic-group">
<label class="form-label">发表到专题</label>
<el-radio-group v-model="form.topic" :disabled="isSubmitting">
<el-radio label="news">新闻</el-radio>
<el-radio label="cases">案例资源</el-radio>
<el-radio label="community">社区服务</el-radio>
</el-radio-group>
</div>
<!-- 3. 封面图上传 -->
<div class="form-group cover-group">
<label class="form-label">文章封面</label>
<el-upload action="http://localhost:8080/api/upload/cover" name="image" :show-file-list="false" :on-success="handleCoverSuccess" :before-upload="beforeCoverUpload" :on-error="handleCoverError" :disabled="isSubmitting">
<img v-if="form.cover" :src="form.cover" class="cover-preview" alt="封面"/>
<el-icon v-else class="cover-uploader-icon"><Plus /></el-icon>
</el-upload>
</div>
<!-- 4. 文章摘要编辑 -->
<div class="form-group excerpt-group">
<label class="form-label">文章摘要</label>
<el-input
v-model="form.excerpt"
type="textarea"
:rows="4"
placeholder="请输入文章摘要(可选,若不填则自动从正文截取)"
clearable
:disabled="isSubmitting"
/>
</el-form-item>
<el-form-item label="文章内容">
<!-- 富文本内容 textarea 模拟实际可替换为专业富文本编辑 -->
<el-input
type="textarea"
v-model="currentNews.content"
rows="8"
placeholder="请输入富文本内容(支持换行等格式)"
/>
</el-form-item>
</el-form>
</div>
<!-- 5. 文章内容富文本编辑 -->
<div class="form-group content-group">
<label class="form-label">文章内容</label>
<div ref="editorRef" class="quill-editor"></div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveEdit">保存</el-button>
</span>
<div style="flex: auto">
<el-button @click="handleDrawerClose">取消</el-button>
<el-button type="primary" @click="submitArticle" :loading="isSubmitting">
{{ isEditMode ? '更新文章' : '发布文章' }}
</el-button>
</div>
</template>
</el-dialog>
</div>
</el-drawer>
</el-config-provider>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue';
import { ElMessage} from 'element-plus';
import { ref, onMounted, watch, nextTick } from 'vue';
import { ElMessage, ElMessageBox, ElDialog, ElConfigProvider, ElDrawer, ElInput, ElRadioGroup, ElRadio, ElUpload } from 'element-plus';
import { Edit, Delete, Plus } from '@element-plus/icons-vue';
import zhCn from 'element-plus/dist/locale/zh-cn.mjs';
import Quill from 'quill';
import 'quill/dist/quill.snow.css';
import type { UploadProps } from 'element-plus';
// 模拟“写死”的新闻数据
const newsList = ref([
{
id: 1,
title: '人工智能最新进展',
subtitle: '自然语言处理领域取得突破',
cover: 'https://ts2.tc.mm.bing.net/th/id/OIP-C.Mk--gp8OIJtRPOJEByk4qwHaHa?w=80&h=80&c=1&bgcl=1065f3&r=0&o=7&cb=12&pid=ImgRC&rm=3', // 模拟封面图URL
content: '人工智能在自然语言处理领域取得重大突破,能够更精准地理解人类意图,实现了多轮对话的连贯性和准确性提升。',
},
{
id: 2,
title: '新能源汽车技术革新',
subtitle: '续航与充电效率双提升',
cover: 'https://picsum.photos/200/150?random=2',
content: '某车企发布新一代新能源汽车续航里程突破800公里同时快充技术实现10分钟充电80%,极大缓解用户里程焦虑。',
},
{
id: 3,
title: '全球气候变化研讨会',
subtitle: '多国专家共商应对策略',
cover: '', // 无封面示例
content: '近日全球气候变化研讨会在瑞士举行来自30多个国家的气候专家齐聚一堂共同探讨减少碳排放、应对极端天气的有效策略。',
},
]);
// --- 类型定义 ---
interface Article {
id: number;
title: string;
content: string;
cover: string;
create_at: string;
update_at: string;
is_delete: number;
topic: string;
excerpt: string;
}
// 查询表单:双向绑定的查询条件
const searchForm = reactive({
title: '', // 标题关键字
subtitle: '',// 副标题关键字
});
interface ListArticleReq {
page: number;
size: number;
}
// 【计算属性】根据查询条件过滤后的新闻列表
const filteredNewsList = computed(() => {
return newsList.value.filter((news) => {
// 标题匹配:包含关键字
if (searchForm.title && !news.title.includes(searchForm.title)) {
return false;
}
// 副标题匹配:包含关键字
if (searchForm.subtitle && !news.subtitle.includes(searchForm.subtitle)) {
return false;
}
return true;
});
});
// --- API 基地址 ---
const API_BASE_URL = 'http://localhost:8080/api';
// 编辑弹窗状态与当前编辑的新闻
const dialogVisible = ref(false); // 弹窗显隐
const currentNews = reactive({
id: 0,
// =================================================================
// 列表页相关状态与逻辑
// =================================================================
const loading = ref(true);
const tableData = ref<Article[]>([]);
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
const contentDialogVisible = ref(false);
const currentArticle = ref<Article>({ id: 0, title: '', content: '', cover: '', create_at: '', update_at: '', is_delete: 0, topic: '', excerpt: '' });
const fetchData = async () => {
loading.value = true;
try {
const reqData: ListArticleReq = { page: currentPage.value, size: pageSize.value };
const response = await fetch(`${API_BASE_URL}/articles/getarticle`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(reqData)
});
if (!response.ok) throw new Error(`请求失败!状态码:${response.status}`);
const data = await response.json();
tableData.value = data.Article_list || [];
total.value = data.total || 0;
} catch (error) {
console.error('[ERROR] 获取文章列表失败:', error);
ElMessage.error('获取文章列表失败,请检查接口或网络!');
} finally {
loading.value = false;
}
};
onMounted(fetchData);
const handleViewContent = (row: Article) => {
currentArticle.value = { ...row };
contentDialogVisible.value = true;
};
const handleCloseDialog = () => {
contentDialogVisible.value = false;
};
const handleDelete = (row: Article) => {
ElMessageBox.confirm(`确定要删除文章《${row.title}》吗?此操作无法撤销!`, '警告', { confirmButtonText: '确定删除', cancelButtonText: '取消', type: 'warning' })
.then(async () => {
try {
const response = await fetch(`${API_BASE_URL}/articles/${row.id}`, { method: 'DELETE' });
if (!response.ok) throw new Error('删除失败');
ElMessage.success('删除成功!');
fetchData();
} catch (error) {
console.error('[ERROR] 删除文章失败:', error);
ElMessage.error('删除文章失败,请重试!');
}
})
.catch(() => ElMessage.info('已取消删除'));
};
const handleSizeChange = (val: number) => {
pageSize.value = val;
currentPage.value = 1;
fetchData();
};
const handleCurrentChange = (val: number) => {
currentPage.value = val;
fetchData();
};
// =================================================================
// 抽屉编辑/新建相关状态与逻辑
// =================================================================
const drawerVisible = ref(false);
const isEditMode = ref(false);
const isSubmitting = ref(false);
const defaultFormState = () => ({
id: null as number | null,
title: '',
subtitle: '',
topic: 'news',
cover: '',
content: '',
excerpt: '',
});
const form = ref(defaultFormState());
// --- 富文本编辑器 ---
const editorRef = ref<HTMLDivElement | null>(null);
let quillInstance: Quill | null = null;
const initQuillEditor = () => {
if (editorRef.value && !quillInstance) {
quillInstance = new Quill(editorRef.value, {
theme: 'snow',
modules: { toolbar: [['bold', 'italic', 'underline'], ['link', 'image']] },
placeholder: '请开始创作你的文章...'
});
}
};
watch(drawerVisible, (visible) => {
if (visible) {
nextTick(() => {
initQuillEditor();
if (isEditMode.value) {
if (quillInstance) {
quillInstance.root.innerHTML = form.value.content;
}
} else {
if (quillInstance) {
quillInstance.setContents([]);
}
}
});
} else {
// 🔥 关键修复:当抽屉关闭时,将实例重置为 null。
// `destroy-on-close` 属性会处理 DOM 的销毁。
quillInstance = null;
}
});
// 点击“编辑”:打开弹窗并赋值当前行数据
const handleEdit = (row: any) => {
// 深拷贝避免直接修改原数据
Object.assign(currentNews, JSON.parse(JSON.stringify(row)));
dialogVisible.value = true;
};
// 点击“保存”:更新原数据并关闭弹窗
const saveEdit = () => {
const targetIndex = newsList.value.findIndex((item) => item.id === currentNews.id);
if (targetIndex !== -1) {
newsList.value[targetIndex] = { ...currentNews }; // 替换原数据
ElMessage.success('编辑成功');
// --- 操作处理 ---
const resetForm = () => {
form.value = defaultFormState();
if (quillInstance) {
quillInstance.setContents([]);
}
dialogVisible.value = false;
};
// 点击“删除”:弹窗确认后删除数据
const handleDelete = (row: any) => {
// ElMessage.confirm('确定要删除这条新闻吗?', '提示', {
// confirmButtonText: '确定',
// cancelButtonText: '取消',
// type: 'warning',
// })
// .then(() => {
// newsList.value = newsList.value.filter((item) => item.id !== row.id);
// ElMessage.success('删除成功');
// })
// .catch(() => {
// ElMessage.info('已取消删除');
// });
const handleCreate = () => {
isEditMode.value = false;
resetForm();
drawerVisible.value = true;
};
// 重置查询条件
const resetSearch = () => {
searchForm.title = '';
searchForm.subtitle = '';
const handleEdit = (row: Article) => {
isEditMode.value = true;
form.value = {
id: row.id,
title: row.title,
topic: row.topic,
cover: row.cover,
excerpt: row.excerpt,
content: row.content,
};
drawerVisible.value = true;
};
// 执行查询(计算属性自动过滤,这里仅作提示)
const search = () => {
ElMessage.info('查询完成');
const handleDrawerClose = () => {
drawerVisible.value = false;
};
const submitArticle = async () => {
if (!form.value.title) {
ElMessage.warning('请输入文章标题');
return;
}
isSubmitting.value = true;
const contentHTML = quillInstance?.root.innerHTML || '';
const excerpt = form.value.excerpt.trim() || quillInstance?.getText().trim().slice(0, 150) || '';
const submitData = {
...form.value,
content: contentHTML,
excerpt: excerpt,
};
try {
const url = isEditMode.value ? `${API_BASE_URL}/articles/${form.value.id}` : `${API_BASE_URL}/articles`;
const method = isEditMode.value ? 'PUT' : 'POST';
const response = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(submitData),
});
if (!response.ok) throw new Error('提交失败');
ElMessage.success(isEditMode.value ? '文章更新成功!' : '文章发布成功!');
drawerVisible.value = false;
fetchData(); // 刷新列表
} catch (error) {
ElMessage.error('提交文章失败!');
} finally {
isSubmitting.value = false;
}
};
// --- 封面上传处理 ---
const handleCoverSuccess: UploadProps['onSuccess'] = (response) => {
const ossUrl = response.data?.url;
if (ossUrl) {
form.value.cover = ossUrl;
ElMessage.success('封面上传成功');
} else {
ElMessage.error('封面上传失败');
}
};
const beforeCoverUpload: UploadProps['beforeUpload'] = (rawFile) => {
const isLt5M = rawFile.size / 1024 / 1024 < 5;
if (!isLt5M) {
ElMessage.error('图片大小不能超过 5MB!');
}
return isLt5M;
};
const handleCoverError = () => {
ElMessage.error('封面上传失败');
};
</script>
<style scoped>
.news-manage {
padding: 20px;
}
.search-form {
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.news-table {
margin-top: 20px;
}
.dialog-footer {
text-align: right;
}
</style>
/* 表格样式 */
.el-table .el-table__cell { vertical-align: middle; }
.el-table__header-wrapper th { background-color: #fafafa !important; font-weight: 600; color: #333; }
.action-buttons .el-button { margin-right: 8px; }
.content-preview { color: #666; line-height: 1.5; word-break: break-all; }
.content-full { min-height: 300px; padding: 20px; background-color: #f9fafb; border-radius: 8px; }
.el-dialog__title { font-size: 18px !important; font-weight: 600 !important; }
/* 分页器样式 */
.custom-pagination { justify-content: flex-end !important; }
.custom-pagination .el-pagination__total,
.custom-pagination .el-pagination__sizes,
.custom-pagination .el-pagination__jump { margin-right: 16px !important; }
.custom-pagination .el-pagination__jump .el-input { width: 60px !important; }
/* 抽屉内表单样式 */
.publish-form-container { padding: 0 20px; }
.form-group { margin-bottom: 24px; }
.form-label { display: block; margin-bottom: 8px; color: #333; font-size: 14px; font-weight: 600; }
.quill-editor { height: 350px; border-radius: 4px; border: 1px solid #dcdfe6; }
.cover-uploader .el-upload { border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; }
.cover-uploader .el-upload:hover { border-color: #409EFF; }
.cover-uploader-icon { font-size: 28px; color: #8c939d; width: 178px; height: 178px; text-align: center; }
.cover-preview { width: 178px; height: 178px; display: block; object-fit: cover; }
</style>

View File

@@ -1,22 +1,23 @@
<template>
<div class="article-publish-page">
<!-- 页面标题 -->
<!-- 页面标题根据是否编辑动态显示 -->
<div class="page-header">
<h1>发表新文章</h1>
<p class="page-desc">创作优质内容分享你的观点</p>
<h1>{{ articleId ? '编辑文章' : '发表新文章' }}</h1>
<p class="page-desc">{{ articleId ? '修改文章内容,更新后即时生效' : '创作优质内容分享你的观点' }}</p>
</div>
<!-- 发表表单容器 -->
<div class="publish-form-container">
<!-- 发表/编辑表单容器 -->
<div class="publish-form-container" v-loading="isLoading">
<!-- 1. 文章标题输入 -->
<div class="form-group title-group">
<label class="form-label">文章标题</label>
<input
v-model="articleTitle"
type="text"
class="title-input"
placeholder="请输入文章标题不少于5个字不超过50字"
@input="checkTitleValid"
v-model="articleTitle"
type="text"
class="title-input"
placeholder="请输入文章标题不少于5个字不超过50字"
@input="checkTitleValid"
:disabled="isSubmitting || isSavingDraft"
>
<!-- 标题校验提示 -->
<p class="valid-hint" :class="{ error: !titleIsValid && articleTitle.length > 0 }">
@@ -24,12 +25,16 @@
</p>
</div>
<!-- 新增专题选择 -->
<!-- 2. 专题选择 -->
<div class="form-group topic-group">
<label class="form-label">发表到专题</label>
<p class="form-desc">选择文章所属的专题分类</p>
<el-radio-group v-model="selectedTopic" class="topic-radio-group">
<el-radio-group
v-model="selectedTopic"
class="topic-radio-group"
:disabled="isSubmitting || isSavingDraft"
>
<el-radio label="news" class="topic-radio">
<el-icon><News /></el-icon>
<span>新闻</span>
@@ -53,19 +58,20 @@
</el-radio-group>
</div>
<!-- 2. 封面图上传 -->
<!-- 3. 封面图上传 -->
<div class="form-group cover-group">
<label class="form-label">文章封面</label>
<p class="form-desc">建议上传16:9比例图片支持JPG/PNG/WEBP格式大小不超过5MB</p>
<div class="cover-uploader">
<!-- 已上传封面预览 -->
<!-- 已上传封面预览编辑时加载已有封面 -->
<div v-if="coverUrl" class="cover-preview">
<img :src="coverUrl" alt="文章封面" class="cover-img">
<button
class="remove-cover-btn"
@click="removeCover"
title="删除封面"
<button
class="remove-cover-btn"
@click="removeCover"
title="删除封面"
:disabled="isSubmitting || isSavingDraft"
>
<el-icon><Close /></el-icon>
</button>
@@ -73,14 +79,15 @@
<!-- 未上传时的上传区域 -->
<el-upload
v-else
class="cover-upload-area"
action="http://localhost:8080/api/upload/cover"
name="image"
:show-file-list="false"
:on-success="handleCoverSuccess"
:before-upload="beforeCoverUpload"
:on-error="handleCoverError"
v-else
class="cover-upload-area"
action="http://localhost:8080/api/upload/cover"
name="image"
:show-file-list="false"
:on-success="handleCoverSuccess"
:before-upload="beforeCoverUpload"
:on-error="handleCoverError"
:disabled="isSubmitting || isSavingDraft"
>
<div class="upload-placeholder">
<el-icon class="upload-icon"><Upload /></el-icon>
@@ -91,11 +98,11 @@
</div>
</div>
<!-- 3. 文章内容富文本编辑 -->
<!-- 4. 文章内容富文本编辑编辑时加载已有内容 -->
<div class="form-group content-group">
<label class="form-label">文章内容</label>
<p class="form-desc">请使用编辑器创作文章支持文字图片代码块等格式</p>
<!-- 富文本编辑器容器 -->
<div class="editor-wrapper">
<div ref="editor" class="quill-editor"></div>
@@ -110,29 +117,30 @@
</p>
</div>
<!-- 4. 底部操作按钮 -->
<!-- 5. 底部操作按钮 -->
<div class="form-actions">
<el-button
type="default"
size="large"
class="draft-btn"
@click="saveDraft"
:loading="isSavingDraft"
<el-button
type="default"
size="large"
class="draft-btn"
@click="saveDraft"
:loading="isSavingDraft"
:disabled="isSubmitting"
>
<el-icon v-if="isSavingDraft"><Loading /></el-icon>
<span>保存草稿</span>
</el-button>
<el-button
type="primary"
size="large"
class="publish-btn"
@click="submitArticle"
:disabled="!isFormValid"
:loading="isSubmitting"
<el-button
type="primary"
size="large"
class="publish-btn"
@click="submitArticle"
:disabled="!isFormValid || isLoading"
:loading="isSubmitting"
>
<el-icon v-if="isSubmitting"><Loading /></el-icon>
<span>发布文章</span>
<span>{{ articleId ? '更新文章' : '发布文章' }}</span>
</el-button>
</div>
</div>
@@ -140,29 +148,33 @@
</template>
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue';
import {onMounted, ref, computed, onUnmounted} from 'vue';
import { useRoute, useRouter } from 'vue-router'; // 路由相关获取ID、跳转
import Quill from 'quill';
import 'quill/dist/quill.snow.css';
// Element Plus 组件与图标
import { ElUpload, ElButton, ElIcon, ElMessage, ElRadioGroup, ElRadio } from 'element-plus';
import { ElUpload, ElButton, ElIcon, ElMessage, ElRadioGroup, ElRadio, ElLoading } from 'element-plus';
import { Upload, Close, Loading, Document, UserFilled, Trophy, Files } from '@element-plus/icons-vue';
import type { UploadProps } from 'element-plus';
import type { UploadProps, LoadingInstance } from 'element-plus';
// -------------------------- 状态管理 --------------------------
// 文章标题
const articleTitle = ref('');
// 选中的专题
const selectedTopic = ref('news'); // 默认选中"新闻"
// 封面图URL
const coverUrl = ref('');
// 富文本编辑器容器引用
const editor = ref<HTMLDivElement | null>(null);
// 富文本实例
let quillInstance: Quill | null = null;
// -------------------------- 基础状态管理 --------------------------
// 路由相关获取编辑的文章ID从路由参数如 /article/edit/123 中获取)
const route = useRoute();
const router = useRouter();
const articleId = ref<number | null>(null); // 文章IDnull=新增,有值=编辑
// 加载状态
const isSubmitting = ref(false); // 发布中
// 表单核心字段
const articleTitle = ref(''); // 标题
const selectedTopic = ref('news'); // 专题(默认新闻)
const coverUrl = ref(''); // 封面图URL
const editor = ref<HTMLDivElement | null>(null); // 富文本容器
let quillInstance: Quill | null = null; // 富文本实例
// 加载/提交状态
const isLoading = ref(false); // 整体加载(如加载详情)
const isSubmitting = ref(false); // 提交中(发布/更新)
const isSavingDraft = ref(false); // 保存草稿中
let loadingInstance: LoadingInstance | null = null; // 加载遮罩实例
// -------------------------- 表单校验 --------------------------
// 标题校验
@@ -185,19 +197,13 @@ const checkTitleValid = () => {
}
};
// 内容校验(是否有有效内容)
// 内容校验
const contentIsValid = ref(false);
const contentHintText = ref('请输入文章内容');
const checkContentValid = () => {
console.log('校验触发quillInstance是否存在', quillInstance !== null);
if (quillInstance) {
const content = quillInstance.getText().trim();
console.log('当前内容(去空后):', content);
console.log('内容长度:', content.length);
}
if (!quillInstance) return;
const contentLen = quillInstance.getText().trim().length;
if (contentLen === 0) {
contentHintText.value = '文章内容不能为空';
contentIsValid.value = false;
@@ -210,13 +216,12 @@ const checkContentValid = () => {
}
};
// 计算属性:安全获取编辑器字数
// 字数统计
const wordCount = computed(() => {
if (!quillInstance) return null;
return quillInstance.getText().length;
return quillInstance ? quillInstance.getText().length : null;
});
// 表单整体是否有效(发布按钮是否可点击)
// 表单整体有效性(提交按钮是否可点击)
const isFormValid = computed(() => {
return titleIsValid.value && contentIsValid.value && !isSubmitting.value && !isSavingDraft.value;
});
@@ -259,91 +264,72 @@ const removeCover = () => {
};
// -------------------------- 富文本编辑器逻辑 --------------------------
onMounted(() => {
if (editor.value) {
// 初始化Quill编辑器
quillInstance = new Quill(editor.value, {
theme: 'snow',
modules: {
editor: {
scrollingContainer: '.quill-editor'
},
toolbar: {
container: [
['bold', 'italic', 'underline', 'strike'],
[{ 'color': [] }, { 'background': [] }],
[{ 'font': [] }, { 'size': ['small', false, 'large', 'huge'] }],
[{ 'align': [] }],
['blockquote', 'code-block'],
[{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }],
[{ 'indent': '-1' }, { 'indent': '+1' }],
['link', 'image', 'video'],
['clean']
],
handlers: {
image: handleEditorImageUpload
}
}
// 初始化富文本编辑器
const initQuillEditor = () => {
if (!editor.value) return;
// 防止重复初始化
if (quillInstance) return quillInstance;
quillInstance = new Quill(editor.value, {
theme: 'snow',
modules: {
editor: {
scrollingContainer: '.quill-editor'
},
placeholder: '请开始创作你的文章...(支持文字、图片、代码块等格式)'
});
// 初始化后延迟校验,确保实例就绪
setTimeout(() => {
checkContentValid();
}, 100);
// 监听所有文本变化,实时触发校验
quillInstance.on('text-change', (delta, oldDelta, source) => {
// 先触发内容校验,覆盖所有输入场景
checkContentValid();
// 处理粘贴图片逻辑
if (source === 'user') {
const pastedBase64 = getPastedBase64Image(delta);
if (pastedBase64) {
const selection = quillInstance?.getSelection();
if (!selection) return;
const imageIndex = selection.index;
quillInstance?.deleteText(imageIndex, 1); // 删除base64占位符
const blob = base64ToBlob(pastedBase64);
if (blob) {
const file = new File(
[blob],
`editor-image-${Date.now()}.png`,
{ type: blob.type }
);
uploadEditorImage(file, imageIndex);
}
toolbar: {
container: [
['bold', 'italic', 'underline', 'strike'],
[{ 'color': [] }, { 'background': [] }],
[{ 'font': [] }, { 'size': ['small', false, 'large', 'huge'] }],
[{ 'align': [] }],
['blockquote', 'code-block'],
[{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }],
[{ 'indent': '-1' }, { 'indent': '+1' }],
['link', 'image', 'video'],
['clean']
],
handlers: {
image: handleEditorImageUpload
}
}
});
}
},
placeholder: '请开始创作你的文章...(支持文字、图片、代码块等格式)'
});
// 加载本地草稿
const savedDraft = localStorage.getItem('articleDraft');
if (savedDraft) {
const draft = JSON.parse(savedDraft);
articleTitle.value = draft.title;
coverUrl.value = draft.coverUrl;
// 加载保存的专题选择
if (draft.topic) {
selectedTopic.value = draft.topic;
}
if (quillInstance && draft.contentHtml) {
quillInstance.root.innerHTML = draft.contentHtml;
// 草稿加载后触发校验
setTimeout(() => {
checkContentValid();
checkTitleValid();
}, 100);
}
}
});
// 监听内容变化,实时校验
quillInstance.on('text-change', (delta, _, source) => {
checkContentValid();
// 编辑器工具栏「图片」按钮点击
// 处理粘贴图片
if (source === 'user') {
const pastedBase64 = getPastedBase64Image(delta);
if (pastedBase64) {
const selection = quillInstance?.getSelection();
if (!selection) return;
const imageIndex = selection.index;
quillInstance?.deleteText(imageIndex, 1); // 删除base64占位符
const blob = base64ToBlob(pastedBase64);
if (blob) {
const file = new File(
[blob],
`editor-image-${Date.now()}.png`,
{ type: blob.type }
);
uploadEditorImage(file, imageIndex);
}
}
}
});
// 初始化后校验内容
setTimeout(checkContentValid, 100);
return quillInstance;
};
// 编辑器工具栏图片上传
function handleEditorImageUpload() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
@@ -366,13 +352,9 @@ function handleEditorImageUpload() {
async function uploadEditorImage(file: File, insertIndex: number) {
if (!quillInstance) return;
// 插入上传中占位文本
// 插入上传中占位
const loadingIndex = insertIndex;
quillInstance.insertText(
loadingIndex,
'[图片上传中...]',
{ color: '#666', italic: true }
);
quillInstance.insertText(loadingIndex, '[图片上传中...]', { color: '#666', italic: true });
try {
const formData = new FormData();
@@ -390,7 +372,7 @@ async function uploadEditorImage(file: File, insertIndex: number) {
if (!imageUrl) throw new Error('未获取到图片地址');
// 替换占位文本为真实图片
// 替换占位为真实图片
quillInstance.deleteText(loadingIndex, '[图片上传中...]'.length);
quillInstance.insertEmbed(loadingIndex, 'image', imageUrl);
quillInstance.setSelection(loadingIndex + 1);
@@ -398,11 +380,7 @@ async function uploadEditorImage(file: File, insertIndex: number) {
} catch (err) {
const errMsg = err instanceof Error ? err.message : '未知错误';
quillInstance.deleteText(loadingIndex, '[图片上传中...]'.length);
quillInstance.insertText(
loadingIndex,
`[图片上传失败:${errMsg}]`,
{ color: '#ff4444', italic: true }
);
quillInstance.insertText(loadingIndex, `[图片上传失败:${errMsg}]`, { color: '#ff4444', italic: true });
ElMessage.error(`编辑器图片上传失败:${errMsg}`);
}
}
@@ -412,10 +390,10 @@ function getPastedBase64Image(delta: any): string | null {
if (!delta?.ops) return null;
for (const op of delta.ops) {
if (
op.insert &&
typeof op.insert === 'object' &&
op.insert.image &&
op.insert.image.startsWith('data:image')
op.insert &&
typeof op.insert === 'object' &&
op.insert.image &&
op.insert.image.startsWith('data:image')
) {
return op.insert.image;
}
@@ -446,8 +424,58 @@ function base64ToBlob(base64: string): Blob | null {
}
}
// -------------------------- 表单提交逻辑 --------------------------
// 保存草稿
// -------------------------- 编辑核心:加载文章详情 --------------------------
// 根据文章ID获取详情编辑时调用
const fetchArticleDetail = async (id: number) => {
isLoading.value = true;
loadingInstance = ElLoading.service({
lock: true,
text: '正在加载文章数据...',
background: 'rgba(255, 255, 255, 0.7)'
});
try {
// 调用后端文章详情接口(请确保后端有该接口)
const response = await fetch(`http://localhost:8080/api/articles/${id}`, {
method: 'GET',
credentials: 'include'
});
if (!response.ok) throw new Error(`加载失败HTTP状态码${response.status}`);
const result = await response.json();
const article = result.data || {};
// 1. 回显基础字段
articleTitle.value = article.title || '';
selectedTopic.value = article.topic || 'news';
coverUrl.value = article.cover || '';
// 2. 回显富文本内容(需等编辑器初始化完成)
const quill = initQuillEditor();
if (quill && article.content) {
// 清空编辑器并设置已有内容(保留格式)
quill.root.innerHTML = article.content;
// 重新校验内容
setTimeout(checkContentValid, 100);
}
// 3. 触发标题校验
checkTitleValid();
ElMessage.success('文章数据加载成功');
} catch (err) {
const errMsg = err instanceof Error ? err.message : '未知错误';
ElMessage.error(`加载文章失败:${errMsg}`);
console.error('文章详情加载错误:', err);
// 加载失败返回列表页
setTimeout(() => router.push('/article/list'), 1500);
} finally {
isLoading.value = false;
if (loadingInstance) loadingInstance.close();
}
};
// -------------------------- 草稿保存逻辑 --------------------------
const saveDraft = async () => {
if (!titleIsValid.value && articleTitle.value.trim().length > 0) {
ElMessage.warning('标题格式不正确,请调整后再保存');
@@ -458,62 +486,133 @@ const saveDraft = async () => {
try {
await new Promise(resolve => setTimeout(resolve, 800));
// 草稿数据包含文章ID区分编辑/新增)
const draftData = {
id: articleId.value, // 编辑时携带ID
title: articleTitle.value.trim() || '未命名草稿',
topic: selectedTopic.value, // 保存选中的专题
topic: selectedTopic.value,
coverUrl: coverUrl.value,
contentHtml: quillInstance?.root.innerHTML || '',
updatedAt: new Date().toISOString()
};
// 保存到本地存储
localStorage.setItem('articleDraft', JSON.stringify(draftData));
ElMessage.success('草稿保存成功');
// 打印草稿数据到控制台
console.log('【草稿保存】当前草稿数据:', draftData);
} catch (err) {
ElMessage.error(`草稿保存失败:${err instanceof Error ? err.message : '未知错误'}`);
console.error('草稿保存错误:', err);
} finally {
isSavingDraft.value = false;
}
};
// 发布文章
// -------------------------- 提交逻辑(区分新增/编辑) --------------------------
const submitArticle = async () => {
if (!isFormValid.value) return;
// 1. 组装提交数据
const submitData = {
id: articleId.value, // 编辑时携带ID新增时为null
title: articleTitle.value.trim(),
topic: selectedTopic.value,
cover: coverUrl.value, // 后端通常用cover字段接收封面
content: quillInstance?.root.innerHTML || '', // 富文本HTML内容
contentText: quillInstance?.getText().trim() || '', // 纯文本内容(用于搜索/摘要)
excerpt: quillInstance?.getText().trim().slice(0, 150) || '' // 自动生成摘要前150字
};
// 2. 打印修改后的信息到控制台(核心需求)
console.log('【文章提交】修改后的完整数据:', submitData);
isSubmitting.value = true;
try {
const articleData = {
title: articleTitle.value.trim(),
topic: selectedTopic.value, // 提交选中的专题
coverUrl: coverUrl.value,
contentHtml: quillInstance?.root.innerHTML || '',
contentText: quillInstance?.getText().trim() || '',
status: 'published',
publishTime: new Date().toISOString()
};
console.log('提交的文章数据:', articleData);
let response: Response;
const apiBase = 'http://localhost:8080/api/articles';
const response = await fetch('http://localhost:8080/api/articles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(articleData),
credentials: 'include'
});
// 3. 区分新增/编辑:调用不同接口
if (articleId.value) {
// 编辑调用PUT更新接口后端需支持
response = await fetch(`${apiBase}/${articleId.value}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(submitData),
credentials: 'include'
});
} else {
// 新增调用POST创建接口
response = await fetch(apiBase, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(submitData),
credentials: 'include'
});
}
if (!response.ok) throw new Error(`发布失败HTTP状态码${response.status}`);
// 4. 处理响应
if (!response.ok) throw new Error(`提交失败HTTP状态码${response.status}`);
const result = await response.json();
ElMessage.success('文章发布成功!即将跳转到文章详情页');
setTimeout(() => {
window.location.href = `/article/${result.data?.id || ''}`;
}, 1500);
// 5. 提示并跳转
const successMsg = articleId.value ? '文章更新成功' : '文章发布成功';
ElMessage.success(`${successMsg}!即将跳转到文章列表`);
console.log('【提交成功】后端返回结果:', result);
// 跳转到文章列表页(根据你的路由调整)
setTimeout(() => router.push('/article/list'), 1500);
} catch (err) {
ElMessage.error(`文章发布失败:${err instanceof Error ? err.message : '网络错误'}`);
const errMsg = err instanceof Error ? err.message : '网络错误';
ElMessage.error(`${articleId.value ? '更新' : '发布'}文章失败:${errMsg}`);
console.error('文章提交错误:', err);
} finally {
isSubmitting.value = false;
}
};
// -------------------------- 页面初始化 --------------------------
onMounted(() => {
// 1. 初始化富文本编辑器
initQuillEditor();
// 2. 从路由参数获取文章ID如 /article/edit/123 → route.params.id = '123'
const idFromRoute = route.params.id;
if (idFromRoute && typeof idFromRoute === 'string') {
articleId.value = parseInt(idFromRoute, 10);
// 3. 编辑模式:加载文章详情
if (!isNaN(articleId.value)) {
fetchArticleDetail(articleId.value);
} else {
ElMessage.error('无效的文章ID');
router.push('/article/list');
}
} else {
// 4. 新增模式:加载本地草稿
const savedDraft = localStorage.getItem('articleDraft');
if (savedDraft) {
const draft = JSON.parse(savedDraft);
articleTitle.value = draft.title || '';
selectedTopic.value = draft.topic || 'news';
coverUrl.value = draft.coverUrl || '';
if (quillInstance && draft.contentHtml) {
quillInstance.root.innerHTML = draft.contentHtml;
setTimeout(checkContentValid, 100);
}
checkTitleValid();
}
}
});
// -------------------------- 页面卸载前清理 --------------------------
onUnmounted(() => {
// 清除本地草稿(可选:如需保留草稿可注释)
// localStorage.removeItem('articleDraft');
quillInstance = null;
if (loadingInstance) loadingInstance.close();
});
</script>
<style scoped lang="scss">
@@ -617,6 +716,11 @@ $transition: all 0.3s ease;
border-color: $primary-color;
box-shadow: 0 0 0 3px $primary-light;
}
&:disabled {
background-color: #F9FAFB;
cursor: not-allowed;
}
}
.valid-hint {
@@ -654,6 +758,15 @@ $transition: all 0.3s ease;
background-color: $primary-light;
}
&.is-disabled {
opacity: 0.6;
cursor: not-allowed;
&:hover {
border-color: $border-color;
background-color: transparent;
}
}
.el-icon {
font-size: 18px;
color: $text-secondary;
@@ -730,9 +843,14 @@ $transition: all 0.3s ease;
&:hover {
background-color: rgba(0, 0, 0, 0.7);
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
.cover-preview:hover .remove-cover-btn {
.cover-preview:hover .remove-cover-btn:not(:disabled) {
opacity: 1;
}
@@ -761,6 +879,15 @@ $transition: all 0.3s ease;
background-color: $primary-light;
}
&.is-disabled {
opacity: 0.6;
cursor: not-allowed;
&:hover {
border-color: $border-color;
background-color: #F9FAFB;
}
}
.upload-icon {
font-size: 32px;
color: $text-placeholder;
@@ -836,6 +963,14 @@ $transition: all 0.3s ease;
&:hover {
color: $primary-color;
}
&.ql-disabled {
opacity: 0.6;
cursor: not-allowed;
&:hover {
color: $text-secondary;
}
}
}
.ql-active {
@@ -879,6 +1014,12 @@ $transition: all 0.3s ease;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
&:disabled {
transform: none;
box-shadow: none;
cursor: not-allowed;
}
}
.draft-btn {
@@ -889,6 +1030,12 @@ $transition: all 0.3s ease;
color: $text-primary;
border-color: #D1D5DB;
}
&:disabled {
background-color: #F9FAFB;
border-color: $border-color;
color: $text-placeholder;
}
}
.publish-btn {
@@ -903,9 +1050,7 @@ $transition: all 0.3s ease;
&:disabled {
background-color: #A3C5FF;
border-color: #A3C5FF;
cursor: not-allowed;
transform: none;
box-shadow: none;
color: #FFFFFF;
}
}
}
@@ -929,7 +1074,7 @@ $transition: all 0.3s ease;
flex-direction: column;
gap: 12px;
}
.topic-radio {
width: 100%;
justify-content: center;
@@ -968,4 +1113,4 @@ $transition: all 0.3s ease;
}
}
}
</style>
</style>