Compare commits
10 Commits
2389d71b54
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7634c0d1d5 | ||
|
|
e8372dab24 | ||
|
|
d2f79cb32e | ||
|
|
0613966ba4 | ||
|
|
2cd47cf330 | ||
|
|
3c346021e3 | ||
|
|
35e0df26fe | ||
|
|
701021c112 | ||
|
|
69e4a66375 | ||
|
|
97243c33c2 |
55
deploy.sh
Executable file
55
deploy.sh
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 服务器配置
|
||||||
|
SERVER="123.57.93.143"
|
||||||
|
USER="root"
|
||||||
|
REMOTE_DIR="/home/media-front"
|
||||||
|
BACKUP_DIR="$REMOTE_DIR/backup"
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
|
# 本地构建
|
||||||
|
echo "Building project..."
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 检查构建是否成功
|
||||||
|
if [ ! -d "dist" ]; then
|
||||||
|
echo "Build failed! dist directory not found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 创建部署包
|
||||||
|
echo "Creating deployment package..."
|
||||||
|
tar -czf dist.tar.gz dist/
|
||||||
|
|
||||||
|
# 创建远程目录
|
||||||
|
echo "Creating remote directories..."
|
||||||
|
ssh $USER@$SERVER "mkdir -p $REMOTE_DIR $BACKUP_DIR"
|
||||||
|
|
||||||
|
# 如果远程目录存在部署文件,先备份
|
||||||
|
echo "Backing up existing deployment..."
|
||||||
|
ssh $USER@$SERVER "if [ -d $REMOTE_DIR/dist ]; then \
|
||||||
|
tar -czf $BACKUP_DIR/dist_backup_$TIMESTAMP.tar.gz $REMOTE_DIR/dist && \
|
||||||
|
rm -rf $REMOTE_DIR/dist; \
|
||||||
|
fi"
|
||||||
|
|
||||||
|
# 上传新的部署包
|
||||||
|
echo "Uploading new deployment package..."
|
||||||
|
scp dist.tar.gz $USER@$SERVER:$REMOTE_DIR/
|
||||||
|
|
||||||
|
# 解压部署包
|
||||||
|
echo "Extracting deployment package..."
|
||||||
|
ssh $USER@$SERVER "cd $REMOTE_DIR && \
|
||||||
|
tar -xzf dist.tar.gz && \
|
||||||
|
rm dist.tar.gz"
|
||||||
|
|
||||||
|
# 清理本地部署包
|
||||||
|
echo "Cleaning up local deployment package..."
|
||||||
|
rm dist.tar.gz
|
||||||
|
|
||||||
|
echo "Deployment completed successfully!"
|
||||||
|
echo "Your application is now deployed to $REMOTE_DIR/dist"
|
||||||
|
echo "A backup has been created at $BACKUP_DIR/dist_backup_$TIMESTAMP.tar.gz"
|
||||||
|
|
||||||
|
# 清理旧的备份(保留最近10个)
|
||||||
|
echo "Cleaning up old backups..."
|
||||||
|
ssh $USER@$SERVER "cd $BACKUP_DIR && ls -t dist_backup_*.tar.gz | tail -n +11 | xargs rm -f 2>/dev/null || true"
|
||||||
134
package-lock.json
generated
134
package-lock.json
generated
@@ -8,9 +8,12 @@
|
|||||||
"name": "my-score",
|
"name": "my-score",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@melt-ui/svelte": "^0.86.6",
|
||||||
"@skeletonlabs/skeleton": "^2.0.0",
|
"@skeletonlabs/skeleton": "^2.0.0",
|
||||||
|
"@types/node": "^24.0.3",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
"tailwindcss": "^3.4.1"
|
"tailwindcss": "^3.4.1"
|
||||||
},
|
},
|
||||||
@@ -473,6 +476,40 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.0",
|
||||||
|
"@floating-ui/utils": "^0.2.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||||
|
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@internationalized/date": {
|
||||||
|
"version": "3.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.8.1.tgz",
|
||||||
|
"integrity": "sha512-PgVE6B6eIZtzf9Gu5HvJxRK3ufUFz9DhspELuhW/N0GuMGMTLvPQNRkHP2hTuP9lblOk+f+1xi96sPiPXANXAA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/helpers": "^0.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@@ -538,6 +575,41 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@melt-ui/svelte": {
|
||||||
|
"version": "0.86.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@melt-ui/svelte/-/svelte-0.86.6.tgz",
|
||||||
|
"integrity": "sha512-Jer+M7DgIwT5IHfTayb4Iw/fkkxWNmC/mqn/nMh9JrbPbkxmyabfLQnhJ+JDn5HK77f84j34lubO3iqFtYAfMg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.3.1",
|
||||||
|
"@floating-ui/dom": "^1.4.5",
|
||||||
|
"@internationalized/date": "^3.5.0",
|
||||||
|
"dequal": "^2.0.3",
|
||||||
|
"focus-trap": "^7.5.2",
|
||||||
|
"nanoid": "^5.0.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.118"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@melt-ui/svelte/node_modules/nanoid": {
|
||||||
|
"version": "5.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz",
|
||||||
|
"integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18 || >=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -930,6 +1002,15 @@
|
|||||||
"vite": "^6.0.0"
|
"vite": "^6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@swc/helpers": {
|
||||||
|
"version": "0.5.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||||
|
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tsconfig/svelte": {
|
"node_modules/@tsconfig/svelte": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.4.tgz",
|
||||||
@@ -943,6 +1024,14 @@
|
|||||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "24.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz",
|
||||||
|
"integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.1",
|
"version": "8.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||||
@@ -1291,6 +1380,16 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
@@ -1328,6 +1427,15 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dequal": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/didyoumean": {
|
"node_modules/didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||||
@@ -1546,6 +1654,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/focus-trap": {
|
||||||
|
"version": "7.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz",
|
||||||
|
"integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tabbable": "^6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.9",
|
"version": "1.15.9",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||||
@@ -2676,6 +2793,12 @@
|
|||||||
"typescript": ">=5.0.0"
|
"typescript": ">=5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tabbable": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.17",
|
"version": "3.4.17",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||||
@@ -2829,6 +2952,12 @@
|
|||||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.8.3",
|
"version": "5.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
@@ -2843,6 +2972,11 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||||
|
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||||
|
|||||||
@@ -18,10 +18,13 @@
|
|||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@melt-ui/svelte": "^0.86.6",
|
||||||
"@skeletonlabs/skeleton": "^2.0.0",
|
"@skeletonlabs/skeleton": "^2.0.0",
|
||||||
"tailwindcss": "^3.4.1",
|
"@types/node": "^24.0.3",
|
||||||
"postcss": "^8.4.35",
|
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
"axios": "^1.6.7"
|
"axios": "^1.6.7",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"postcss": "^8.4.35",
|
||||||
|
"tailwindcss": "^3.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
146
src/App.svelte
146
src/App.svelte
@@ -5,6 +5,7 @@
|
|||||||
import MediaItem from './lib/MediaItem.svelte';
|
import MediaItem from './lib/MediaItem.svelte';
|
||||||
import MediaFormModal from './lib/MediaFormModal.svelte';
|
import MediaFormModal from './lib/MediaFormModal.svelte';
|
||||||
import DeleteConfirmModal from './lib/DeleteConfirmModal.svelte';
|
import DeleteConfirmModal from './lib/DeleteConfirmModal.svelte';
|
||||||
|
import Toast from './lib/Toast.svelte';
|
||||||
// 类型定义
|
// 类型定义
|
||||||
|
|
||||||
interface ApiResponse<T> {
|
interface ApiResponse<T> {
|
||||||
@@ -49,11 +50,23 @@
|
|||||||
{ id: 'other', label: '其他' }
|
{ id: 'other', label: '其他' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 添加简单的Base64编码函数
|
||||||
|
function encodePassword(message: string): string {
|
||||||
|
return btoa(message);
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化函数:检查认证状态
|
// 初始化函数:检查认证状态
|
||||||
async function initializeAuth() {
|
async function initializeAuth() {
|
||||||
const auth = localStorage.getItem('auth');
|
const authString = localStorage.getItem('auth');
|
||||||
if (auth) {
|
if (authString) {
|
||||||
try {
|
try {
|
||||||
|
const authData = JSON.parse(authString);
|
||||||
|
if (new Date().getTime() > authData.expiry) {
|
||||||
|
localStorage.removeItem('auth');
|
||||||
|
isInitializing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 使用新的验证接口检查登录状态
|
// 使用新的验证接口检查登录状态
|
||||||
const response = await request.get<ApiResponse<{username: string}>>('/auth/verify');
|
const response = await request.get<ApiResponse<{username: string}>>('/auth/verify');
|
||||||
|
|
||||||
@@ -100,32 +113,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听类别变化
|
|
||||||
$effect(() => {
|
|
||||||
if (currentCategory) {
|
|
||||||
currentPage = 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听分页变化
|
|
||||||
$effect(() => {
|
|
||||||
if (currentPage) {
|
|
||||||
fetchMediaList();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 登录处理
|
// 登录处理
|
||||||
async function handleLogin(e: Event) {
|
async function handleLogin(e: Event) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
// 保存认证信息
|
// 对密码进行简单编码
|
||||||
const auth = btoa(`${username}:${password}`);
|
const encodedPassword = encodePassword(password);
|
||||||
localStorage.setItem('auth', auth);
|
|
||||||
|
|
||||||
// 使用验证接口验证登录
|
// 使用验证接口验证登录
|
||||||
const response = await request.get<ApiResponse<{username: string}>>('/auth/verify');
|
const response = await request.post<ApiResponse<{token: string}>>('/user/login', {
|
||||||
|
username,
|
||||||
|
password: encodedPassword
|
||||||
|
});
|
||||||
|
|
||||||
if (response.data.code === 0) {
|
if (response.data.code === 200) {
|
||||||
|
const expiry = new Date().getTime() + 2 * 60 * 60 * 1000; // 2 hours
|
||||||
|
const authData = { token: response.data.data.token, expiry };
|
||||||
|
localStorage.setItem('auth', JSON.stringify(authData));
|
||||||
isAuthenticated = true;
|
isAuthenticated = true;
|
||||||
error = '';
|
error = '';
|
||||||
// 获取初始数据
|
// 获取初始数据
|
||||||
@@ -203,12 +207,30 @@
|
|||||||
error = e.message || 'Failed to delete media';
|
error = e.message || 'Failed to delete media';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算总页数
|
||||||
|
let totalPages = 1;
|
||||||
|
let pageNumbers: (number | string)[] = [];
|
||||||
|
$effect(() => {
|
||||||
|
totalPages = Math.ceil(totalItems / pageSize);
|
||||||
|
if (totalPages <= 5) {
|
||||||
|
pageNumbers = Array.from({length: totalPages}, (_, i) => i + 1);
|
||||||
|
} else if (currentPage <= 3) {
|
||||||
|
pageNumbers = [1,2,3,4,'...',totalPages];
|
||||||
|
} else if (currentPage >= totalPages - 2) {
|
||||||
|
pageNumbers = [1,'...',totalPages-3,totalPages-2,totalPages-1,totalPages];
|
||||||
|
} else {
|
||||||
|
pageNumbers = [1,'...',currentPage-1,currentPage,currentPage+1,'...',totalPages];
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>My Score</title>
|
<title>My Score</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
<Toast />
|
||||||
|
|
||||||
{#if isInitializing}
|
{#if isInitializing}
|
||||||
<div class="min-h-screen flex items-center justify-center bg-gray-100">
|
<div class="min-h-screen flex items-center justify-center bg-gray-100">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
@@ -221,35 +243,35 @@
|
|||||||
<div class="w-[480px] space-y-8 p-8 bg-white rounded-lg shadow-lg">
|
<div class="w-[480px] space-y-8 p-8 bg-white rounded-lg shadow-lg">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-center text-3xl font-extrabold text-gray-900">
|
<h2 class="text-center text-3xl font-extrabold text-gray-900">
|
||||||
My Score
|
我的评分
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-2 text-center text-sm text-gray-600">
|
<p class="mt-2 text-center text-sm text-gray-600">
|
||||||
Sign in to your account
|
登录你的账号
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form class="mt-8 space-y-6" onsubmit={handleLogin}>
|
<form class="mt-8 space-y-6" onsubmit={handleLogin}>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="username" class="block text-sm font-medium text-gray-700">Username</label>
|
<label for="username" class="block text-sm font-medium text-gray-700">用户名</label>
|
||||||
<input
|
<input
|
||||||
id="username"
|
id="username"
|
||||||
name="username"
|
name="username"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||||
placeholder="Enter your username"
|
placeholder="请输入用户名"
|
||||||
bind:value={username}
|
bind:value={username}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
|
<label for="password" class="block text-sm font-medium text-gray-700">密码</label>
|
||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||||
placeholder="Enter your password"
|
placeholder="请输入密码"
|
||||||
bind:value={password}
|
bind:value={password}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -266,31 +288,35 @@
|
|||||||
type="submit"
|
type="submit"
|
||||||
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
>
|
>
|
||||||
Sign in
|
登录
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="min-h-screen bg-gray-100" transition:fade>
|
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100" transition:fade>
|
||||||
<div class="min-w-[1200px] mx-auto px-6 py-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<!-- 类别选择栏 -->
|
<!-- 类别选择栏 -->
|
||||||
<div class="bg-white shadow rounded-lg mb-6">
|
<div class="bg-white/80 backdrop-blur-sm shadow-sm rounded-xl mb-6 border border-gray-200">
|
||||||
<div class="px-6 py-4">
|
<div class="px-6 py-4">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex space-x-6">
|
<div class="flex space-x-4">
|
||||||
{#each categories as category}
|
{#each categories as category}
|
||||||
<button
|
<button
|
||||||
class="px-4 py-2 rounded-md text-sm font-medium {currentCategory === category.id ? 'bg-indigo-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
|
class="px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 {currentCategory === category.id ? 'bg-gradient-to-r from-indigo-500 to-indigo-600 text-white shadow-sm' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
|
||||||
onclick={() => currentCategory = category.id}
|
onclick={() => {
|
||||||
|
currentCategory = category.id;
|
||||||
|
currentPage = 1;
|
||||||
|
fetchMediaList();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{category.label}
|
{category.label}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="px-4 py-2 bg-indigo-600 text-white rounded-md text-sm font-medium hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
class="px-4 py-2 text-sm font-medium text-white bg-gradient-to-r from-indigo-500 to-indigo-600 rounded-lg hover:from-indigo-600 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-all duration-200 shadow-sm hover:shadow-md"
|
||||||
onclick={() => showCreateModal = true}
|
onclick={() => showCreateModal = true}
|
||||||
>
|
>
|
||||||
添加新作品
|
添加新作品
|
||||||
@@ -300,38 +326,62 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 媒体列表 -->
|
<!-- 媒体列表 -->
|
||||||
<div class="bg-white shadow rounded-lg">
|
<div class="bg-white/80 backdrop-blur-sm shadow-sm rounded-xl border border-gray-200">
|
||||||
<div class="px-6 py-4">
|
<div class="px-6 py-4">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="text-center py-8">
|
<div class="text-center py-8">
|
||||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600 mx-auto"></div>
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600 mx-auto"></div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{#each mediaList as media}
|
{#each mediaList as media}
|
||||||
<MediaItem {media} onEdit={handleEditClick} onDelete={handleDeleteClick} />
|
<MediaItem {media} onEdit={handleEditClick} onDelete={handleDeleteClick} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页控制 -->
|
<!-- 分页控制 -->
|
||||||
<div class="mt-6 flex justify-between items-center">
|
<div class="mt-6 flex flex-col md:flex-row md:justify-between md:items-center gap-4 md:gap-0">
|
||||||
<div class="text-sm text-gray-700">
|
<div class="text-sm text-gray-600">
|
||||||
Showing {(currentPage - 1) * pageSize + 1} to {Math.min(currentPage * pageSize, totalItems)} of {totalItems} items
|
显示第 {(currentPage - 1) * pageSize + 1} - {Math.min(currentPage * pageSize, totalItems)} 条,共 {totalItems} 条
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2">
|
<div class="flex items-center space-x-1">
|
||||||
<button
|
<button
|
||||||
class="px-4 py-2 rounded-md text-sm font-medium {currentPage === 1 ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-gray-700 hover:bg-gray-50 border'}"
|
class="px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 {currentPage === 1 ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-gray-700 hover:bg-gray-50 border border-gray-200 shadow-sm hover:shadow-md'}"
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
onclick={() => currentPage = currentPage - 1}
|
onclick={() => {
|
||||||
|
currentPage = currentPage - 1;
|
||||||
|
fetchMediaList();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Previous
|
上一页
|
||||||
</button>
|
</button>
|
||||||
|
{#each pageNumbers as num, i}
|
||||||
|
{#if num === '...'}
|
||||||
|
<span class="px-2 text-gray-400 select-none">...</span>
|
||||||
|
{:else}
|
||||||
<button
|
<button
|
||||||
class="px-4 py-2 rounded-md text-sm font-medium {currentPage * pageSize >= totalItems ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-gray-700 hover:bg-gray-50 border'}"
|
class="px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 {currentPage === num ? 'bg-indigo-500 text-white shadow-sm' : 'bg-white text-gray-700 hover:bg-gray-50 border border-gray-200 shadow-sm hover:shadow-md'}"
|
||||||
disabled={currentPage * pageSize >= totalItems}
|
disabled={typeof num === 'number' ? currentPage === num : false}
|
||||||
onclick={() => currentPage = currentPage + 1}
|
onclick={() => {
|
||||||
|
if (typeof num === 'number') {
|
||||||
|
currentPage = num;
|
||||||
|
fetchMediaList();
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Next
|
{num}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<button
|
||||||
|
class="px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 {currentPage * pageSize >= totalItems ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-gray-700 hover:bg-gray-50 border border-gray-200 shadow-sm hover:shadow-md'}"
|
||||||
|
disabled={currentPage * pageSize >= totalItems}
|
||||||
|
onclick={() => {
|
||||||
|
currentPage = currentPage + 1;
|
||||||
|
fetchMediaList();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
117
src/lib/DatePicker.svelte
Normal file
117
src/lib/DatePicker.svelte
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { format, addMonths, subMonths, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isToday, startOfWeek, endOfWeek } from 'date-fns';
|
||||||
|
import { zhCN } from 'date-fns/locale';
|
||||||
|
import { fade, scale } from 'svelte/transition';
|
||||||
|
|
||||||
|
let { value, placeholder = '选择日期', onSelect } = $props();
|
||||||
|
let isOpen = $state(false);
|
||||||
|
let currentMonth = $state(value ? new Date(value) : new Date());
|
||||||
|
let inputElement: HTMLInputElement;
|
||||||
|
|
||||||
|
function handleSelect(date: Date) {
|
||||||
|
const dateStr = format(date, 'yyyy-MM-dd');
|
||||||
|
onSelect(dateStr);
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (inputElement && !inputElement.contains(event.target as Node)) {
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevMonth() {
|
||||||
|
currentMonth = subMonths(currentMonth, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextMonth() {
|
||||||
|
currentMonth = addMonths(currentMonth, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('click', handleClickOutside);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (value) {
|
||||||
|
currentMonth = new Date(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative w-full">
|
||||||
|
<input
|
||||||
|
bind:this={inputElement}
|
||||||
|
type="text"
|
||||||
|
{placeholder}
|
||||||
|
value={value || ''}
|
||||||
|
readonly
|
||||||
|
onclick={() => isOpen = true}
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
{#if isOpen}
|
||||||
|
<div
|
||||||
|
class="absolute z-50 mt-1 bg-white rounded-lg shadow-lg border border-gray-200"
|
||||||
|
transition:scale={{ duration: 200 }}
|
||||||
|
>
|
||||||
|
<div class="p-2">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<button
|
||||||
|
class="p-1 hover:bg-gray-100 rounded-full"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
prevMonth();
|
||||||
|
}}
|
||||||
|
aria-label="上一月"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span class="text-sm font-medium">
|
||||||
|
{format(currentMonth, 'yyyy年MM月', { locale: zhCN })}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
class="p-1 hover:bg-gray-100 rounded-full"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
nextMonth();
|
||||||
|
}}
|
||||||
|
aria-label="下一月"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-7 gap-1">
|
||||||
|
{#each ['日', '一', '二', '三', '四', '五', '六'] as day}
|
||||||
|
<div class="text-center text-sm text-gray-500 py-1">{day}</div>
|
||||||
|
{/each}
|
||||||
|
{#each eachDayOfInterval({
|
||||||
|
start: startOfWeek(startOfMonth(currentMonth), { weekStartsOn: 0 }),
|
||||||
|
end: endOfWeek(endOfMonth(currentMonth), { weekStartsOn: 0 })
|
||||||
|
}) as date}
|
||||||
|
<button
|
||||||
|
class:text-gray-400={!isSameMonth(date, currentMonth)}
|
||||||
|
class:bg-blue-500={value && format(new Date(value), 'yyyy-MM-dd') === format(date, 'yyyy-MM-dd')}
|
||||||
|
class:text-white={value && format(new Date(value), 'yyyy-MM-dd') === format(date, 'yyyy-MM-dd')}
|
||||||
|
class:border-2={isToday(date)}
|
||||||
|
class:border-blue-500={isToday(date)}
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelect(date);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{format(date, 'd')}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Media } from './interfaces';
|
import type { Media } from './interfaces';
|
||||||
import { fade, scale } from 'svelte/transition';
|
import { fade, scale } from 'svelte/transition';
|
||||||
|
import DatePicker from './DatePicker.svelte';
|
||||||
|
import StarRating from './StarRating.svelte';
|
||||||
|
|
||||||
let {show, mode, submitMedia, handleClose, media: initialMedia, itemType} = $props();
|
let {show, mode, submitMedia, handleClose, media: initialMedia, itemType} = $props();
|
||||||
let media: Media = $state({
|
let media: Media = $state({
|
||||||
@@ -43,30 +45,28 @@
|
|||||||
|
|
||||||
function handleSubmit(e: Event) {
|
function handleSubmit(e: Event) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// 暂时留空,后续处理
|
|
||||||
submitMedia(media);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if show}
|
{#if show}
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
class="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-50"
|
||||||
transition:fade={{ duration: 200 }}
|
transition:fade={{ duration: 200 }}
|
||||||
onclick={handleClose}
|
onclick={handleClose}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-white rounded-lg p-6 w-full mx-4 max-w-[800px]"
|
class="bg-gradient-to-br from-white to-gray-50 rounded-xl p-6 w-full mx-4 max-w-[800px] shadow-2xl"
|
||||||
transition:scale={{ duration: 200 }}
|
transition:scale={{ duration: 200 }}
|
||||||
onclick={(e) => e.stopPropagation()}
|
onclick={(e) => e.stopPropagation()}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<h2 id="modal-title" class="text-xl font-semibold text-gray-900">
|
<h2 id="modal-title" class="text-xl font-semibold bg-gradient-to-r from-gray-900 to-gray-600 bg-clip-text text-transparent">
|
||||||
{mode === 'add' ? '添加新作品' : '编辑作品'}
|
{mode === 'add' ? '添加新作品' : '编辑作品'}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
class="text-gray-400 hover:text-gray-500"
|
class="text-gray-400 hover:text-gray-600 transition-colors duration-200"
|
||||||
onclick={handleClose}
|
onclick={handleClose}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="关闭"
|
aria-label="关闭"
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
id="title"
|
id="title"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={media.title}
|
bind:value={media.title}
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
class="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white/50 backdrop-blur-sm transition-all duration-200"
|
||||||
placeholder="请输入标题"
|
placeholder="请输入标题"
|
||||||
required
|
required
|
||||||
aria-required="true"
|
aria-required="true"
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
<select
|
<select
|
||||||
id="type"
|
id="type"
|
||||||
bind:value={media.type}
|
bind:value={media.type}
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
class="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white/50 backdrop-blur-sm transition-all duration-200"
|
||||||
required
|
required
|
||||||
aria-required="true"
|
aria-required="true"
|
||||||
>
|
>
|
||||||
@@ -108,26 +108,18 @@
|
|||||||
|
|
||||||
<div class="flex justify-between items-center gap-6">
|
<div class="flex justify-between items-center gap-6">
|
||||||
<label class="font-medium text-gray-700 whitespace-nowrap" for="date">日期</label>
|
<label class="font-medium text-gray-700 whitespace-nowrap" for="date">日期</label>
|
||||||
<input
|
<DatePicker
|
||||||
id="date"
|
value={media.date}
|
||||||
type="date"
|
onSelect={(date: string) => media.date = date}
|
||||||
bind:value={media.date}
|
placeholder="选择日期"
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
aria-label="选择日期"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between items-center gap-6">
|
<div class="flex start items-center gap-6">
|
||||||
<label class="font-medium text-gray-700 whitespace-nowrap" for="rating">评分</label>
|
<label for="score" class="font-medium text-gray-700 whitespace-nowrap">评分</label>
|
||||||
<input
|
<StarRating
|
||||||
id="rating"
|
value={media.rating}
|
||||||
type="number"
|
onSelect={(score: number) => media.rating = score}
|
||||||
min="0"
|
|
||||||
max="10"
|
|
||||||
step="0.5"
|
|
||||||
bind:value={media.rating}
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
aria-label="输入评分,范围0-10"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if media.type === 'game' || media.type === 'other'}
|
{#if media.type === 'game' || media.type === 'other'}
|
||||||
@@ -139,7 +131,7 @@
|
|||||||
id="platform"
|
id="platform"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={media.platform}
|
bind:value={media.platform}
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
class="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white/50 backdrop-blur-sm transition-all duration-200"
|
||||||
placeholder="例如:Steam、Netflix、Kindle等"
|
placeholder="例如:Steam、Netflix、Kindle等"
|
||||||
aria-label="输入平台名称"
|
aria-label="输入平台名称"
|
||||||
/>
|
/>
|
||||||
@@ -150,24 +142,26 @@
|
|||||||
<textarea
|
<textarea
|
||||||
id="notes"
|
id="notes"
|
||||||
bind:value={media.notes}
|
bind:value={media.notes}
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
class="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white/50 backdrop-blur-sm transition-all duration-200"
|
||||||
rows="3"
|
rows="10"
|
||||||
placeholder="添加一些备注..."
|
placeholder="添加一些备注..."
|
||||||
aria-label="添加备注信息"
|
aria-label="添加备注信息"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-sm text-left text-gray-500">字数:{media.notes?.length || 0}</div>
|
||||||
|
|
||||||
<div class="flex justify-end gap-3 mt-6">
|
<div class="flex justify-end gap-3 mt-6">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
|
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-all duration-200"
|
||||||
onclick={handleClose}
|
onclick={handleClose}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="button"
|
||||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
onclick={() => submitMedia(media)}
|
||||||
|
class="px-4 py-2 text-sm font-medium text-white bg-gradient-to-r from-indigo-500 to-indigo-600 rounded-lg hover:from-indigo-600 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-all duration-200 shadow-sm hover:shadow-md"
|
||||||
>
|
>
|
||||||
{mode === 'add' ? '添加' : '保存'}
|
{mode === 'add' ? '添加' : '保存'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,68 +1,60 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fade } from 'svelte/transition';
|
|
||||||
import type { Media } from './interfaces';
|
import type { Media } from './interfaces';
|
||||||
let {media, onEdit, onDelete}: {media: Media, onEdit: (media: Media) => void, onDelete: (media: Media) => void} = $props();
|
let {media, onEdit, onDelete}: {media: Media, onEdit: (media: Media) => void, onDelete: (media: Media) => void} = $props();
|
||||||
|
|
||||||
// 状态映射
|
|
||||||
const statusMap = {
|
|
||||||
'completed': '已完成',
|
|
||||||
'in_progress': '进行中',
|
|
||||||
'plan_to_watch': '计划中'
|
|
||||||
};
|
|
||||||
|
|
||||||
// 星星评分组件
|
// 星星评分组件
|
||||||
import { StarRating } from './utils';
|
import { StarRating } from './utils';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="border rounded-lg p-4 hover:bg-gray-50 relative" transition:fade>
|
<div class="bg-white/90 backdrop-blur-sm rounded-xl shadow-[0_2px_8px_-2px_rgba(0,0,0,0.1)] hover:shadow-[0_4px_12px_-4px_rgba(0,0,0,0.15)] transition-all duration-200 border border-gray-100/50 overflow-hidden">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex justify-between items-center mb-1">
|
||||||
|
<span>
|
||||||
|
<h2 class="text-lg font-semibold text-gray-800 line-clamp-1">
|
||||||
|
{media.title}
|
||||||
|
{#if media.platform}
|
||||||
|
<span class="px-2 py-1 text-gray-600 text-sm bg-gray-50/80 rounded-full">{`(${media.platform})`}</span>
|
||||||
|
{/if}
|
||||||
|
</h2>
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
class="delete-button absolute top-0 right-0 p-2 text-gray-400/50 hover:text-red-500 hover:bg-red-50 rounded-full transition-all duration-200"
|
onclick={() => onEdit(media)}
|
||||||
onclick={(e) => {
|
class="text-gray-400 hover:text-indigo-400 transition-colors duration-200"
|
||||||
e.stopPropagation();
|
aria-label="编辑"
|
||||||
onDelete(media);
|
>
|
||||||
}}
|
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => onDelete(media)}
|
||||||
|
class="text-gray-400 hover:text-rose-400 transition-colors duration-200"
|
||||||
aria-label="删除"
|
aria-label="删除"
|
||||||
>
|
>
|
||||||
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex justify-between items-start" role="presentation" onclick={() => onEdit(media)}>
|
|
||||||
<div class="space-y-2 flex-1">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h3 class="text-lg font-medium text-gray-900 truncate max-w-[70%]">
|
|
||||||
{media.title}
|
|
||||||
{#if media.platform}
|
|
||||||
<span class="px-2 py-1 bg-gray-100 rounded text-sm text-gray-700">
|
|
||||||
平台:{media.platform}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-2 ml-auto mr-2">
|
|
||||||
{#if media.rating}
|
|
||||||
<div class="px-2 py-1 bg-gray-100 rounded text-sm text-gray-700 flex items-center gap-1">
|
|
||||||
<span>评分:</span>
|
|
||||||
<div class="flex items-center">
|
|
||||||
{@html StarRating(media.rating / 2)}
|
|
||||||
</div>
|
|
||||||
<span class="ml-1">({media.rating}/10)</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-500">
|
|
||||||
{media.date ? new Date(media.date).toLocaleDateString() : '未设置日期'}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="space-y-2 text-sm text-gray-600 flex justify-between">
|
||||||
|
<p class="flex items-center gap-2">
|
||||||
|
<span class="text-gray-500">评分:</span>
|
||||||
|
<span class="flex items-center gap-1">{@html StarRating((media.rating ?? 0) / 2)}</span>
|
||||||
|
</p>
|
||||||
|
<p class="flex items-center gap-2">
|
||||||
|
<span class="text-gray-500">日期:</span>
|
||||||
|
<span class="text-gray-700">{media.date}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{#if media.notes}
|
{#if media.notes}
|
||||||
<div class="mt-2 text-sm text-gray-600 rounded">
|
<div class="mt-1 pt-1 border-t border-gray-100/50">
|
||||||
<div class="whitespace-pre-wrap text-left line-clamp-4">{media.notes}</div>
|
<p class="text-sm text-gray-600 line-clamp-3">{media.notes}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.delete-button {
|
.delete-button {
|
||||||
|
|||||||
38
src/lib/StarRating.svelte
Normal file
38
src/lib/StarRating.svelte
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { value = 0, maxStars = 10, onSelect } = $props();
|
||||||
|
let hoverValue = $state(0);
|
||||||
|
|
||||||
|
function handleStarClick(index: number) {
|
||||||
|
onSelect(index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStarHover(index: number) {
|
||||||
|
hoverValue = index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave() {
|
||||||
|
hoverValue = 0;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1" onmouseleave={handleMouseLeave} role="presentation">
|
||||||
|
{#each Array(maxStars) as _, i}
|
||||||
|
<div
|
||||||
|
class="text-xl focus:outline-none transition-all duration-200 cursor-pointer hover:scale-110"
|
||||||
|
class:text-yellow-400={i < (hoverValue || value)}
|
||||||
|
class:text-gray-200={i >= (hoverValue || value)}
|
||||||
|
onclick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleStarClick(i);
|
||||||
|
}}
|
||||||
|
onmouseenter={() => handleStarHover(i)}
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
★
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{#if value > 0}
|
||||||
|
<span class="ml-2 text-sm font-medium bg-gradient-to-r from-gray-900 to-gray-600 bg-clip-text text-transparent">{value}分</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
9
src/lib/Toast.svelte
Normal file
9
src/lib/Toast.svelte
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { toast } from './toast';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $toast.visible}
|
||||||
|
<div class="fixed top-6 left-1/2 transform -translate-x-1/2 bg-red-500 text-white px-4 py-2 rounded shadow z-50 transition-all">
|
||||||
|
{ $toast.message }
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
/*
|
/*
|
||||||
* @Date: 2025-05-19 18:10:10
|
* @Date: 2025-05-19 18:10:10
|
||||||
* @LastEditors: 陈子健
|
* @LastEditors: 陈子健
|
||||||
* @LastEditTime: 2025-05-20 18:14:14
|
* @LastEditTime: 2025-06-23 14:54:40
|
||||||
* @FilePath: /my-score/frontend/src/lib/request.ts
|
* @FilePath: /my-score/frontend/src/lib/request.ts
|
||||||
*/
|
*/
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { showToast } from './toast';
|
||||||
|
|
||||||
const request = axios.create({
|
const request = axios.create({
|
||||||
baseURL: '/api',
|
baseURL: '/api',
|
||||||
@@ -18,9 +19,17 @@ const request = axios.create({
|
|||||||
request.interceptors.request.use(
|
request.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
// 从 localStorage 获取认证信息
|
// 从 localStorage 获取认证信息
|
||||||
const auth = localStorage.getItem('auth');
|
const authString = localStorage.getItem('auth');
|
||||||
if (auth) {
|
if (authString) {
|
||||||
config.headers.Authorization = `Basic ${auth}`;
|
try {
|
||||||
|
const auth = JSON.parse(authString);
|
||||||
|
if (auth && auth.token) {
|
||||||
|
config.headers.Authorization = `Basic ${auth.token}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse auth from localStorage', e);
|
||||||
|
localStorage.removeItem('auth');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
@@ -36,9 +45,19 @@ request.interceptors.response.use(
|
|||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
// 清除认证信息
|
// For login failures, we don't redirect. The component will handle the error.
|
||||||
|
if (error.config.url === '/user/login') {
|
||||||
|
return Promise.reject(error.response?.data || error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other 401 errors, it means the token is invalid/expired.
|
||||||
|
// Clear auth data and show toast, then redirect.
|
||||||
localStorage.removeItem('auth');
|
localStorage.removeItem('auth');
|
||||||
return
|
showToast('登录已过期,请重新登录');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/';
|
||||||
|
}, 1500);
|
||||||
|
return new Promise(() => {}); // Prevent further promise chain execution
|
||||||
}
|
}
|
||||||
return Promise.reject(error.response?.data || error);
|
return Promise.reject(error.response?.data || error);
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/lib/toast.ts
Normal file
10
src/lib/toast.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export const toast = writable<{ message: string; visible: boolean }>({ message: '', visible: false });
|
||||||
|
|
||||||
|
export function showToast(message: string, duration = 2000) {
|
||||||
|
toast.set({ message, visible: true });
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.set({ message: '', visible: false });
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
/*
|
/*
|
||||||
* @Date: 2025-05-26 17:39:36
|
* @Date: 2025-05-26 17:39:36
|
||||||
* @LastEditors: 陈子健
|
* @LastEditors: 陈子健
|
||||||
* @LastEditTime: 2025-05-26 17:39:47
|
* @LastEditTime: 2025-06-13 14:32:26
|
||||||
* @FilePath: /my-score/frontend/src/lib/utils.ts
|
* @FilePath: /my-score/frontend/src/lib/utils.ts
|
||||||
*/
|
*/
|
||||||
export const StarRating = (rating: number, maxStars: number = 5) => {
|
export const StarRating = (rating: number, maxStars: number = 5) => {
|
||||||
const stars = [];
|
const stars = [];
|
||||||
|
|
||||||
for (let i = 1; i <= maxStars; i++) {
|
for (let i = 1; i <= maxStars; i++) {
|
||||||
const fill = i <= Math.floor(rating) ? '#FFD700' : 'white';
|
const fill = i <= Math.floor(rating) ? '#FFD700' : 'white';
|
||||||
const isHalfStar = i - 0.5 <= rating && rating < i;
|
const isHalfStar = i - 0.5 <= rating && rating < i;
|
||||||
|
|
||||||
if (isHalfStar) {
|
if (isHalfStar) {
|
||||||
|
console.log('isHalfStar', isHalfStar);
|
||||||
stars.push(`
|
stars.push(`
|
||||||
<div class="relative w-4 h-4">
|
<div class="relative w-4 h-4">
|
||||||
<svg class="absolute w-4 h-4" viewBox="0 0 24 24" fill="white" stroke="currentColor">
|
<svg class="absolute w-4 h-4" viewBox="0 0 24 24" fill="white" stroke="currentColor">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Date: 2025-05-20 17:59:45
|
* @Date: 2025-05-20 17:59:45
|
||||||
* @LastEditors: 陈子健
|
* @LastEditors: 陈子健
|
||||||
* @LastEditTime: 2025-05-20 18:04:34
|
* @LastEditTime: 2025-08-15 11:20:05
|
||||||
* @FilePath: /my-score/frontend/vite.config.ts
|
* @FilePath: /my-score/frontend/vite.config.ts
|
||||||
*/
|
*/
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
@@ -13,7 +13,7 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:5000',
|
target: 'http://review.falsita.cn',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false
|
secure: false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user