Compare commits
11 Commits
0e6f611455
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21539772ef | ||
|
|
b8a8cc474b | ||
|
|
78c468a7d2 | ||
|
|
ca9e083399 | ||
|
|
1952f4de64 | ||
|
|
bdf9fad89f | ||
|
|
c97d31afe4 | ||
|
|
7675b6c299 | ||
|
|
9f05776781 | ||
|
|
63c97c9b00 | ||
|
|
3d0a37a751 |
@@ -1,59 +0,0 @@
|
|||||||
name: Deploy docs for Site
|
|
||||||
run-name: ${{ gitea.actor }} is building out Gitea Actions 🚀
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [20.x]
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository code
|
|
||||||
uses: https://gitea.com/actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: 'main'
|
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
|
|
||||||
# Skip pnpm cache to reduce overhead
|
|
||||||
- name: Install pnpm
|
|
||||||
run: |
|
|
||||||
if ! command -v pnpm &> /dev/null; then
|
|
||||||
npm install -g pnpm@9
|
|
||||||
else
|
|
||||||
echo "pnpm is already installed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Check Node.js and pnpm versions
|
|
||||||
run: |
|
|
||||||
echo "pnpm version ===>" && pnpm -v
|
|
||||||
echo "node version ===>" && node -v
|
|
||||||
|
|
||||||
# Simplify node_modules handling without caching
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
if [ ! -d "node_modules" ] || [ "$(find package.json -newer node_modules)" ]; then
|
|
||||||
echo "Dependencies are outdated or missing, installing..."
|
|
||||||
pnpm install
|
|
||||||
else
|
|
||||||
echo "Dependencies are up-to-date, skipping installation."
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build site
|
|
||||||
run: pnpm build
|
|
||||||
|
|
||||||
- name: ssh deploy
|
|
||||||
uses: easingthemes/ssh-deploy@v5.1.0
|
|
||||||
with:
|
|
||||||
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_PRIVATE_KEY }}
|
|
||||||
REMOTE_HOST: ${{ secrets.DEPLOY_HOST }}
|
|
||||||
REMOTE_USER: ${{ secrets.DEPLOY_USER }}
|
|
||||||
SOURCE: "/dist/"
|
|
||||||
TARGET: "/opt/1panel/apps/openresty/openresty/www/sites/zhaoguiyang.com/index/dist"
|
|
||||||
EXCLUDE: "/node_modules/"
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -24,3 +24,6 @@ pnpm-debug.log*
|
|||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
.trae/
|
.trae/
|
||||||
|
|
||||||
|
# deploy script
|
||||||
|
deploy.sh
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"tw-animate-css": "^1.2.4"
|
"tw-animate-css": "^1.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.16"
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
|
"@waline/client": "^3.8.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
240
pnpm-lock.yaml
generated
240
pnpm-lock.yaml
generated
@@ -75,6 +75,9 @@ importers:
|
|||||||
'@tailwindcss/typography':
|
'@tailwindcss/typography':
|
||||||
specifier: ^0.5.16
|
specifier: ^0.5.16
|
||||||
version: 0.5.16(tailwindcss@4.1.10)
|
version: 0.5.16(tailwindcss@4.1.10)
|
||||||
|
'@waline/client':
|
||||||
|
specifier: ^3.8.0
|
||||||
|
version: 3.8.0(typescript@5.8.3)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -159,6 +162,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
|
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@7.28.5':
|
||||||
|
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/helper-validator-option@7.27.1':
|
'@babel/helper-validator-option@7.27.1':
|
||||||
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
|
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -172,6 +179,11 @@ packages:
|
|||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@babel/parser@7.28.5':
|
||||||
|
resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@babel/plugin-transform-react-jsx-self@7.27.1':
|
'@babel/plugin-transform-react-jsx-self@7.27.1':
|
||||||
resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
|
resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -200,6 +212,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
|
resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/types@7.28.5':
|
||||||
|
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@capsizecss/unpack@2.4.0':
|
'@capsizecss/unpack@2.4.0':
|
||||||
resolution: {integrity: sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q==}
|
resolution: {integrity: sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q==}
|
||||||
|
|
||||||
@@ -510,6 +526,9 @@ packages:
|
|||||||
'@jridgewell/sourcemap-codec@1.5.0':
|
'@jridgewell/sourcemap-codec@1.5.0':
|
||||||
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
|
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5':
|
||||||
|
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.25':
|
'@jridgewell/trace-mapping@0.3.25':
|
||||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||||
|
|
||||||
@@ -946,6 +965,9 @@ packages:
|
|||||||
'@types/unist@3.0.3':
|
'@types/unist@3.0.3':
|
||||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||||
|
|
||||||
|
'@types/web-bluetooth@0.0.21':
|
||||||
|
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
||||||
|
|
||||||
'@ungap/structured-clone@1.3.0':
|
'@ungap/structured-clone@1.3.0':
|
||||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||||
|
|
||||||
@@ -955,6 +977,56 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0
|
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0
|
||||||
|
|
||||||
|
'@vue/compiler-core@3.5.26':
|
||||||
|
resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==}
|
||||||
|
|
||||||
|
'@vue/compiler-dom@3.5.26':
|
||||||
|
resolution: {integrity: sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==}
|
||||||
|
|
||||||
|
'@vue/compiler-sfc@3.5.26':
|
||||||
|
resolution: {integrity: sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==}
|
||||||
|
|
||||||
|
'@vue/compiler-ssr@3.5.26':
|
||||||
|
resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==}
|
||||||
|
|
||||||
|
'@vue/reactivity@3.5.26':
|
||||||
|
resolution: {integrity: sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==}
|
||||||
|
|
||||||
|
'@vue/runtime-core@3.5.26':
|
||||||
|
resolution: {integrity: sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==}
|
||||||
|
|
||||||
|
'@vue/runtime-dom@3.5.26':
|
||||||
|
resolution: {integrity: sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==}
|
||||||
|
|
||||||
|
'@vue/server-renderer@3.5.26':
|
||||||
|
resolution: {integrity: sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: 3.5.26
|
||||||
|
|
||||||
|
'@vue/shared@3.5.26':
|
||||||
|
resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==}
|
||||||
|
|
||||||
|
'@vueuse/core@14.1.0':
|
||||||
|
resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.5.0
|
||||||
|
|
||||||
|
'@vueuse/metadata@14.1.0':
|
||||||
|
resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==}
|
||||||
|
|
||||||
|
'@vueuse/shared@14.1.0':
|
||||||
|
resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.5.0
|
||||||
|
|
||||||
|
'@waline/api@1.0.0':
|
||||||
|
resolution: {integrity: sha512-o0lWjt+/oZH1/4q3DJxTf5kdkgNbSmoLRXIiGznW225A6hq9/9IkOO1DiAijIsxGYJS6psFC+58+IzkD1EerBA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@waline/client@3.8.0':
|
||||||
|
resolution: {integrity: sha512-UAbqLmqy2Pns/CXFlrLYEDG4F1peRPo+wp2FmpfFL9BO0ceQO6KvvAJXw3EhX3wWcMiKlX29G0gJVtG4UlCzGw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
acorn-jsx@5.3.2:
|
acorn-jsx@5.3.2:
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1015,6 +1087,9 @@ packages:
|
|||||||
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'}
|
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
autosize@6.0.1:
|
||||||
|
resolution: {integrity: sha512-f86EjiUKE6Xvczc4ioP1JBlWG7FKrE13qe/DxBCpe8GCipCq2nFw73aO8QEBKHfSbYGDN5eB9jXWKen7tspDqQ==}
|
||||||
|
|
||||||
axobject-query@4.1.0:
|
axobject-query@4.1.0:
|
||||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1153,6 +1228,9 @@ packages:
|
|||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
|
csstype@3.2.3:
|
||||||
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
debug@4.4.1:
|
debug@4.4.1:
|
||||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@@ -1224,6 +1302,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
|
entities@7.0.0:
|
||||||
|
resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
es-module-lexer@1.7.0:
|
es-module-lexer@1.7.0:
|
||||||
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
|
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
|
||||||
|
|
||||||
@@ -1713,6 +1795,9 @@ packages:
|
|||||||
magic-string@0.30.17:
|
magic-string@0.30.17:
|
||||||
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
||||||
|
|
||||||
|
magic-string@0.30.21:
|
||||||
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
magicast@0.3.5:
|
magicast@0.3.5:
|
||||||
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
|
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
|
||||||
|
|
||||||
@@ -1723,6 +1808,16 @@ packages:
|
|||||||
markdown-table@3.0.4:
|
markdown-table@3.0.4:
|
||||||
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
||||||
|
|
||||||
|
marked-highlight@2.2.3:
|
||||||
|
resolution: {integrity: sha512-FCfZRxW/msZAiasCML4isYpxyQWKEEx44vOgdn5Kloae+Qc3q4XR7WjpKKf8oMLk7JP9ZCRd2vhtclJFdwxlWQ==}
|
||||||
|
peerDependencies:
|
||||||
|
marked: '>=4 <18'
|
||||||
|
|
||||||
|
marked@16.4.2:
|
||||||
|
resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==}
|
||||||
|
engines: {node: '>= 20'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
mdast-util-definitions@6.0.0:
|
mdast-util-definitions@6.0.0:
|
||||||
resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==}
|
resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==}
|
||||||
|
|
||||||
@@ -2003,6 +2098,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==}
|
resolution: {integrity: sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
postcss@8.5.6:
|
||||||
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
prismjs@1.30.0:
|
prismjs@1.30.0:
|
||||||
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
|
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -2037,6 +2136,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||||
engines: {node: '>= 14.18.0'}
|
engines: {node: '>= 14.18.0'}
|
||||||
|
|
||||||
|
recaptcha-v3@1.11.3:
|
||||||
|
resolution: {integrity: sha512-sEE6J0RzUkS+sKEBpgCD/AqCU0ffrAVOADGjvAx9vcttN+VLK42SWMkj/J/I6vHu3Kew+xcfbBqDVb65N0QGDw==}
|
||||||
|
|
||||||
recma-build-jsx@1.0.0:
|
recma-build-jsx@1.0.0:
|
||||||
resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==}
|
resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==}
|
||||||
|
|
||||||
@@ -2430,6 +2532,14 @@ packages:
|
|||||||
vite:
|
vite:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vue@3.5.26:
|
||||||
|
resolution: {integrity: sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
web-namespaces@2.0.1:
|
web-namespaces@2.0.1:
|
||||||
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
||||||
|
|
||||||
@@ -2661,6 +2771,8 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/helper-validator-identifier@7.27.1': {}
|
'@babel/helper-validator-identifier@7.27.1': {}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@7.28.5': {}
|
||||||
|
|
||||||
'@babel/helper-validator-option@7.27.1': {}
|
'@babel/helper-validator-option@7.27.1': {}
|
||||||
|
|
||||||
'@babel/helpers@7.27.6':
|
'@babel/helpers@7.27.6':
|
||||||
@@ -2672,6 +2784,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.27.6
|
'@babel/types': 7.27.6
|
||||||
|
|
||||||
|
'@babel/parser@7.28.5':
|
||||||
|
dependencies:
|
||||||
|
'@babel/types': 7.28.5
|
||||||
|
|
||||||
'@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.4)':
|
'@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.27.4
|
'@babel/core': 7.27.4
|
||||||
@@ -2707,6 +2823,11 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.27.1
|
'@babel/helper-validator-identifier': 7.27.1
|
||||||
|
|
||||||
|
'@babel/types@7.28.5':
|
||||||
|
dependencies:
|
||||||
|
'@babel/helper-string-parser': 7.27.1
|
||||||
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
|
|
||||||
'@capsizecss/unpack@2.4.0':
|
'@capsizecss/unpack@2.4.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
blob-to-buffer: 1.2.9
|
blob-to-buffer: 1.2.9
|
||||||
@@ -2898,6 +3019,8 @@ snapshots:
|
|||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.0': {}
|
'@jridgewell/sourcemap-codec@1.5.0': {}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.25':
|
'@jridgewell/trace-mapping@0.3.25':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
@@ -3291,6 +3414,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/unist@3.0.3': {}
|
'@types/unist@3.0.3': {}
|
||||||
|
|
||||||
|
'@types/web-bluetooth@0.0.21': {}
|
||||||
|
|
||||||
'@ungap/structured-clone@1.3.0': {}
|
'@ungap/structured-clone@1.3.0': {}
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1))':
|
'@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1))':
|
||||||
@@ -3305,6 +3430,87 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@vue/compiler-core@3.5.26':
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 7.28.5
|
||||||
|
'@vue/shared': 3.5.26
|
||||||
|
entities: 7.0.0
|
||||||
|
estree-walker: 2.0.2
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
'@vue/compiler-dom@3.5.26':
|
||||||
|
dependencies:
|
||||||
|
'@vue/compiler-core': 3.5.26
|
||||||
|
'@vue/shared': 3.5.26
|
||||||
|
|
||||||
|
'@vue/compiler-sfc@3.5.26':
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 7.28.5
|
||||||
|
'@vue/compiler-core': 3.5.26
|
||||||
|
'@vue/compiler-dom': 3.5.26
|
||||||
|
'@vue/compiler-ssr': 3.5.26
|
||||||
|
'@vue/shared': 3.5.26
|
||||||
|
estree-walker: 2.0.2
|
||||||
|
magic-string: 0.30.21
|
||||||
|
postcss: 8.5.6
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
'@vue/compiler-ssr@3.5.26':
|
||||||
|
dependencies:
|
||||||
|
'@vue/compiler-dom': 3.5.26
|
||||||
|
'@vue/shared': 3.5.26
|
||||||
|
|
||||||
|
'@vue/reactivity@3.5.26':
|
||||||
|
dependencies:
|
||||||
|
'@vue/shared': 3.5.26
|
||||||
|
|
||||||
|
'@vue/runtime-core@3.5.26':
|
||||||
|
dependencies:
|
||||||
|
'@vue/reactivity': 3.5.26
|
||||||
|
'@vue/shared': 3.5.26
|
||||||
|
|
||||||
|
'@vue/runtime-dom@3.5.26':
|
||||||
|
dependencies:
|
||||||
|
'@vue/reactivity': 3.5.26
|
||||||
|
'@vue/runtime-core': 3.5.26
|
||||||
|
'@vue/shared': 3.5.26
|
||||||
|
csstype: 3.2.3
|
||||||
|
|
||||||
|
'@vue/server-renderer@3.5.26(vue@3.5.26(typescript@5.8.3))':
|
||||||
|
dependencies:
|
||||||
|
'@vue/compiler-ssr': 3.5.26
|
||||||
|
'@vue/shared': 3.5.26
|
||||||
|
vue: 3.5.26(typescript@5.8.3)
|
||||||
|
|
||||||
|
'@vue/shared@3.5.26': {}
|
||||||
|
|
||||||
|
'@vueuse/core@14.1.0(vue@3.5.26(typescript@5.8.3))':
|
||||||
|
dependencies:
|
||||||
|
'@types/web-bluetooth': 0.0.21
|
||||||
|
'@vueuse/metadata': 14.1.0
|
||||||
|
'@vueuse/shared': 14.1.0(vue@3.5.26(typescript@5.8.3))
|
||||||
|
vue: 3.5.26(typescript@5.8.3)
|
||||||
|
|
||||||
|
'@vueuse/metadata@14.1.0': {}
|
||||||
|
|
||||||
|
'@vueuse/shared@14.1.0(vue@3.5.26(typescript@5.8.3))':
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.26(typescript@5.8.3)
|
||||||
|
|
||||||
|
'@waline/api@1.0.0': {}
|
||||||
|
|
||||||
|
'@waline/client@3.8.0(typescript@5.8.3)':
|
||||||
|
dependencies:
|
||||||
|
'@vueuse/core': 14.1.0(vue@3.5.26(typescript@5.8.3))
|
||||||
|
'@waline/api': 1.0.0
|
||||||
|
autosize: 6.0.1
|
||||||
|
marked: 16.4.2
|
||||||
|
marked-highlight: 2.2.3(marked@16.4.2)
|
||||||
|
recaptcha-v3: 1.11.3
|
||||||
|
vue: 3.5.26(typescript@5.8.3)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- typescript
|
||||||
|
|
||||||
acorn-jsx@5.3.2(acorn@8.15.0):
|
acorn-jsx@5.3.2(acorn@8.15.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.15.0
|
acorn: 8.15.0
|
||||||
@@ -3457,6 +3663,8 @@ snapshots:
|
|||||||
- uploadthing
|
- uploadthing
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
|
autosize@6.0.1: {}
|
||||||
|
|
||||||
axobject-query@4.1.0: {}
|
axobject-query@4.1.0: {}
|
||||||
|
|
||||||
bail@2.0.2: {}
|
bail@2.0.2: {}
|
||||||
@@ -3582,6 +3790,8 @@ snapshots:
|
|||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
debug@4.4.1:
|
debug@4.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@@ -3631,6 +3841,8 @@ snapshots:
|
|||||||
|
|
||||||
entities@6.0.1: {}
|
entities@6.0.1: {}
|
||||||
|
|
||||||
|
entities@7.0.0: {}
|
||||||
|
|
||||||
es-module-lexer@1.7.0: {}
|
es-module-lexer@1.7.0: {}
|
||||||
|
|
||||||
esast-util-from-estree@2.0.0:
|
esast-util-from-estree@2.0.0:
|
||||||
@@ -4157,6 +4369,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
|
magic-string@0.30.21:
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
magicast@0.3.5:
|
magicast@0.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.27.5
|
'@babel/parser': 7.27.5
|
||||||
@@ -4167,6 +4383,12 @@ snapshots:
|
|||||||
|
|
||||||
markdown-table@3.0.4: {}
|
markdown-table@3.0.4: {}
|
||||||
|
|
||||||
|
marked-highlight@2.2.3(marked@16.4.2):
|
||||||
|
dependencies:
|
||||||
|
marked: 16.4.2
|
||||||
|
|
||||||
|
marked@16.4.2: {}
|
||||||
|
|
||||||
mdast-util-definitions@6.0.0:
|
mdast-util-definitions@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mdast': 4.0.4
|
'@types/mdast': 4.0.4
|
||||||
@@ -4713,6 +4935,12 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
postcss@8.5.6:
|
||||||
|
dependencies:
|
||||||
|
nanoid: 3.3.11
|
||||||
|
picocolors: 1.1.1
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
prismjs@1.30.0: {}
|
prismjs@1.30.0: {}
|
||||||
|
|
||||||
prompts@2.4.2:
|
prompts@2.4.2:
|
||||||
@@ -4737,6 +4965,8 @@ snapshots:
|
|||||||
|
|
||||||
readdirp@4.1.2: {}
|
readdirp@4.1.2: {}
|
||||||
|
|
||||||
|
recaptcha-v3@1.11.3: {}
|
||||||
|
|
||||||
recma-build-jsx@1.0.0:
|
recma-build-jsx@1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
@@ -5191,6 +5421,16 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 6.3.5(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1)
|
vite: 6.3.5(@types/node@24.0.1)(jiti@2.4.2)(lightningcss@1.30.1)
|
||||||
|
|
||||||
|
vue@3.5.26(typescript@5.8.3):
|
||||||
|
dependencies:
|
||||||
|
'@vue/compiler-dom': 3.5.26
|
||||||
|
'@vue/compiler-sfc': 3.5.26
|
||||||
|
'@vue/runtime-dom': 3.5.26
|
||||||
|
'@vue/server-renderer': 3.5.26(vue@3.5.26(typescript@5.8.3))
|
||||||
|
'@vue/shared': 3.5.26
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 5.8.3
|
||||||
|
|
||||||
web-namespaces@2.0.1: {}
|
web-namespaces@2.0.1: {}
|
||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
webidl-conversions@3.0.1: {}
|
||||||
|
|||||||
64
src/components/blog/Comments.tsx
Normal file
64
src/components/blog/Comments.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import type { WalineInstance, WalineInitOptions } from '@waline/client';
|
||||||
|
import { init } from '@waline/client';
|
||||||
|
import '@waline/client/style';
|
||||||
|
import '@/styles/waline-custom.css';
|
||||||
|
|
||||||
|
import type { Lang } from '@/types/i18n';
|
||||||
|
|
||||||
|
export type CommentsProps = Partial<Omit<WalineInitOptions, 'el' | 'serverURL'>> & { lang?: Lang };
|
||||||
|
|
||||||
|
export default function Comments({ lang = 'en', ...props }: CommentsProps) {
|
||||||
|
const walineInstanceRef = useRef<WalineInstance | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const walineLang = lang === 'zh' ? 'zh-CN' : 'en';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
const getTheme = () => document.documentElement.classList.contains('dark') ? 'html.dark' : false;
|
||||||
|
|
||||||
|
const pathname = window.location.pathname;
|
||||||
|
const postsMatch = pathname.match(/\/posts\/([^/]+)/);
|
||||||
|
const path = postsMatch ? postsMatch[1] : pathname;
|
||||||
|
|
||||||
|
walineInstanceRef.current = init({
|
||||||
|
...props,
|
||||||
|
el: containerRef.current,
|
||||||
|
serverURL: import.meta.env.PUBLIC_WALINE_SERVER_URL,
|
||||||
|
lang: walineLang,
|
||||||
|
dark: getTheme(),
|
||||||
|
pageview: true,
|
||||||
|
comment: true,
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => walineInstanceRef.current?.destroy();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getTheme = () => document.documentElement.classList.contains('dark') ? 'html.dark' : false;
|
||||||
|
|
||||||
|
walineInstanceRef.current?.update({
|
||||||
|
...props,
|
||||||
|
lang: walineLang,
|
||||||
|
dark: getTheme(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleThemeChange = () => {
|
||||||
|
walineInstanceRef.current?.update({
|
||||||
|
...props,
|
||||||
|
lang: walineLang,
|
||||||
|
dark: getTheme(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = new MutationObserver(handleThemeChange);
|
||||||
|
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [lang, props, walineLang]);
|
||||||
|
|
||||||
|
return <div ref={containerRef} className="waline-container" />;
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import Giscus from '@giscus/react';
|
|
||||||
import type { Lang } from '@/types/i18n';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
export interface GiscusCommentsProps {
|
|
||||||
lang?: Lang;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GiscusComments({ lang = 'en' }: GiscusCommentsProps) {
|
|
||||||
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
|
||||||
const [term, setTerm] = useState<string>('');
|
|
||||||
|
|
||||||
const giscusLang = lang === 'zh' ? 'zh-CN' : lang;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getTheme = () => document.documentElement.classList.contains('dark') ? 'dark' : 'light';
|
|
||||||
|
|
||||||
const pathname = window.location.pathname;
|
|
||||||
const postsMatch = pathname.match(/\/posts\/([^/]+)/);
|
|
||||||
const discussionTerm = postsMatch ? postsMatch[1] : pathname;
|
|
||||||
|
|
||||||
setTheme(getTheme());
|
|
||||||
setTerm(discussionTerm);
|
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
setTheme(getTheme());
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
||||||
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Giscus
|
|
||||||
repo="zguiyang/blog-giscus"
|
|
||||||
repoId="R_kgDOQ2Wnxw"
|
|
||||||
category="Announcements"
|
|
||||||
categoryId="DIC_kwDOQ2Wnx84C0vSJ"
|
|
||||||
mapping="specific"
|
|
||||||
term={term}
|
|
||||||
strict="0"
|
|
||||||
reactionsEnabled="1"
|
|
||||||
emitMetadata="0"
|
|
||||||
inputPosition="top"
|
|
||||||
theme={theme}
|
|
||||||
lang={giscusLang}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,7 @@ export const translations = {
|
|||||||
},
|
},
|
||||||
hero: {
|
hero: {
|
||||||
greeting: "Hello, I'm",
|
greeting: "Hello, I'm",
|
||||||
viewProjects: 'View Projects',
|
viewProjects: 'About Me',
|
||||||
contactMe: 'Contact Me',
|
contactMe: 'Contact Me',
|
||||||
lookingForJob: 'Looking for a Frontend/TS Full-stack Engineer (Remote)? Contact me!',
|
lookingForJob: 'Looking for a Frontend/TS Full-stack Engineer (Remote)? Contact me!',
|
||||||
digitalNomad: 'Exploring the freelance journey, striving to become a digital nomad',
|
digitalNomad: 'Exploring the freelance journey, striving to become a digital nomad',
|
||||||
@@ -77,7 +77,7 @@ export const translations = {
|
|||||||
},
|
},
|
||||||
hero: {
|
hero: {
|
||||||
greeting: '你好,我是',
|
greeting: '你好,我是',
|
||||||
viewProjects: '查看项目',
|
viewProjects: '关于我',
|
||||||
contactMe: '联系我',
|
contactMe: '联系我',
|
||||||
lookingForJob: '正在寻找前端/TS 全栈工程师(远程)?联系我!',
|
lookingForJob: '正在寻找前端/TS 全栈工程师(远程)?联系我!',
|
||||||
digitalNomad: '探索自由职业道路,努力成为数字游民',
|
digitalNomad: '探索自由职业道路,努力成为数字游民',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import TableOfContents from '@/components/layout/TableOfContents.astro';
|
|||||||
import BlogNavigation from '@/components/layout/BlogNavigation.astro';
|
import BlogNavigation from '@/components/layout/BlogNavigation.astro';
|
||||||
import PostMeta from '@/components/blog/PostMeta.astro';
|
import PostMeta from '@/components/blog/PostMeta.astro';
|
||||||
import Container from '../components/ui/Container';
|
import Container from '../components/ui/Container';
|
||||||
import GiscusComments from '@/components/blog/GiscusComments';
|
import Comments from '@/components/blog/Comments';
|
||||||
|
|
||||||
// Use Astro's MarkdownLayoutProps for proper type safety
|
// Use Astro's MarkdownLayoutProps for proper type safety
|
||||||
export type Props = MarkdownLayoutProps<FrontmatterProps>;
|
export type Props = MarkdownLayoutProps<FrontmatterProps>;
|
||||||
@@ -57,7 +57,7 @@ const finalReadingTime = readTime ? parseInt(readTime.replace(/\D/g, '')) : unde
|
|||||||
<header class="mb-10">
|
<header class="mb-10">
|
||||||
<a
|
<a
|
||||||
href={blogListUrl}
|
href={blogListUrl}
|
||||||
class="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-primary transition-colors duration-200 mb-6 group"
|
class="inline-flex items-center gap-2 text-foreground hover:text-primary transition-colors duration-200 mb-6 group"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="16"
|
width="16"
|
||||||
@@ -69,7 +69,7 @@ const finalReadingTime = readTime ? parseInt(readTime.replace(/\D/g, '')) : unde
|
|||||||
>
|
>
|
||||||
<path d="M6.85355 3.85355C7.04882 3.65829 7.04882 3.34171 6.85355 3.14645C6.65829 2.95118 6.34171 2.95118 6.14645 3.14645L2.14645 7.14645C1.95118 7.34171 1.95118 7.65829 2.14645 7.85355L6.14645 11.8536C6.34171 12.0488 6.65829 12.0488 6.85355 11.8536C7.04882 11.6583 7.04882 11.3417 6.85355 11.1464L3.20711 7.5L6.85355 3.85355Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path>
|
<path d="M6.85355 3.85355C7.04882 3.65829 7.04882 3.34171 6.85355 3.14645C6.65829 2.95118 6.34171 2.95118 6.14645 3.14645L2.14645 7.14645C1.95118 7.34171 1.95118 7.65829 2.14645 7.85355L6.14645 11.8536C6.34171 12.0488 6.65829 12.0488 6.85355 11.8536C7.04882 11.6583 7.04882 11.3417 6.85355 11.1464L3.20711 7.5L6.85355 3.85355Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path>
|
||||||
</svg>
|
</svg>
|
||||||
{t('blog.backToList')}
|
<span class="font-medium">{t('blog.backToList')}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<PostMeta
|
<PostMeta
|
||||||
@@ -107,7 +107,7 @@ const finalReadingTime = readTime ? parseInt(readTime.replace(/\D/g, '')) : unde
|
|||||||
|
|
||||||
<!-- Comments Section -->
|
<!-- Comments Section -->
|
||||||
<div class="mt-10 sm:mt-16 border-t border-border pt-10">
|
<div class="mt-10 sm:mt-16 border-t border-border pt-10">
|
||||||
<GiscusComments client:only="react" lang={lang} />
|
<Comments client:only="react" lang={lang} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Author Card moved to bottom with enhanced styling -->
|
<!-- Author Card moved to bottom with enhanced styling -->
|
||||||
|
|||||||
127
src/pages/blog/posts/2024060801.md
Normal file
127
src/pages/blog/posts/2024060801.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
---
|
||||||
|
layout: "@/layouts/BlogPostLayout.astro"
|
||||||
|
title: "1Panel Automated Deployment Notes: Say Goodbye to Manual Uploads, One-Command Takeoff from Local"
|
||||||
|
description: "Learn how to build a lightweight deployment pipeline using 1Panel with SSH key authentication and ACL permissions. Complete guide for solo developers to automate server deployments."
|
||||||
|
date: "2024-06-08"
|
||||||
|
image: "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=1470&auto=format&fit=crop"
|
||||||
|
tags: ["1Panel", "DevOps", "Automation", "SSH", "Deployment"]
|
||||||
|
tagId: ["1panel", "devops", "automation", "ssh", "deployment"]
|
||||||
|
category: "DevOps"
|
||||||
|
categoryId: "devops"
|
||||||
|
readTime: "5 min read"
|
||||||
|
---
|
||||||
|
|
||||||
|
Updating code used to make me feel like a "human FTP client":
|
||||||
|
|
||||||
|
1. Package everything locally, sweat included
|
||||||
|
2. Open browser, log into 1Panel (forgot password again?)
|
||||||
|
3. Dig through file manager like an archaeologist
|
||||||
|
4. Manual upload, manual overwrite, plus manual cleanup of old files
|
||||||
|
|
||||||
|
This whole ritual took at least 5 minutes each time, and last week I actually brought the site down for 10 minutes because I forgot to upload a new config file. Enough is enough—I built this lightweight deployment pipeline. No heavy Jenkins setup needed, perfect for solo developers or small projects. Fast, reliable, and nobody else to blame.
|
||||||
|
|
||||||
|
## 1. Create a "Deployment Runner" Account
|
||||||
|
|
||||||
|
Running scripts as root? That's like performing surgery with a chainsaw. Let's create a dedicated account just for deployments.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create the deployment account
|
||||||
|
sudo adduser deploy_user
|
||||||
|
|
||||||
|
# Critical: disable password login, SSH keys only
|
||||||
|
# Even if someone guesses the password, they can't get in
|
||||||
|
# -l locks the password account (disabled password login)
|
||||||
|
# -u unlocks the password account (if you need enabled password login)
|
||||||
|
sudo passwd -l deploy_user
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. The Permission Puzzle: ACL to the Rescue
|
||||||
|
|
||||||
|
Here's the tricky part: 1Panel has its own permission system. If you `chown` everything to your deployment user, websites in the panel might start throwing 500 errors.
|
||||||
|
|
||||||
|
My solution: **ACL** (Access Control Lists). Think of it as giving your deployment user a "VIP backstage pass"—it can read/write files without messing with 1Panel's original file ownership.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install ACL tools first
|
||||||
|
sudo apt install acl -y
|
||||||
|
|
||||||
|
# Grant access to sites directory (make sure this is your web root)
|
||||||
|
# -R means recursive, but be careful if directory has other sites
|
||||||
|
sudo setfacl -R -m u:deploy_user:rwx /opt/1panel/www/sites
|
||||||
|
|
||||||
|
# Set default inheritance: new sites get access automatically
|
||||||
|
sudo setfacl -R -d -m u:deploy_user:rwx /opt/1panel/www/sites
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. SSH Key Access: No More Password Typing
|
||||||
|
|
||||||
|
Copy your local SSH public key to the server, and never type a password again.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create SSH directory for deployment user
|
||||||
|
sudo -u deploy_user mkdir -p /home/deploy_user/.ssh
|
||||||
|
|
||||||
|
# Append your public key (using >> not > to avoid overwriting)
|
||||||
|
echo "your-ssh-public-key-content" >> /home/deploy_user/.ssh/authorized_keys
|
||||||
|
|
||||||
|
# Permissions must be exact, or SSH will refuse
|
||||||
|
sudo chown -R deploy_user:deploy_user /home/deploy_user/.ssh
|
||||||
|
sudo chmod 700 /home/deploy_user/.ssh
|
||||||
|
sudo chmod 600 /home/deploy_user/.ssh/authorized_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
**Even lazier method**: If you have `ssh-copy-id` locally, just run `ssh-copy-id deploy_user@your-server-ip`.
|
||||||
|
|
||||||
|
## 4. SSH Aliases (For the Truly Lazy)
|
||||||
|
|
||||||
|
Memorize IP addresses? Not in this decade. Add this to your local `~/.ssh/config`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Host your-server-alias # Nickname for your server
|
||||||
|
HostName your-server-ip-address # Your server IP
|
||||||
|
User deploy_user # Login user
|
||||||
|
IdentityFile ~/.ssh/id_rsa # Private key path
|
||||||
|
```
|
||||||
|
|
||||||
|
Now just type `ssh your-server-alias` and you're in. Magic.
|
||||||
|
|
||||||
|
## 5. The Grand Finale: One-Command Deployment
|
||||||
|
|
||||||
|
Create `deploy.sh` in your project root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# For frontend projects, build first
|
||||||
|
# echo "📦 Building..."
|
||||||
|
# npm run build
|
||||||
|
|
||||||
|
echo "🚀 Syncing to production..."
|
||||||
|
# rsync for incremental updates
|
||||||
|
# -a: archive mode (preserves everything)
|
||||||
|
# -v: verbose (show me what's happening)
|
||||||
|
# -z: compress during transfer
|
||||||
|
# --delete: ⚠️ Warning: removes files on target that don't exist locally!
|
||||||
|
# Remove this flag for first sync to be safe
|
||||||
|
rsync -avz --delete --progress ./dist/ your-server-alias:/opt/1panel/www/sites/your-project-directory/
|
||||||
|
|
||||||
|
echo "✅ Deployment complete!"
|
||||||
|
echo "⏱️ Next time just run: ./deploy.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
Make it executable: `chmod +x deploy.sh`. Now deploying is literally one command.
|
||||||
|
|
||||||
|
## 6. Pitfalls I Stepped In (So You Don't Have To)
|
||||||
|
|
||||||
|
1. **Skip `--delete` on first sync**: Do a dry run first. This flag can delete important files if you're not careful.
|
||||||
|
2. **Check ACL permissions**: If files still have permission issues, run `getfacl /opt/1panel/www/sites/your-project` to verify.
|
||||||
|
3. **Verify paths**: 1Panel's default path is `/opt/1panel/www/sites/`, but check if you customized it.
|
||||||
|
4. **Frontend projects**: Don't upload `node_modules`. Your server doesn't need that 200MB baggage.
|
||||||
|
|
||||||
|
## Wrapping Up
|
||||||
|
|
||||||
|
I've been using this setup for weeks, and it just works. What used to be a 5-minute chore is now a 30-second command. The mental load is gone—no more "did I forget that one file?" anxiety.
|
||||||
|
|
||||||
|
For small to medium projects that don't need full CI/CD complexity, this lightweight approach is perfect. A few minutes to set up, a lifetime of easy deployments.
|
||||||
|
|
||||||
|
**Final reminder**: Automation is powerful but dangerous. Test with a dummy directory first, especially before using `--delete`. Happy deploying, and may your uptime be forever!
|
||||||
@@ -1,78 +1,87 @@
|
|||||||
---
|
---
|
||||||
layout: "@/layouts/BlogPostLayout.astro"
|
layout: "@/layouts/BlogPostLayout.astro"
|
||||||
title: "Stop Being Held Hostage by 'Best Practices': Confessions of a Full-Stack Developer's Tech Stack Struggles"
|
title: "All This Tinkering... For What? Engineering Reflections from a Full-Stack Developer"
|
||||||
description: "A full-stack developer's honest reflection on getting trapped by chasing 'best practices' while building a simple 30-endpoint project. This article explores the cognitive load of heavy frameworks, the Monorepo trap, and proposes a practical two-tier tech stack selection strategy."
|
description: "An honest reflection on tech stack selection paralysis. One developer's journey from chasing 'best practices' to realizing that technology is a tool, not a totem—how to avoid over-engineering and find the right fit for your actual needs."
|
||||||
date: "2026-01-08"
|
date: "2026-01-08"
|
||||||
image: "https://images.unsplash.com/photo-1518770660439-4636190af475?q=80&w=1470&auto=format&fit=crop"
|
image: "https://images.unsplash.com/photo-1518770660439-4636190af475?q=80&w=1470&auto=format&fit=crop"
|
||||||
tags: ["Tech Stack", "Full-Stack Development", "Framework Selection", "Best Practices", "Developer Experience"]
|
tags: ["Reflections", "Full-Stack", "Engineering", "Personal Reflection"]
|
||||||
tagId: ["tech-stack", "fullstack", "framework", "best-practices", "developer-experience"]
|
tagId: ["reflections", "fullstack", "engineering", "development-reflection"]
|
||||||
category: "Technology"
|
category: "Engineering"
|
||||||
categoryId: "technology"
|
categoryId: "engineering"
|
||||||
readTime: "6 min read"
|
readTime: "8 min read"
|
||||||
---
|
---
|
||||||
|
|
||||||
> **Foreword:**
|
> *A log of the soul-crushing details I encountered during a recent project setup. This is a summary of my own internal tug-of-war—lessons learned the hard way through endless trial and error. It might not be the "correct" way, but every word was paid for in lost time.*
|
||||||
> I am a developer who transitioned from Frontend to Node.js Full-Stack. This article is simply a summary of my recent experiences and reflections while developing a project. Given my limited knowledge and perspective, the views expressed here may not be universally "correct" or represent industry standards. This is just a personal debrief after stepping into countless pitfalls, shared in the hope of exchanging ideas with the community and providing a reference for those facing similar dilemmas. If there are any inaccuracies, please feel free to correct me in the comments.
|
|
||||||
|
|
||||||
Recently, I set out to build a small bookmark-style tool with only about 30 endpoints. I thought it would take two weeks; instead, I spent over a month just "wrestling" with the tech stack.
|
I recently set out to build a simple bookmarking tool. It only needed about 30 endpoints, yet I spent nearly a month agonizing over the tech stack. I felt like I was wandering through a dark forest, sprinting toward every glimmer of light only to find a new pitfall waiting for me.
|
||||||
|
|
||||||
I felt like a hunter lost in a technical fog: wherever I saw a light (a new tool or an "expert" opinion), I rushed toward it, only to find a deeper pit hidden behind every glow.
|
### Step 1: Chasing the "Mainstream" and Feeling the Friction
|
||||||
|
|
||||||
## 1. Chasing Trends is the Start of Internal Friction
|
It started because I wanted to build a side project. In my day job, I use Vue; it’s second nature to me. But everywhere I looked online, the consensus was: **"Next.js + React + shadcn-ui"** is the gold standard. Great ecosystem, endless components, future-proof.
|
||||||
|
|
||||||
I started by following the crowd and chose **Next.js + NestJS + shadcn-ui**. I thought, "Since everyone says this is the 'Full-Stack Gold Standard,' I can't go wrong." The reality, however, gave me a swift wake-up call.
|
I told myself: *Don’t get stuck in your comfort zone. Try something new.* So, I benched Vue and picked up React. I was immediately hit by **choice paralysis.**
|
||||||
|
|
||||||
In Next.js, I wasted a whole week just deciding on a data-fetching and state management solution (SWR vs. Zustand?). Once I finally started, I was overwhelmed by the complexity of Server Components (RSC) vs. Client Components—constantly defining `"use client"`, fixing mysterious Hydration errors, and manually managing state dependencies while optimizing endless callback functions.
|
It wasn't just a choice between A and B; it was a choice between five different ways to do everything. I spent more time reading docs and comparisons than writing logic:
|
||||||
|
|
||||||
I kept thinking: **I just want to write some simple business logic. Why am I spending 80% of my energy dealing with the overhead of the framework?**
|
* Do I fetch data with **SWR** or **TanStack Query**?
|
||||||
|
* Is global state better in **Zustand**, **Redux**, or **Jotai**?
|
||||||
|
* How do I bridge the gap between server and client data?
|
||||||
|
|
||||||
## 2. The Heavier the Framework, the Heavier the Cognitive Load
|
Instead of building features, I was researching "how to build." To make matters worse, I kept hitting those cryptic **"Hydration Errors."** One refresh, one error, and an hour gone trying to find the mismatch.
|
||||||
|
|
||||||
Later, I switched the frontend to Nuxt, which was indeed smoother. But on the backend, I stuck with **NestJS**, chasing so-called "standardization" and "enterprise engineering."
|
The most draining part was tagging everything with `"use client"`. I felt less like a developer and more like a clerk labeling boxes. I kept thinking: **"If most of this ends up running on the client anyway, why am I using a framework that splits the stack so aggressively and demands such high mental overhead?"**
|
||||||
|
|
||||||
But I only had 30 endpoints. The logic was incredibly simple. In NestJS, I was forced to write Controllers, Services, Modules, DTOs... the amount of code tripled. Even worse was the **ESM compatibility issue**. NestJS still clings to the CommonJS dream, leading to constant configuration errors when I tried to use modern ESM-only libraries. To run a simple TypeScript Worker thread, I had to spend hours researching ESM compilers.
|
### Step 2: Retreating to Comfort—and the Fear of Missing Out
|
||||||
|
|
||||||
The most frustrating part was **Swagger integration**. Most people prefer Zod for validation now, but Swagger is deeply coupled with the Class-Validator (Decorator) pattern. To get Swagger to recognize my Zod schemas and generate documentation, I had to manually write adapters and custom decorators.
|
I went back to my familiar Vue ecosystem and fired up Nuxt. Suddenly, I could breathe. My productivity skyrocketed.
|
||||||
|
|
||||||
**I felt like I wasn't building a product; I was repairing a broken tractor with incompatible parts.**
|
But the peace didn't last. I’d see another post praising the Next.js ecosystem and start doubting myself: **"Am I choosing the wrong path? Am I falling behind?"** I gave Next.js another shot for a few days, and like clockwork, all those petty, nagging frustrations returned.
|
||||||
|
|
||||||
## 3. Monorepo: The "Tender Trap" for Indie Developers
|
That’s when I realized: **Tech isn't about "better" or "worse"; it’s about fit.** Trust your "handfeel." That sense of flow (or lack thereof) doesn't lie.
|
||||||
|
|
||||||
To pursue "code reuse," I even set up a **Monorepo**.
|
### Step 3: The "Professionalism" Trap
|
||||||
|
|
||||||
I thought: *Front-end and back-end sharing types, enums, and error codes—how elegant!* The reality: trying to get a pure ESM frontend to share a package with a non-pure ESM backend plunged me into a bottomless pit of build configurations. Due to the NestJS environment, I had to compile and export the shared package every time I made a change, making frequent debugging and code modification an absolute nightmare.
|
Once the frontend was settled, I moved to the backend. I chose **NestJS** because it’s billed as the most "Enterprise-grade" option. To make it even more "professional," I forced it into a **Monorepo**. I effectively built myself a prison:
|
||||||
|
|
||||||
Code that should have taken one minute to write took ten because I was busy dealing with cross-package debugging, TS type synchronization, and build logic.
|
* **Immense Friction:** Changing a single shared enum or type meant re-building, waiting for workspace syncs, and restarting services. A 10-second change turned into a 1-minute wait.
|
||||||
|
* **Compatibility Hell:** NestJS is still clinging to CommonJS. Trying to use a pure ESM library or running a TS Worker thread resulted in config errors that made me question my career choices.
|
||||||
|
* **Ceremony Over Substance:** For 30 endpoints, I was writing endless Controllers, Services, Modules, and DTOs. The lines of code tripled, but the core logic stayed exactly the same.
|
||||||
|
* **The Swagger Tax:** I wanted to use Zod for validation, but Swagger only recognizes decorators (class-validator). I ended up maintaining two nearly identical data definitions just to have an API doc.
|
||||||
|
|
||||||
**I finally realized: Monorepos are built to solve "organizational collaboration." For an indie developer, they are often a productivity killer.**
|
**I traded my immediate efficiency for the "advantages of a large team" that I don't actually have.** I wanted speed; the framework demanded "maintainability" for a scale I'll likely never hit.
|
||||||
|
|
||||||
## 4. Returning to Pragmatism: My "Two-Tier Strategy"
|
### Step 4: Stripping it Down and Losing My Way
|
||||||
|
|
||||||
At the end of all this exhaustion, I reflected: Is there a perfect framework? The answer is no; there is only the *suitable* one. Consequently, I have simplified my selection logic into two tiers:
|
I ditched the heavy frameworks for **Fastify**, thinking I’d keep it lean. But I quickly discovered that the problem wasn't just "weight"—it was **"the anxiety of absolute freedom."**
|
||||||
|
|
||||||
* **Tier A: Rapid Validation (MVP / Personal Projects)**
|
Fastify is liberating, but for someone used to structure, freedom is a burden:
|
||||||
**Stack: Nuxt All-in-One.** Don't even separate the frontend and backend. Nuxt's built-in Server API (Nitro) is more than enough for small to medium businesses. Types are naturally shared, and there are no CORS or build-sync headaches. At the validation stage, **"Speed" is a hundred times more important than "Elegance."**
|
|
||||||
|
|
||||||
* **Tier B: Complex Business (Large Projects / Team Collaboration)**
|
* "Where should this file go?"
|
||||||
**Stack: Nuxt + NestJS (Decoupled) + Monorepo.** Only when the business is complex enough to require strict layering, Dependency Injection (DI) for decoupling, and multi-person collaboration will I endure the "ceremony" and management costs of these heavy frameworks.
|
* "How do I organize this into a plugin?"
|
||||||
|
* Logging, queues, auth—everything NestJS gave me for free now required me to find, integrate, and debug a third-party library.
|
||||||
|
|
||||||
|
I hated the rules of heavy frameworks, yet I craved their structure. My mindset was completely torn.
|
||||||
|
|
||||||
|
### The Epiphany: Tech is a Tool, Not a Totem
|
||||||
|
|
||||||
## 5. A Side Note: A New Hope in AdonisJS
|
One night, working late, I asked myself: **"Why am I doing this? I just wanted to build a simple tool."**
|
||||||
|
|
||||||
Just as I was summarizing these strategies, I stumbled upon a new framework—**AdonisJS**. Many developers describe it as the "Laravel of Node.js."
|
Everything clicked. I was applying "Long-term Enterprise Maintenance" standards to a "Solo Rapid Prototype." It was like trying to build an eight-lane highway just to go to the grocery store around the corner.
|
||||||
|
|
||||||
I took a quick look at its philosophy, and it seems to precisely hit the pain points I mentioned: it supports ESM natively, has a powerful built-in ORM and Auth solution, and doesn't require jumping through hoops with custom adapters just to get automated Swagger documentation.
|
I set a new, simple rule for myself:
|
||||||
|
|
||||||
This "Convention over Configuration" full-stack framework seems to balance development efficiency with engineering quality. I plan to use it in my next project and will share my findings once I have more experience.
|
1. **For Prototypes & Small Tools:** Use **Nuxt (or a monolithic framework) exclusively.** Frontend and backend in one repo, natural type synchronization, no CORS issues, no sync friction. The goal is to *ship*.
|
||||||
|
2. **For Complex Systems & Large Teams:** *Then* consider **Nuxt + NestJS + Monorepo.** The complexity is a fair trade for the architectural guardrails.
|
||||||
|
|
||||||
## 6. Conclusion: A Few Words of Advice
|
### A New Contender: AdonisJS
|
||||||
|
|
||||||
1. **There is no perfect framework, only the one that fits the moment.** Don't expect any "star" framework to solve all your problems; they all come with a cost.
|
During this spiral, I stumbled upon **AdonisJS**. It’s often called the "Laravel of Node.js." From a quick look, it seems to hit the sweet spot: it has the structure I crave (ORM and Auth built-in) but feels modern and supports ESM natively. I might give it a spin next time.
|
||||||
2. **Do not easily try a tech stack you aren't familiar with during indie development or tight deadlines.** Unless you truly have the time and energy to burn. You think you're learning new tech, but you're actually burning your product's lifespan.
|
|
||||||
3. **Be wary of "Big Tech Best Practices."** Many tools built to solve pain points in giant corporations (like Monorepos or extreme layering) only create pain points in personal projects.
|
|
||||||
4. **Familiarity > Modernity.** Even if a framework is called "old school," if it's intuitive to you, lets you finish work early, and helps you write clearer logic with AI assistance, it is your "silver bullet."
|
|
||||||
|
|
||||||
**The best tech stack is the one that allows you to forget the technology itself and focus on creating value.**
|
### Final Thoughts
|
||||||
|
|
||||||
Finally, the solutions I've summarized are only what fits my personal habits and current understanding; they may not work for everyone. Everyone's business scenarios and technical backgrounds are different. **If you have better ideas or different solutions, I'd love to hear them in the comments so I can learn from you too.** If I've missed anything, please let me know. Thanks in advance!
|
1. **Don’t blind-follow the hype:** What works for Vercel might not work for your weekend project.
|
||||||
|
2. **Trust your "handfeel":** If a tool makes you feel productive, that's worth more than any "State of JS" ranking.
|
||||||
|
3. **Ship first, optimize later:** For a solo project, an un-launched architecture is just expensive fan fiction.
|
||||||
|
4. **Watch out for "Config Friction":** If you spend more time in `.json` and `.config.js` files than in your logic, your tools are failing you.
|
||||||
|
|
||||||
|
This was my journey through the weeds. **My approach might be "wrong" or even a bit clumsy.** I’d love to hear how you handle the "stack anxiety." **Do you have a go-to setup that just works, or are you still searching for the "perfect" balance?**
|
||||||
@@ -55,7 +55,7 @@ const tags = extractTags(allPostsArray);
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<BlogLayout title="博客 - 赵桂阳" description="深入我对编程、技术趋势和开发者生活的思考。探索我的最新文章。">
|
<BlogLayout title="博客 - Joy Zhao" description="深入我对编程、技术趋势和开发者生活的思考。探索我的最新文章。">
|
||||||
<main class="min-h-screen">
|
<main class="min-h-screen">
|
||||||
<!-- 头部区域 -->
|
<!-- 头部区域 -->
|
||||||
<Container client:load className="pt-24 pb-12">
|
<Container client:load className="pt-24 pb-12">
|
||||||
|
|||||||
127
src/pages/zh/blog/posts/2024060801.md
Normal file
127
src/pages/zh/blog/posts/2024060801.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
---
|
||||||
|
layout: "@/layouts/BlogPostLayout.astro"
|
||||||
|
title: "1Panel 自动化部署笔记:告别\"手动操作\",本地一键起飞"
|
||||||
|
description: "学习如何基于 1Panel 构建轻量级部署流水线,配合 SSH 密钥认证和 ACL 权限管理。本文为单兵作战的开发者提供了完整的自动化部署方案。"
|
||||||
|
date: "2024-06-08"
|
||||||
|
image: "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=1470&auto=format&fit=crop"
|
||||||
|
tags: ["1Panel", "DevOps", "自动化", "SSH", "部署"]
|
||||||
|
tagId: ["1panel", "devops", "automation", "ssh", "deployment"]
|
||||||
|
category: "DevOps"
|
||||||
|
categoryId: "devops"
|
||||||
|
readTime: "5 min read"
|
||||||
|
---
|
||||||
|
|
||||||
|
以前每次更新代码,都感觉自己像个"人肉传输带":
|
||||||
|
|
||||||
|
1. 本地吭哧吭哧打包
|
||||||
|
2. 打开浏览器,输密码登录 1Panel 面板
|
||||||
|
3. 在文件管理器里翻来覆去找目录
|
||||||
|
4. 手动上传、手动覆盖,有时候还得手动删服务器上的旧文件,就怕哪个缓存文件没清干净
|
||||||
|
|
||||||
|
这一套流程下来,没个三五分钟搞不定,关键还特别容易漏文件。上周就因为我忘记上传一个新加的配置文件,线上挂了十分钟才反应过来。痛定思痛,我决定搞一套轻量级部署方案——不用整 Jenkins 那种大炮打蚊子,特别适合我这种单兵作战的小项目,要的就是快、稳、不求人。
|
||||||
|
|
||||||
|
## 1. 先整个"跑腿小弟"账号
|
||||||
|
|
||||||
|
用 root 跑脚本?那太野了,万一脚本写崩了,服务器直接原地升天。得建个专门干这活的"小弟账号"。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建专门负责部署的账号
|
||||||
|
sudo adduser deploy_user
|
||||||
|
|
||||||
|
# 关键一步:禁用这个账号的密码登录,只认 SSH 密钥
|
||||||
|
# 这样就算有人猜到密码也进不来,安全第一
|
||||||
|
# -l 锁定账号密码(禁用密码登录)
|
||||||
|
# -u 解锁账号密码(如需恢复密码登录)
|
||||||
|
sudo passwd -l deploy_user
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 权限难题的"优雅解法":ACL 授权
|
||||||
|
|
||||||
|
这里有个坑:1Panel 管理的文件有自己的一套权限体系,如果你用 `chown` 强行改属主,面板里的网站可能就直接 500 错误了。
|
||||||
|
|
||||||
|
我的解决方案是 **ACL**(访问控制列表)。这玩意就像给文件加了张"访客通行证",让 `deploy_user` 这个账号能读写文件,但又不会破坏 1Panel 原本的文件归属关系。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 先安装 ACL 工具
|
||||||
|
sudo apt install acl -y
|
||||||
|
|
||||||
|
# 给 sites 目录开绿灯(注意:确认这是你的网站根目录)
|
||||||
|
# -R 表示递归,但如果目录里已有其他站点,建议先备份或确认操作
|
||||||
|
sudo setfacl -R -m u:deploy_user:rwx /opt/1panel/www/sites
|
||||||
|
|
||||||
|
# 设置默认权限继承:以后在面板里新建的站点,自动给 deploy_user 权限
|
||||||
|
sudo setfacl -R -d -m u:deploy_user:rwx /opt/1panel/www/sites
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. SSH 免密登录:从此进出如风
|
||||||
|
|
||||||
|
把你本地电脑的 SSH 公钥塞到服务器上,以后就不用输密码了。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 切换到部署账号的家目录
|
||||||
|
sudo -u deploy_user mkdir -p /home/deploy_user/.ssh
|
||||||
|
|
||||||
|
# 追加你的公钥(用 >> 而不是 >,避免覆盖别人的密钥)
|
||||||
|
echo "your-ssh-public-key-content" >> /home/deploy_user/.ssh/authorized_keys
|
||||||
|
|
||||||
|
# 权限必须严格设置,否则 SSH 会拒绝连接
|
||||||
|
sudo chown -R deploy_user:deploy_user /home/deploy_user/.ssh
|
||||||
|
sudo chmod 700 /home/deploy_user/.ssh
|
||||||
|
sudo chmod 600 /home/deploy_user/.ssh/authorized_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
**更懒的方法**:如果你本地有 `ssh-copy-id` 命令,直接 `ssh-copy-id deploy_user@your-server-ip-address`。
|
||||||
|
|
||||||
|
## 4. SSH 别名配置(懒癌患者的福音)
|
||||||
|
|
||||||
|
记 IP 地址?不存在的。在本地 `~/.ssh/config` 文件里加一段:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Host your-server-alias # 给你服务器起的外号,随便起,好记就行
|
||||||
|
HostName your-server-ip-address # 你的服务器 IP
|
||||||
|
User deploy_user # 登录用户
|
||||||
|
IdentityFile ~/.ssh/id_rsa # 私钥路径
|
||||||
|
```
|
||||||
|
|
||||||
|
配置完,以后登录就直接 `ssh your-server-alias`,爽!
|
||||||
|
|
||||||
|
## 5. 终极一击:一键部署脚本
|
||||||
|
|
||||||
|
在项目根目录创建 `deploy.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 如果是前端项目,先打包
|
||||||
|
# echo "📦 正在打包..."
|
||||||
|
# npm run build
|
||||||
|
|
||||||
|
echo "🚀 开始同步到生产环境..."
|
||||||
|
# 使用 rsync 增量同步
|
||||||
|
# -a: 归档模式,保持文件属性
|
||||||
|
# -v: 显示详细过程
|
||||||
|
# -z: 压缩传输
|
||||||
|
# --delete: ⚠️ 注意:这会删除目标端有而源端没有的文件!
|
||||||
|
# 首次同步建议先去掉这个参数,确认无误后再加上
|
||||||
|
rsync -avz --delete --progress ./dist/ your-server-alias:/opt/1panel/www/sites/your-project-directory/
|
||||||
|
|
||||||
|
echo "✅ 部署完成!"
|
||||||
|
echo "⏱️ 下次更新只需: ./deploy.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
给脚本执行权限:`chmod +x deploy.sh`,以后部署就是一行命令的事儿。
|
||||||
|
|
||||||
|
## 6. 我踩过的坑和注意事项
|
||||||
|
|
||||||
|
1. **首次同步别用 `--delete`**:先完整同步一次,确认文件都对,再加这个参数。不然可能把服务器上的重要文件删了。
|
||||||
|
2. **ACL 权限检查**:如果同步后还是没权限,可以用 `getfacl /opt/1panel/www/sites/your-project-directory` 看看 ACL 设置是否生效。
|
||||||
|
3. **文件路径要对**:1Panel 的默认站点路径是 `/opt/1panel/www/sites/`,但如果你改过,记得调整。
|
||||||
|
4. **前端项目注意**:打包前清理 `node_modules`,这玩意别传到服务器。
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
这套方案我从上周用到现在,真香!以前更新一次至少折腾5分钟,现在30秒搞定。关键是心理负担小了——再也不用担心漏传文件,`rsync` 会帮我搞定一切。
|
||||||
|
|
||||||
|
对于还没到需要 CI/CD 的中小型项目,这种轻量级方案简直完美。几分钟配置,一劳永逸。如果你也在用 1Panel,又被手动上传困扰,试试这个方案吧。
|
||||||
|
|
||||||
|
**最后提醒**:任何自动化操作都有风险,尤其是删除操作。第一次在生产环境用的时候,建议先找个测试目录跑一遍,确认无误再上。祝大家部署顺利,永不宕机!
|
||||||
@@ -1,76 +1,89 @@
|
|||||||
---
|
---
|
||||||
layout: "@/layouts/BlogPostLayout.astro"
|
layout: "@/layouts/BlogPostLayout.astro"
|
||||||
title: "别再被\"最佳实践\"绑架了:一个全栈开发者的选型忏悔录"
|
title: "折腾来折腾去,到底是为了什么? 一个全栈开发者的工程化反思"
|
||||||
description: "一名全栈开发者在开发 30 个接口的小项目时,过度追逐技术潮流而陷入困境的真实记录。本文探讨了重型框架的心智负担、Monorepo 的陷阱,并提出了一套务实的两套技术栈选型方案。"
|
description: "关于技术选型困境的真实反思。一个开发者在'最佳实践'诱惑下的挣扎与醒悟:技术是工具不是目的,如何避免过度工程化,找到真正适合自己的方案。"
|
||||||
date: "2026-01-08"
|
date: "2026-01-08"
|
||||||
image: "https://images.unsplash.com/photo-1518770660439-4636190af475?q=80&w=1470&auto=format&fit=crop"
|
image: "https://images.unsplash.com/photo-1518770660439-4636190af475?q=80&w=1470&auto=format&fit=crop"
|
||||||
tags: ["技术栈选型", "全栈开发", "框架选择", "最佳实践", "开发者体验"]
|
tags: ["全栈", "工程化", "开发感悟"]
|
||||||
tagId: ["tech-stack", "fullstack", "framework", "best-practices", "developer-experience"]
|
tagId: ["fullstack", "engineering", "development-reflection"]
|
||||||
category: "技术分享"
|
category: "工程化"
|
||||||
categoryId: "technology"
|
categoryId: "engineering"
|
||||||
readTime: "6 分钟阅读"
|
readTime: "8 min read"
|
||||||
---
|
---
|
||||||
|
|
||||||
> **写在前面:**
|
> 记录一下最近做项目选型时,那些真能把人搞崩溃的细节。是我自己跟自己较劲,来回折腾后的一点总结。不一定对,但每句话都是花时间踩坑换来的。
|
||||||
> 本人是一名从前端转 Node.js 全栈的开发者。这篇文章只是基于我近期开发项目时的一些真实经历和感悟。由于个人知识储备和认知水平有限,文中的观点不一定正确,更不代表行业标准。这仅仅是我在踩了无数坑后的一点自我总结,发出来是希望能和大家交流,也给有类似纠结的朋友提供一个参考。如有谬误,欢迎评论区指正。
|
|
||||||
|
|
||||||
最近我为了做一个只有 30 个接口的书签类小工具,把自己折腾疯了。
|
最近想做个书签小工具,一共也就 30 来个接口,结果在“用什么技术”这个问题上,我纠结了快一个月。感觉就像在技术的林子里瞎转,看见哪儿有光就往哪儿跑,最后发现每个亮光后面都是个坑。
|
||||||
|
|
||||||
起初,我像个在技术丛林里乱撞的猎人:哪里有亮光(新工具/大牛言论),我就往哪冲。结果发现,每一个亮光后面都藏着一个更深的坑。
|
## 第一步:跟着“技术主流”走,结果浑身难受
|
||||||
|
|
||||||
## 1. 追逐流行,是内耗的开始
|
事情是这样的,我想自己做点小产品。工作上我一直用 Vue,很顺手。但网上看了一圈,大家都在说 **“Next.js + React + shadcn-ui”** 才是现在做东西的正道。生态好、组件多、未来可期。
|
||||||
|
|
||||||
我最开始随大流选了 **Next.js + NestJS + shadcn-ui**。我想着,既然大家都说这是"全栈天花板",那选它准没错。结果现实反手就给了我一巴掌。
|
我一想,不能老用自己熟悉的,换个口味吧。于是放下 Vue,开始用 React。结果一上来就懵了:**选择太多了,不知道该用哪个。**
|
||||||
|
|
||||||
在 Next.js 里,为了选一个数据处理和状态管理方案(SWR 还是 Zustand?),我硬生生磨掉了一周。好不容易开工了,又被服务端渲染(SSR)带来的复杂度搞得头大——频繁定义 `"use client"` 指令、处理莫名其妙的"状态水合(Hydration)"报错、还要手动管理状态依赖并优化一堆回调函数。
|
根本不是简单的二选一,而是每个地方都有好几个选择,我大部分时间都在搜资料、看对比、做选择:
|
||||||
|
|
||||||
我当时就在想:**我只是想写个简单的业务逻辑,为什么要花 80% 的精力去处理框架带来的麻烦?**
|
* 数据请求是用 **SWR** 还是 **TanStack Query**?
|
||||||
|
* 全局状态是用 **Zustand** 还是继续用 **Redux**、**Jotai**?
|
||||||
|
* 服务端和客户端数据怎么同步?在这个边界上我反复拉扯。
|
||||||
|
|
||||||
## 2. 框架重一点,心智负担就大一点
|
感觉我的主要工作不是写代码,而是在研究“该用什么写代码”。更烦的是那些莫名其妙的 **“水合错误(Hydration Error)”**,写个页面一刷新就报错,然后就得花时间找哪里对不上。
|
||||||
|
|
||||||
后来前端切到了 Nuxt 确实顺手了些,但后端我依然守着 **NestJS**,追求所谓的"大而全"。
|
最心累的是到处都要加 `“use client”`。我感觉自己不是在写功能,是在给代码打标签。每加一行我就在想:**“如果这么多东西最终都要在客户端跑,那我为啥要选这个把前后端拆这么碎、心智负担这么重的框架?”**
|
||||||
|
|
||||||
但我一共才 30 个接口,业务逻辑极其简单。但在 NestJS 里,我不得不写 Controller、Service、Module、DTO……代码量翻了几倍。更崩溃的是 **ESM 的兼容问题**,NestJS 依然守着 CommonJS 的旧梦,导致我想用一些最新的 ESM 库时,各种配置文件报错。为了跑通一个简单的 TypeScript Worker 线程,我还得自己去研究 ESM 编译器。
|
## 第二步:用回顺手的,又怕自己落伍了
|
||||||
|
|
||||||
最心累的是 **Swagger 的集成**。现在大家都爱用 Zod 做验证,但 Swagger 深度绑定类装饰器模式(class-validator)。为了让 Swagger 识别 Zod Schema 并生成文档,我得自己手搓适配器和装饰器。
|
我回到熟悉的 Vue 生态,用了 Nuxt。果然舒服多了,开发效率起飞。
|
||||||
|
|
||||||
**我感觉自己不是在开发产品,我是在修一辆零件互不兼容的破拖拉机。**
|
但安静没两天,看网上还是到处在夸 Next.js 生态好。心里又开始想:**“我是不是选错了?会不会错过什么?”** 不信邪,又回头去试了几天 Next.js。结果,那些熟悉的、琐碎的麻烦全回来了。
|
||||||
|
|
||||||
## 3. Monorepo:独立开发者的"温柔陷阱"
|
这回我彻底认了:**技术没有绝对的好坏,只有合不合适。我相信自己用着舒服的感觉,这种“手感”骗不了人。**
|
||||||
|
|
||||||
为了追求代码复用,我还折腾了 **Monorepo**。
|
## 第三步:总想“搞得专业点”,结果给自己挖坑
|
||||||
|
|
||||||
我想着,前后端共享类型、枚举、错误码,多优雅!但现实是:为了让纯 ESM 的前端和非纯血 ESM 的后端共享一个包,我陷入了无穷无尽的编译配置中。由于 NestJS 的环境问题,我必须为共享包进行编译导出才能使用,导致频繁修改调试代码时异常麻烦。
|
前端定了,该写后端了。我选了大家说最工程化的 **NestJS**。为了显得更专业,又强行上了 **Monorepo**。这下彻底麻烦了,我亲手给自己搞了个大工程:
|
||||||
|
|
||||||
原本一分钟写完的代码,因为要处理跨包调试、TS 类型同步和打包逻辑,硬生生变成了十分钟。
|
* **开发摩擦力极大:** 在 Monorepo 里改个公共的枚举值或类型,要重新构建、等 Workspace 同步、重启服务。原本 10 秒钟的事,现在要等 1 分钟。
|
||||||
|
* **兼容性地狱:** NestJS 守着 CommonJS 的旧梦,我想用个纯 ESM 的库或者跑个 TypeScript Worker 线程,配置文件的报错能让你怀疑人生,得花大把时间研究编译器路径。
|
||||||
|
* **仪式感的内耗:** 就 30 个接口,要写一堆 Controller、Service、Module、DTO。代码量翻了几倍,但核心逻辑一点没变。
|
||||||
|
* **Swagger 的折磨:** 我想用 Zod 做验证,Swagger 却只认装饰器(class-validator)。为了那张 API 文档,我得被迫维护两份几乎一样的数据定义,手动同步两份 DTO 的痛苦只有写过的人才懂。
|
||||||
|
|
||||||
**我悟了:Monorepo 是为了解决"组织协作"的,对于独立开发者,它往往是效率杀手。**
|
**我用一个可能永远用不上的“大团队优势”,换来了眼前的“低效率”。** 我想要的是快点做出来,但重框架让我优先考虑怎么让代码“更好维护”,这完全本末倒置了。
|
||||||
|
|
||||||
## 4. 回归务实:我的"两套方案"
|
## 第四步:换个轻量的,反而更不会写了
|
||||||
|
|
||||||
在折腾的终点,我反思:有没有完美的框架?结论是没有,只有适合的。于是,我把我的选型逻辑简化成了两套:
|
我决定不用这些重框架了,选了 **Fastify**,想着以后不行还能换回去。但我发现,问题不只是“重”,而是 **“自由带来的恐慌”**。
|
||||||
|
|
||||||
* **方案 A:快速验证(MVP / 个人项目)**
|
Fastify 很自由,但对我这种习惯了某种结构的人来说,自由反而成了压力:
|
||||||
**技术栈:Nuxt 一把梭。** 别分前后端了,Nuxt 的内置 Server API(Nitro)足够处理中小业务。类型天然共享,没有跨域烦恼。在验证阶段,**"快"比"优雅"重要一百倍。**
|
|
||||||
|
|
||||||
* **方案 B:复杂业务(大型项目 / 团队协作)**
|
* “代码该放哪?”
|
||||||
**技术栈:Nuxt + NestJS 分离 + Monorepo。** 当业务复杂到需要严格的分层、需要依赖注入(DI)解耦、需要多人协作时,才去忍受这种"重"框架带来的仪式感和管理成本。
|
* “这个功能怎么组织成插件?”
|
||||||
|
* 日志、队列、鉴权这些在 NestJS 里现成的,现在都要自己找库、集成、调试。
|
||||||
|
|
||||||
## 5. 题外话:意外发现的新希望 AdonisJS
|
我嫌重框架规矩多,但真给自由了,我又不知道该怎么用了。我从复杂的流程里出来,又掉进了“什么都要自己找”的坑。
|
||||||
|
|
||||||
就在我总结完上述方案后,我最近无意间发现了一个新的框架——**AdonisJS**。它被很多开发者评价为 Node.js 界的 Laravel。
|
**我终于明白:我选的那些框架,都基于我自己的知识惯性。我嫌重框架规矩多,但内心又想要固定结构,这种心态本身就是撕裂的。**
|
||||||
|
|
||||||
我简单看了一下它的设计理念,感觉它似乎精准地击中了我上述遇到的很多痛点:它原生支持 ESM、内置了强大的 ORM 和 Auth 方案、不需要像 NestJS 那样去折腾各种复杂的适配器来搞定自动化的 Swagger 文档生成。
|
## 终于想明白:技术是拿来用的,不是拿来供着的
|
||||||
|
|
||||||
这种"约定优于配置"的全栈框架,似乎能平衡开发效率与工程质量。我准备在接下来的项目中实际试用一下,如果确实好用,有了心得后再专门写篇文章分享给大家。
|
又一个加班到很晚的深夜,我对着屏幕问自己:**我折腾这么久,到底图啥?不就是想快点做出这个小工具吗?**
|
||||||
|
|
||||||
## 6. 总结:给开发者的一点建议
|
一下子通透了。我之前一直在用“大团队长期维护”的标准,来要求我这个“个人快速项目”。这就像为了去隔壁超市买菜,非要先修一条八车道的高速公路。
|
||||||
|
|
||||||
1. **没有完美的框架,只有最适合当下的。** 不要指望任何一个明星框架能解决所有问题,它们都有代价。
|
我给自己定了个简单的规矩:
|
||||||
2. **不要在独立开发或工作中轻易尝试自己不熟悉的技术栈。** 除非你真的有大把的时间和精力去踩坑。你以为在学新技术,其实你是在浪费产品的寿命。
|
|
||||||
3. **警惕"大厂最佳实践"。** 很多在大公司里解决痛点的工具(如 Monorepo、过度分层),在个人项目里往往只会制造痛点。
|
|
||||||
4. **熟悉度大于先进性。** 哪怕一个框架被说成是"老古董",只要你用得顺手、能让你早点下班,它就是你的银弹。
|
|
||||||
|
|
||||||
**最好的技术栈,是那个能让你忘记技术本身,而专注于创造价值的工具。**
|
* **做原型、小工具、简单应用**:直接用 **Nuxt 一把梭**。前后端在一个包里写,类型天然同步,没有跨域,没有同步内耗。目标就是快,能跑起来比什么都强。
|
||||||
|
* **复杂系统、多人协作、长期维护**:再考虑 **Nuxt + NestJS + Monorepo**。这时候复杂点能接受,因为确实需要架构支撑。
|
||||||
|
|
||||||
最后想说,我总结的这些方案也仅仅是适合我个人的开发习惯和目前的认知,并不一定适合所有人。每个人面对的业务场景和技术背景都不同,**大家如果有更好的方案或想法,非常欢迎在评论区讨论,让我也有机会学习一下。** 不对的地方也希望各位大佬多多指教,先行谢过!
|
## 顺便提一句:看到个新选择
|
||||||
|
|
||||||
|
折腾的时候注意到了 **AdonisJS**,有人说是 Node.js 里的 Laravel。简单看了看,它好像正好平衡了我的纠结:有我要的固定结构(ORM、Auth 都有),又是现代的风格,原生支持 ESM。下次可以试试。
|
||||||
|
|
||||||
|
## 最后说几句
|
||||||
|
|
||||||
|
1. **别盲目追技术热点**:适合别人的不一定适合你。
|
||||||
|
2. **相信自己用着顺手的感觉**:这比流行趋势重要得多。
|
||||||
|
3. **先做出来再优化**:对于个人项目,跑不起来的架构就是一堆废纸。
|
||||||
|
4. **警惕“配置内耗”**:如果配置时间比写代码还长,就该换工具了。
|
||||||
|
|
||||||
|
这些就是我折腾一圈的想法。**我的做法可能不对,甚至挺笨的。** 所以想听听你们怎么看。**你们有没有类似的问题?最后怎么解决的?** 欢迎在评论区聊聊,让我也学习一下。
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# 从追逐流行到回归工程:我技术选型的“降噪”思考
|
|
||||||
|
|
||||||
过去几年里,我尝试过很多 Node.js 框架和前端技术栈:**Express、Koa、Fastify、Hono.js**,以及前端的 **Next.js**。这些框架和工具都有各自的优点:轻量、灵活、性能好,社区里也有不少人推荐。然而,随着项目的深入,我逐渐发现了一个事实:**简单、快速实现、自己熟悉、生态完善、可扩展性好**,才是最重要的,而不是盲目追逐“最流行”或“性能最高”的框架。
|
|
||||||
|
|
||||||
## 为什么不是 Next.js?
|
|
||||||
|
|
||||||
我也写过 React 和 Next.js,但总感觉生态太杂。尤其是 **状态管理**,从 Redux、MobX 到 Zustand、Jotai,再到 React Query、TanStack Query,每一种都有各自的优缺点,但选择过多反而让我心智负担加重。很多问题并不是没有解决方案,而是解决方案太多、没有统一的“最佳实践”。
|
|
||||||
|
|
||||||
相比之下,**NuxtJS 在 Vue 生态下要更工程化**。比如:
|
|
||||||
|
|
||||||
* 内置路由、布局、服务端渲染等功能,不需要额外选型。
|
|
||||||
* Vue 的响应式和 Pinia 状态管理简单好用,不需要反复对比。
|
|
||||||
* 配合 Nuxt UI、TailwindCSS,可以快速构建出一致的前端体验。
|
|
||||||
|
|
||||||
对我来说,NuxtJS 的“约定优于配置”正好能让我聚焦在业务上,而不是被生态碎片化牵扯精力。
|
|
||||||
|
|
||||||
## 为什么选择 NestJS?
|
|
||||||
|
|
||||||
很多人说 **NestJS 太重**,但我的实际体验是:**它恰到好处**。
|
|
||||||
|
|
||||||
* NestJS 自带 CLI,可以快速生成模块、控制器、服务,大大提升开发效率。
|
|
||||||
* 内置依赖注入、装饰器模式,让代码组织更清晰、可维护。
|
|
||||||
* 和 TypeScript 深度结合,不仅提高了开发体验,也减少了运行时错误。
|
|
||||||
* 对数据库、认证、缓存、消息队列等都有完善的解决方案,遇到问题时社区文档和生态资源都很丰富。
|
|
||||||
|
|
||||||
更重要的是,在 **AI 辅助开发** 的今天,像 NestJS 和 NuxtJS 这种约定清晰、工程化程度高的框架,更容易被 AI 理解和提供正确的解决方案。相比一些过于灵活的轻量框架,AI 在这类框架上的“知识盲区”更少。
|
|
||||||
|
|
||||||
## 两套技术方案
|
|
||||||
|
|
||||||
结合我的经验和实际需求,我总结出了两套适合的技术栈:
|
|
||||||
|
|
||||||
### 1. 轻量级方案(业务简单)
|
|
||||||
|
|
||||||
* **NuxtJS**
|
|
||||||
* **Drizzle ORM**
|
|
||||||
* **Nuxt UI**
|
|
||||||
* **TailwindCSS**
|
|
||||||
|
|
||||||
适合快速原型、简单应用、验证市场。
|
|
||||||
|
|
||||||
### 2. 完整方案(业务复杂)
|
|
||||||
|
|
||||||
* **NestJS**
|
|
||||||
* **NuxtJS**
|
|
||||||
* **Zod**
|
|
||||||
* **Drizzle ORM**
|
|
||||||
* **Redis**
|
|
||||||
* **Postgres**
|
|
||||||
* **Nuxt UI**
|
|
||||||
* **TailwindCSS**
|
|
||||||
|
|
||||||
适合需要后端独立、扩展性强的项目,可以通过微服务、缓存、消息队列等方式支撑更复杂的业务。
|
|
||||||
|
|
||||||
## 性能并不是唯一的答案
|
|
||||||
|
|
||||||
很多人推崇 **Hono.js**、**Elysia.js** 等轻量框架,说它们“简单、快、性能好”。确实,在 Hello World 或简单 API 的场景下,它们的性能数据很漂亮。但在真实的生产环境里,**性能瓶颈往往不在框架**,而在:
|
|
||||||
|
|
||||||
* 数据库查询优化
|
|
||||||
* 缓存策略
|
|
||||||
* 多实例部署
|
|
||||||
* 微服务架构
|
|
||||||
* 负载均衡与扩展能力
|
|
||||||
|
|
||||||
这时候,单一框架的“性能优势”几乎可以忽略。真正决定系统表现的,是整体架构设计和资源利用。
|
|
||||||
|
|
||||||
## 结论
|
|
||||||
|
|
||||||
我的体会是:**技术选型没有绝对的好坏,关键在于适合自己和项目需求**。
|
|
||||||
对我来说,NestJS + NuxtJS 这套组合:
|
|
||||||
|
|
||||||
* 工程化、生态完善,能快速实现功能。
|
|
||||||
* 熟悉度高,减少了心智负担。
|
|
||||||
* 易于扩展,能应对简单和复杂的业务场景。
|
|
||||||
* 在 AI 辅助的今天,更容易获得准确支持。
|
|
||||||
|
|
||||||
因此,哪怕有人说它们“重”或“不如某某框架”,我依然会坚持:**适合自己的,才是最好的技术栈**。
|
|
||||||
342
src/styles/waline-custom.css
Normal file
342
src/styles/waline-custom.css
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
/**
|
||||||
|
* Waline Custom Styles
|
||||||
|
* 适配网站主题风格 - 紫色渐变主题
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ========== 基础变量覆盖 ========== */
|
||||||
|
:root {
|
||||||
|
/* 主题色 - 使用网站的紫色渐变accent */
|
||||||
|
--waline-theme-color: #8B5CF6;
|
||||||
|
--waline-active-color: #EC4899;
|
||||||
|
|
||||||
|
/* 背景色 */
|
||||||
|
--waline-bg-color: oklch(1 0 0);
|
||||||
|
--waline-bg-color-light: oklch(0.97 0 0);
|
||||||
|
--waline-bg-color-hover: oklch(0.93 0 0);
|
||||||
|
|
||||||
|
/* 文字颜色 */
|
||||||
|
--waline-color: oklch(0.4 0 0);
|
||||||
|
--waline-light-grey: oklch(0.65 0 0);
|
||||||
|
--waline-dark-grey: oklch(0.3 0 0);
|
||||||
|
|
||||||
|
/* 边框颜色 */
|
||||||
|
--waline-border-color: oklch(0.87 0 0);
|
||||||
|
|
||||||
|
/* 其他颜色 */
|
||||||
|
--waline-badge-color: #8B5CF6;
|
||||||
|
--waline-info-bg-color: oklch(0.97 0 0);
|
||||||
|
--waline-info-color: oklch(0.55 0 0);
|
||||||
|
--waline-bq-color: oklch(0.93 0 0);
|
||||||
|
|
||||||
|
/* 代码块背景 */
|
||||||
|
--waline-code-bg-color: oklch(0.25 0 0);
|
||||||
|
|
||||||
|
/* 圆角 - 与网站一致 */
|
||||||
|
--waline-border-radius: var(--radius, 0.75rem);
|
||||||
|
|
||||||
|
/* 阴影 */
|
||||||
|
--waline-box-shadow: var(--shadow-md, 0 4px 12px rgba(0, 0, 0, 0.08));
|
||||||
|
|
||||||
|
/* 头像圆角 - 使用网站风格 */
|
||||||
|
--waline-avatar-radius: var(--radius-md, 0.5rem);
|
||||||
|
|
||||||
|
/* 按钮样式 */
|
||||||
|
--waline-btn-radius: var(--radius, 0.75rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 暗黑模式 ========== */
|
||||||
|
.dark {
|
||||||
|
--waline-bg-color: oklch(0.205 0 0);
|
||||||
|
--waline-bg-color-light: oklch(0.269 0 0);
|
||||||
|
--waline-bg-color-hover: oklch(0.32 0 0);
|
||||||
|
|
||||||
|
--waline-color: oklch(0.9 0 0);
|
||||||
|
--waline-light-grey: oklch(0.6 0 0);
|
||||||
|
--waline-dark-grey: oklch(0.75 0 0);
|
||||||
|
|
||||||
|
--waline-border-color: oklch(1 0 0 / 15%);
|
||||||
|
|
||||||
|
--waline-info-bg-color: oklch(0.269 0 0);
|
||||||
|
--waline-info-color: oklch(0.6 0 0);
|
||||||
|
--waline-bq-color: oklch(0.269 0 0);
|
||||||
|
|
||||||
|
--waline-code-bg-color: oklch(0.12 0 0);
|
||||||
|
|
||||||
|
--waline-box-shadow: var(--shadow-md, 0 4px 12px rgba(0, 0, 0, 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 评论面板样式 ========== */
|
||||||
|
.wl-panel {
|
||||||
|
border-radius: var(--waline-border-radius) !important;
|
||||||
|
border: 1px solid var(--waline-border-color) !important;
|
||||||
|
box-shadow: var(--waline-box-shadow) !important;
|
||||||
|
background: var(--waline-bg-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 输入框样式 ========== */
|
||||||
|
.wl-header {
|
||||||
|
border-bottom-color: var(--waline-border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-header label {
|
||||||
|
color: var(--waline-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-header input {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-editor {
|
||||||
|
background: var(--waline-bg-color-light) !important;
|
||||||
|
border-radius: var(--waline-btn-radius) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-editor:focus {
|
||||||
|
background: var(--waline-bg-color-hover) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 按钮样式 ========== */
|
||||||
|
.wl-btn {
|
||||||
|
border-radius: var(--waline-btn-radius) !important;
|
||||||
|
font-size: 0.875em !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-btn:hover {
|
||||||
|
border-color: var(--waline-theme-color) !important;
|
||||||
|
color: var(--waline-theme-color) !important;
|
||||||
|
background: var(--waline-bg-color-light) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-btn.primary {
|
||||||
|
background: linear-gradient(135deg, #8B5CF6, #EC4899) !important;
|
||||||
|
border: none !important;
|
||||||
|
color: white !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-btn.primary:hover {
|
||||||
|
background: linear-gradient(135deg, #7C3AED, #DB2777) !important;
|
||||||
|
border: none !important;
|
||||||
|
color: white !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 头部标签页 ========== */
|
||||||
|
.wl-header label {
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-header label:hover {
|
||||||
|
background: var(--waline-bg-color-light) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 评论卡片样式 ========== */
|
||||||
|
.wl-card {
|
||||||
|
border-bottom-color: var(--waline-border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-card:first-child {
|
||||||
|
margin-inline-start: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-nick {
|
||||||
|
color: var(--waline-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-time {
|
||||||
|
color: var(--waline-info-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-content {
|
||||||
|
color: var(--waline-color) !important;
|
||||||
|
line-height: 1.8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 徽章样式 ========== */
|
||||||
|
.wl-badge {
|
||||||
|
background: linear-gradient(135deg, #8B5CF6, #EC4899) !important;
|
||||||
|
border: none !important;
|
||||||
|
color: white !important;
|
||||||
|
font-size: 0.7em !important;
|
||||||
|
padding: 2px 8px !important;
|
||||||
|
border-radius: 9999px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 操作按钮 ========== */
|
||||||
|
.wl-action {
|
||||||
|
color: var(--waline-light-grey) !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-action:hover {
|
||||||
|
color: var(--waline-theme-color) !important;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-action.active {
|
||||||
|
color: var(--waline-active-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 评论操作 ========== */
|
||||||
|
.wl-delete:hover,
|
||||||
|
.wl-like:hover,
|
||||||
|
.wl-reply:hover,
|
||||||
|
.wl-edit:hover {
|
||||||
|
color: var(--waline-theme-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-delete.active,
|
||||||
|
.wl-like.active,
|
||||||
|
.wl-reply.active,
|
||||||
|
.wl-edit.active {
|
||||||
|
color: var(--waline-active-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 排序按钮 ========== */
|
||||||
|
.wl-sort li {
|
||||||
|
color: var(--waline-info-color) !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-sort li:hover {
|
||||||
|
color: var(--waline-theme-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-sort li.active {
|
||||||
|
color: var(--waline-theme-color) !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 表情弹窗 ========== */
|
||||||
|
.wl-emoji-popup {
|
||||||
|
border-radius: var(--waline-border-radius) !important;
|
||||||
|
box-shadow: var(--waline-box-shadow) !important;
|
||||||
|
background: var(--waline-bg-color) !important;
|
||||||
|
border: 1px solid var(--waline-border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-emoji-popup .wl-tab.active {
|
||||||
|
background: var(--waline-bg-color-light) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-emoji-popup button:hover {
|
||||||
|
background: var(--waline-bg-color-hover) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== GIF 弹窗 ========== */
|
||||||
|
.wl-gif-popup {
|
||||||
|
border-radius: var(--waline-border-radius) !important;
|
||||||
|
box-shadow: var(--waline-box-shadow) !important;
|
||||||
|
background: var(--waline-bg-color) !important;
|
||||||
|
border: 1px solid var(--waline-border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 评论统计 ========== */
|
||||||
|
.wl-count {
|
||||||
|
color: var(--waline-theme-color) !important;
|
||||||
|
font-size: 1.5em !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 空状态 ========== */
|
||||||
|
.wl-empty {
|
||||||
|
color: var(--waline-info-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 头像样式 ========== */
|
||||||
|
.wl-avatar {
|
||||||
|
border-radius: var(--waline-avatar-radius) !important;
|
||||||
|
border: 2px solid var(--waline-border-color) !important;
|
||||||
|
box-shadow: var(--waline-box-shadow) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 回复引用框 ========== */
|
||||||
|
.wl-quote {
|
||||||
|
border-left-color: var(--waline-border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 预览区域 ========== */
|
||||||
|
.wl-preview .wl-content {
|
||||||
|
background: var(--waline-bg-color-light) !important;
|
||||||
|
border-radius: var(--waline-btn-radius) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 反应区域 ========== */
|
||||||
|
.wl-reaction-title {
|
||||||
|
color: var(--waline-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-reaction-item.active .wl-reaction-text {
|
||||||
|
color: var(--waline-theme-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-reaction-votes {
|
||||||
|
border-color: var(--waline-theme-color) !important;
|
||||||
|
color: var(--waline-theme-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-reaction-item.active .wl-reaction-votes {
|
||||||
|
background: var(--waline-theme-color) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 加载动画 ========== */
|
||||||
|
.wl-loading svg {
|
||||||
|
color: var(--waline-theme-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 滚动条样式 ========== */
|
||||||
|
[data-waline] ::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-waline] ::-webkit-scrollbar-track-piece {
|
||||||
|
background: var(--waline-bg-color-light);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-waline] ::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--waline-theme-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-waline] ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--waline-active-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 链接样式 ========== */
|
||||||
|
[data-waline] a {
|
||||||
|
color: var(--waline-theme-color) !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-waline] a:hover {
|
||||||
|
color: var(--waline-active-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 代码块样式 ========== */
|
||||||
|
.wl-content pre,
|
||||||
|
.wl-content pre[class*="language-"] {
|
||||||
|
background: var(--waline-code-bg-color) !important;
|
||||||
|
border-radius: var(--waline-btn-radius) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 移动端适配 ========== */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.wl-panel {
|
||||||
|
border-radius: var(--radius-md, 0.5rem) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-btn {
|
||||||
|
font-size: 0.8em !important;
|
||||||
|
padding: 0.4em 0.8em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wl-editor {
|
||||||
|
min-height: 6em !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user