Compare commits

...

42 Commits

Author SHA1 Message Date
sqj
e0a1e0af39 updata:v1.2.7 2025-09-13 05:37:57 +08:00
sqj
c1d3a61f9f feat:歌单导入体验优化&封面更新逻辑优化 2025-09-13 05:37:04 +08:00
sqj
d6d806c96e feat:歌单名编辑 2025-09-13 05:14:26 +08:00
sqj
089406464b fix:优化设置页面ui 修复播放界面问题 2025-09-13 04:56:27 +08:00
sqj
c28d5d6ad0 fix:docs 2025-09-11 01:06:46 +08:00
sqj
471147ac82 fix:docs 2025-09-11 01:04:55 +08:00
sqj
7558a67df3 fix:docs 2025-09-11 00:41:32 +08:00
sqj
4a3f0ee124 fix:docs 2025-09-11 00:27:09 +08:00
sqj
5fe6d93d5e feat:支持本地歌单功能&测试阶段网络歌单导入功能 2025-09-10 23:10:19 +08:00
sqj
30fd2ebb9f Merge branch 'main' of https://github.com/timeshiftsauce/CeruMusic
# the commit.
2025-09-09 20:37:59 +08:00
sqj
0f78f117d0 fix: 禁止打开多实例,添加默认封面图,修复已知bug 2025-09-09 20:37:14 +08:00
时迁酱
c79b6951d6 Update sync-releases-to-webdav.yml 2025-09-07 02:02:55 +08:00
时迁酱
5118874712 Update sync-releases-to-webdav.yml 2025-09-07 01:54:52 +08:00
时迁酱
7e5baba969 Update sync-releases-to-webdav.yml 2025-09-07 01:45:13 +08:00
时迁酱
a7af89e35d Update sync-releases-to-webdav.yml 2025-09-07 01:42:39 +08:00
时迁酱
d511efdfce Update sync-releases-to-webdav.yml 2025-09-07 01:35:22 +08:00
时迁酱
9b6050be7a Update sync-releases-to-webdav.yml 2025-09-07 01:34:06 +08:00
时迁酱
57736e60f3 Update sync-releases-to-webdav.yml 2025-09-07 01:23:35 +08:00
时迁酱
6165a2619e Update sync-releases-to-webdav.yml 2025-09-07 01:21:58 +08:00
时迁酱
c933b6e0b4 Update sync-releases-to-webdav.yml 2025-09-07 01:19:35 +08:00
时迁酱
e0e01cbdca Update sync-releases-to-webdav.yml 2025-09-07 01:16:44 +08:00
时迁酱
9b34ecbed9 Update sync-releases-to-webdav.yml 2025-09-07 01:14:28 +08:00
时迁酱
1dda213013 Update sync-releases-to-webdav.yml 2025-09-07 01:03:33 +08:00
sqj
94c2dc740f feat:修改了软件下载源&添加版本上传 2025-09-06 19:32:54 +08:00
sqj
61f062455e feat:修改了软件下载源&添加版本上传 2025-09-06 19:08:45 +08:00
sqj
79827f14f7 feat:修改了软件下载源 2025-09-06 18:57:26 +08:00
sqj
394bdd573c feat:修改了软件下载源 2025-09-06 18:56:35 +08:00
时迁酱
d03d62c8d4 Update uploadpan.yml 2025-09-06 17:25:38 +08:00
时迁酱
be0b0b0390 Update uploadpan.yml 2025-09-06 17:07:01 +08:00
时迁酱
c1d2f3dc8d Update uploadpan.yml 2025-09-06 16:56:55 +08:00
时迁酱
e590c33c66 Update uploadpan.yml 2025-09-06 16:53:37 +08:00
时迁酱
604ac7b553 Update uploadpan.yml 2025-09-06 16:51:33 +08:00
时迁酱
18e233ae10 Update uploadpan.yml 2025-09-06 16:43:40 +08:00
时迁酱
61699c4853 Update uploadpan.yml 2025-09-06 16:41:20 +08:00
时迁酱
6e69920a5d Update uploadpan.yml 2025-09-06 16:31:20 +08:00
时迁酱
a767b008a0 Update uploadpan.yml 2025-09-06 10:08:38 +08:00
时迁酱
41b104e96d Update uploadpan.yml 2025-09-06 10:06:22 +08:00
时迁酱
8562b7c954 Update uploadpan.yml 2025-09-06 10:04:02 +08:00
时迁酱
b61e88b7d9 Create uploadpan.yml 2025-09-06 09:14:55 +08:00
sqj
cc1dbcaf3f 优化整体界面ui效果 2025-09-05 20:58:20 +08:00
sqj
941af10830 新增音频可视化 2025-09-04 21:19:59 +08:00
sqj
576f9697d4 fix: 修复最小化控制栏事件监听取消问题,单曲循环的不会自动开始播放的问题。 2025-08-31 18:01:32 +08:00
95 changed files with 19256 additions and 8394 deletions

158
.github/workflows/auto-sync-release.yml vendored Normal file
View File

@@ -0,0 +1,158 @@
name: Auto Sync New Release to WebDAV
on:
release:
types: [published]
permissions:
contents: read
jobs:
auto-sync-to-webdav:
runs-on: ubuntu-latest
steps:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y curl jq
- name: Get release info
id: release-info
run: |
echo "tag_name=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
echo "release_id=${{ github.event.release.id }}" >> $GITHUB_OUTPUT
echo "release_name=${{ github.event.release.name }}" >> $GITHUB_OUTPUT
- name: Sync new release to WebDAV
run: |
TAG_NAME="${{ steps.release-info.outputs.tag_name }}"
RELEASE_ID="${{ steps.release-info.outputs.release_id }}"
RELEASE_NAME="${{ steps.release-info.outputs.release_name }}"
echo "🚀 开始同步新发布的版本到 WebDAV..."
echo "版本标签: $TAG_NAME"
echo "版本名称: $RELEASE_NAME"
echo "Release ID: $RELEASE_ID"
echo "WebDAV 根路径: ${{ secrets.WEBDAV_BASE_URL }}/yd/ceru"
# 获取该release的所有资源文件
assets_json=$(curl -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID/assets")
assets_count=$(echo "$assets_json" | jq '. | length')
echo "找到 $assets_count 个资源文件"
if [ "$assets_count" -eq 0 ]; then
echo "⚠️ 该版本没有资源文件,跳过同步"
exit 0
fi
# 先创建版本目录
dir_path="/yd/ceru/$TAG_NAME"
dir_url="${{ secrets.WEBDAV_BASE_URL }}$dir_path"
echo "创建版本目录: $dir_path"
if curl -s -f -X MKCOL \
-u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
"$dir_url"; then
echo "✅ 目录创建成功"
else
echo " 目录可能已存在或创建失败,继续执行"
fi
# 处理每个asset
success_count=0
failed_count=0
for i in $(seq 0 $(($assets_count - 1))); do
asset=$(echo "$assets_json" | jq -c ".[$i]")
asset_name=$(echo "$asset" | jq -r '.name')
asset_url=$(echo "$asset" | jq -r '.url')
asset_size=$(echo "$asset" | jq -r '.size')
echo "📦 处理资源: $asset_name (大小: $asset_size bytes)"
# 下载资源文件
safe_filename="./temp_${TAG_NAME}_$(date +%s)_$i"
if ! curl -sL -o "$safe_filename" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/octet-stream" \
"$asset_url"; then
echo "❌ 下载失败: $asset_name"
failed_count=$((failed_count + 1))
continue
fi
if [ -f "$safe_filename" ]; then
actual_size=$(wc -c < "$safe_filename")
if [ "$actual_size" -ne "$asset_size" ]; then
echo "❌ 文件大小不匹配: $asset_name (期望: $asset_size, 实际: $actual_size)"
rm -f "$safe_filename"
failed_count=$((failed_count + 1))
continue
fi
echo "⬆️ 上传到 WebDAV: $asset_name"
# 构建远程路径
remote_path="/yd/ceru/$TAG_NAME/$asset_name"
full_url="${{ secrets.WEBDAV_BASE_URL }}$remote_path"
# 使用 WebDAV PUT 方法上传文件
if curl -s -f -X PUT \
-u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
-T "$safe_filename" \
"$full_url"; then
echo "✅ 上传成功: $asset_name"
success_count=$((success_count + 1))
# 验证文件是否存在
sleep 1
if curl -s -f -u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
-X PROPFIND \
-H "Depth: 0" \
"$full_url" > /dev/null 2>&1; then
echo "✅ 文件验证成功: $asset_name"
else
echo "⚠️ 文件验证失败,但上传可能成功: $asset_name"
fi
else
echo "❌ 上传失败: $asset_name"
failed_count=$((failed_count + 1))
fi
# 清理临时文件
rm -f "$safe_filename"
echo "----------------------------------------"
else
echo "❌ 临时文件不存在: $safe_filename"
failed_count=$((failed_count + 1))
fi
done
echo "========================================"
echo "🎉 同步完成!"
echo "成功: $success_count 个文件"
echo "失败: $failed_count 个文件"
echo "总计: $assets_count 个文件"
if [ "$failed_count" -gt 0 ]; then
echo "⚠️ 有文件同步失败,请检查日志"
exit 1
else
echo "✅ 所有文件同步成功!"
fi
- name: Notify completion
if: always()
run: |
if [ "${{ job.status }}" == "success" ]; then
echo "✅ 版本 ${{ steps.release-info.outputs.tag_name }} 已成功同步到 alist"
else
echo "❌ 版本 ${{ steps.release-info.outputs.tag_name }} 同步失败"
fi

View File

@@ -70,3 +70,146 @@ jobs:
uses: softprops/action-gh-release@v1
with:
files: 'dist/**' # 将dist目录下所有文件添加到release
# 新增:自动同步到 WebDAV
sync-to-webdav:
name: Sync to WebDAV
needs: release # 等待 release 任务完成
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') # 只在标签推送时执行
steps:
- name: Wait for release to be ready
run: |
echo "等待 Release 准备就绪..."
sleep 30 # 等待30秒确保 release 完全创建
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y curl jq
- name: Get latest release info
id: get-release
run: |
# 获取当前标签对应的 release 信息
TAG_NAME=${GITHUB_REF#refs/tags/}
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
# 获取 release 详细信息
response=$(curl -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/releases/tags/$TAG_NAME")
release_id=$(echo "$response" | jq -r '.id')
echo "release_id=$release_id" >> $GITHUB_OUTPUT
echo "找到 Release ID: $release_id"
- name: Sync release to WebDAV
run: |
TAG_NAME="${{ steps.get-release.outputs.tag_name }}"
RELEASE_ID="${{ steps.get-release.outputs.release_id }}"
echo "🚀 开始同步版本 $TAG_NAME 到 WebDAV..."
echo "Release ID: $RELEASE_ID"
echo "WebDAV 根路径: ${{ secrets.WEBDAV_BASE_URL }}/yd/ceru"
# 获取该release的所有资源文件
assets_json=$(curl -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID/assets")
assets_count=$(echo "$assets_json" | jq '. | length')
echo "找到 $assets_count 个资源文件"
if [ "$assets_count" -eq 0 ]; then
echo "⚠️ 该版本没有资源文件,跳过同步"
exit 0
fi
# 先创建版本目录
dir_path="/yd/ceru/$TAG_NAME"
dir_url="${{ secrets.WEBDAV_BASE_URL }}$dir_path"
echo "创建版本目录: $dir_path"
curl -s -X MKCOL \
-u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
"$dir_url" || echo "目录可能已存在"
# 处理每个asset
success_count=0
failed_count=0
for i in $(seq 0 $(($assets_count - 1))); do
asset=$(echo "$assets_json" | jq -c ".[$i]")
asset_name=$(echo "$asset" | jq -r '.name')
asset_url=$(echo "$asset" | jq -r '.url')
asset_size=$(echo "$asset" | jq -r '.size')
echo "📦 处理资源: $asset_name (大小: $asset_size bytes)"
# 下载资源文件
safe_filename="./temp_${TAG_NAME}_$(date +%s)_$i"
if ! curl -sL -o "$safe_filename" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/octet-stream" \
"$asset_url"; then
echo "❌ 下载失败: $asset_name"
failed_count=$((failed_count + 1))
continue
fi
if [ -f "$safe_filename" ]; then
actual_size=$(wc -c < "$safe_filename")
if [ "$actual_size" -ne "$asset_size" ]; then
echo "❌ 文件大小不匹配: $asset_name (期望: $asset_size, 实际: $actual_size)"
rm -f "$safe_filename"
failed_count=$((failed_count + 1))
continue
fi
echo "⬆️ 上传到 WebDAV: $asset_name"
# 构建远程路径
remote_path="/yd/ceru/$TAG_NAME/$asset_name"
full_url="${{ secrets.WEBDAV_BASE_URL }}$remote_path"
# 使用 WebDAV PUT 方法上传文件
if curl -s -f -X PUT \
-u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
-T "$safe_filename" \
"$full_url"; then
echo "✅ 上传成功: $asset_name"
success_count=$((success_count + 1))
else
echo "❌ 上传失败: $asset_name"
failed_count=$((failed_count + 1))
fi
# 清理临时文件
rm -f "$safe_filename"
echo "----------------------------------------"
else
echo "❌ 临时文件不存在: $safe_filename"
failed_count=$((failed_count + 1))
fi
done
echo "========================================"
echo "🎉 同步完成!"
echo "成功: $success_count 个文件"
echo "失败: $failed_count 个文件"
echo "总计: $assets_count 个文件"
- name: Notify completion
if: always()
run: |
if [ "${{ job.status }}" == "success" ]; then
echo "✅ 版本 ${{ steps.get-release.outputs.tag_name }} 已成功同步到 alist"
else
echo "❌ 版本 ${{ steps.get-release.outputs.tag_name }} 同步失败"
fi

View File

@@ -0,0 +1,154 @@
name: Sync Existing Releases to Alist
on:
workflow_dispatch:
inputs:
tag_name:
description: '要同步的特定标签(如 v1.0.0),留空则同步所有版本'
required: false
default: ''
permissions:
contents: read
jobs:
sync-releases-to-alist:
runs-on: ubuntu-latest
steps:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y curl jq
- name: Get all releases
id: get-releases
run: |
response=$(curl -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/releases")
echo "releases_json=$(echo "$response" | jq -c '.')" >> $GITHUB_OUTPUT
- name: Sync releases to Alist自动登录 & 上传)
run: |
# ========== 1. 读取输入参数 ==========
SPECIFIC_TAG="${{ github.event.inputs.tag_name }}"
RELEASES_JSON='${{ steps.get-releases.outputs.releases_json }}'
# ========== 2. Alist 连接信息 ==========
ALIST_URL="https://alist.shiqianjiang.cn" # https://pan.example.com
ALIST_USER="${{ secrets.WEBDAV_USERNAME }}" # Alist 登录账号
ALIST_PASS="${{ secrets.WEBDAV_PASSWORD }}" # Alist 登录密码
ALIST_DIR="/yd/ceru" # 目标根目录
# ========== 3. 登录拿 token ==========
echo "正在登录 Alist ..."
login_resp=$(curl -s -X POST "$ALIST_URL/api/auth/login" \
-H "Content-Type: application/json" \
-d "{
\"username\": \"$ALIST_USER\",
\"password\": \"$ALIST_PASS\"
}")
echo "$login_resp"
token=$(echo "$login_resp" | jq -r '.data.token // empty')
if [ -z "$token" ]; then
echo "❌ 登录失败,返回:$login_resp"
exit 1
fi
echo "✅ 登录成功token 已获取"
# ========== 4. 循环处理 release ==========
releases_count=$(echo "$RELEASES_JSON" | jq '. | length')
echo "找到 $releases_count 个 releases"
for i in $(seq 0 $(($releases_count - 1))); do
release=$(echo "$RELEASES_JSON" | jq -c ".[$i]")
tag_name=$(echo "$release" | jq -r '.tag_name')
release_id=$(echo "$release" | jq -r '.id')
[ -n "$SPECIFIC_TAG" ] && [ "$tag_name" != "$SPECIFIC_TAG" ] && {
echo "跳过 $tag_name不是指定标签"
continue
}
echo "处理版本: $tag_name (ID: $release_id)"
assets_json=$(curl -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/releases/$release_id/assets")
assets_count=$(echo "$assets_json" | jq '. | length')
echo "找到 $assets_count 个资源文件"
for j in $(seq 0 $(($assets_count - 1))); do
asset=$(echo "$assets_json" | jq -c ".[$j]")
asset_name=$(echo "$asset" | jq -r '.name')
asset_url=$(echo "$asset" | jq -r '.url')
asset_size=$(echo "$asset" | jq -r '.size')
echo "下载资源: $asset_name (大小: $asset_size bytes)"
safe_filename="./temp_download_$(date +%s)_$j"
# 下载
curl -sL -o "$safe_filename" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/octet-stream" \
"$asset_url" || {
echo "❌ 下载失败: $asset_name"
continue
}
# 大小校验
actual_size=$(wc -c < "$safe_filename")
[ "$actual_size" -ne "$asset_size" ] && {
echo "❌ 文件大小不匹配: $asset_name"
rm -f "$safe_filename"
continue
}
# 组装远程路径URL 编码)
remote_path="$ALIST_DIR/$tag_name/$asset_name"
file_path_encoded=$(printf %s "$remote_path" | jq -sRr @uri)
echo "上传到 Alist: $remote_path"
# 调用 /api/fs/put 上传(带 As-Task 异步)
response=$(
curl -s -X PUT "$ALIST_URL/api/fs/put" \
-H "Authorization: $token" \
-H "File-Path: $file_path_encoded" \
-H "Content-Type: application/octet-stream" \
-H "Content-Length: $actual_size" \
-H "As-Task: true" \
--data-binary @"$safe_filename"
)
echo "==== 上传接口原始返回 ===="
echo "$response"
code=$(echo "$response" | jq -r '.code // empty')
if [ "$code" = "200" ]; then
echo "✅ Alist 上传任务创建成功: $asset_name"
else
echo "❌ Alist 上传失败: $asset_name"
fi
rm -f "$safe_filename"
echo "----------------------------------------"
done
echo "版本 $tag_name 处理完成"
echo "========================================"
done
# ========== 5. 退出登录 ==========
echo "退出登录 ..."
curl -s -X POST "$ALIST_URL/api/auth/logout" \
-H "Authorization: $token" > /dev/null || true
echo "🎉 Alist 同步完成"
- name: Summary
run: |
echo "同步任务已完成!"
echo "请检查 Alist 中的文件是否正确上传。"
echo "如果遇到问题,请检查以下配置:"
echo "1. ALIST_URL - Alist 服务器地址"
echo "2. ALIST_USERNAME - Alist 登录账号"
echo "3. ALIST_PASSWORD - Alist 登录密码"
echo "4. GITHUB_TOKEN - GitHub 访问令牌"

160
.github/workflows/uploadpan.yml vendored Normal file
View File

@@ -0,0 +1,160 @@
name: Sync Existing Releases to WebDAV
on:
workflow_dispatch:
inputs:
tag_name:
description: '要同步的特定标签(如 v1.0.0),留空则同步所有版本'
required: false
default: ''
permissions:
contents: read
jobs:
sync-releases-to-webdav:
runs-on: ubuntu-latest
steps:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y curl
- name: Get all releases
id: get-releases
run: |
response=$(curl -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/releases")
echo "releases_json=$(echo "$response" | jq -c '.')" >> $GITHUB_OUTPUT
- name: Sync releases to WebDAV
run: |
# 读取输入参数
SPECIFIC_TAG="${{ github.event.inputs.tag_name }}"
RELEASES_JSON='${{ steps.get-releases.outputs.releases_json }}'
echo "开始同步 releases..."
echo "特定标签: ${SPECIFIC_TAG:-所有版本}"
echo "WebDAV 根路径: ${{ secrets.WEBDAV_BASE_URL }}/yd/ceru"
# 处理每个 release
releases_count=$(echo "$RELEASES_JSON" | jq '. | length')
echo "找到 $releases_count 个 releases"
for i in $(seq 0 $(($releases_count - 1))); do
release=$(echo "$RELEASES_JSON" | jq -c ".[$i]")
tag_name=$(echo "$release" | jq -r '.tag_name')
release_id=$(echo "$release" | jq -r '.id')
if [ -n "$SPECIFIC_TAG" ] && [ "$tag_name" != "$SPECIFIC_TAG" ]; then
echo "跳过 $tag_name不是指定的标签 $SPECIFIC_TAG"
continue
fi
echo "正在处理版本: $tag_name (ID: $release_id)"
# 获取该release的所有资源文件
assets_json=$(curl -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/releases/$release_id/assets")
assets_count=$(echo "$assets_json" | jq '. | length')
echo "找到 $assets_count 个资源文件"
# 处理每个asset
for j in $(seq 0 $(($assets_count - 1))); do
asset=$(echo "$assets_json" | jq -c ".[$j]")
asset_name=$(echo "$asset" | jq -r '.name')
asset_url=$(echo "$asset" | jq -r '.url')
asset_size=$(echo "$asset" | jq -r '.size')
echo "下载资源: $asset_name (大小: $asset_size bytes)"
# 下载资源文件
safe_filename="./temp_download"
if ! curl -sL -o "$safe_filename" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/octet-stream" \
"$asset_url"; then
echo "❌ 下载失败: $asset_name"
continue
fi
if [ -f "$safe_filename" ]; then
actual_size=$(wc -c < "$safe_filename")
if [ "$actual_size" -ne "$asset_size" ]; then
echo "❌ 文件大小不匹配: $asset_name (期望: $asset_size, 实际: $actual_size)"
rm -f "$safe_filename"
continue
fi
echo "上传到 WebDAV: $asset_name"
# 构建远程路径
remote_path="/yd/ceru/$tag_name/$asset_name"
full_url="${{ secrets.WEBDAV_BASE_URL }}$remote_path"
echo "完整路径: $full_url"
# 使用 WebDAV PUT 方法上传文件
if curl -s -f -X PUT \
-u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
-T "$safe_filename" \
"$full_url"; then
echo "✅ WebDAV 上传成功: $asset_name"
# 验证文件是否存在
echo "验证文件是否存在..."
sleep 2
if curl -s -f -u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
-X PROPFIND \
-H "Depth: 0" \
"$full_url" > /dev/null 2>&1; then
echo "✅ 文件确认存在: $asset_name"
else
echo "⚠️ 文件验证失败,但上传可能成功"
fi
else
echo "❌ WebDAV 上传失败: $asset_name"
echo "尝试创建目录后重新上传..."
# 尝试先创建目录
dir_path="/yd/ceru/$tag_name"
dir_url="${{ secrets.WEBDAV_BASE_URL }}$dir_path"
if curl -s -f -X MKCOL \
-u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
"$dir_url"; then
echo "✅ 目录创建成功: $dir_path"
# 重新尝试上传文件
if curl -s -f -X PUT \
-u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
-T "$safe_filename" \
"$full_url"; then
echo "✅ 重新上传成功: $asset_name"
else
echo "❌ 重新上传失败: $asset_name"
fi
else
echo "❌ 目录创建失败: $dir_path"
fi
fi
# 安全删除临时文件
rm -f "$safe_filename"
echo "----------------------------------------"
else
echo "❌ 文件不存在: $safe_filename"
fi
done
done
echo "🎉 WebDAV 同步完成"

View File

@@ -1,14 +1,12 @@
# Ceru Music澜音
一个跨平台的音乐播放器应用,支持基于合规插件获取公开音乐信息与播放功能。
## 项目简介
Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工具,**仅提供插件运行框架与播放功能**,不直接存储、提供任何音乐源文件。用户需通过自行选择、安装合规插件获取音乐相关数据,项目旨在为开发者提供桌面应用技术实践与学习案例,为用户提供合规的音乐播放工具框架。
<img src="assets/image-20250827175023917.png" alt="image-20250827175023917" style="zoom: 33%;" /><img src="assets/image-20250827175109430.png" alt="image-20250827175109430" style="zoom:33%;" />
<img src="assets/image-20250827175023917.png" alt="image-20250827175023917" style="zoom: 33%;" /><img src="assets/image-20250827175109430.png" alt="image-20250827175109430" style="zoom:33%;" />
## 技术栈
@@ -22,8 +20,6 @@ Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工
## 主要功能
- 提供插件加载与管理功能,支持通过合规插件获取公开音乐信息
- 支持通过插件获取歌词、专辑封面等公开元数据
- 支持虚拟滚动列表,优化大量数据渲染性能
@@ -36,16 +32,12 @@ Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工
### 推荐开发环境
- **IDE**: VS Code 或 WebStorm
- **Node.js 版本**: 22 及以上
- **包管理器**: **yarn**
### 项目设置
1. 安装依赖:
```bash
@@ -66,8 +58,6 @@ Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工
### 平台构建指令
- Windows
```bash
@@ -86,8 +76,6 @@ Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工
yarn build:linux
```
> 提示:构建后的应用仅包含播放器框架,需用户自行配置合规插件方可获取音乐数据。
## 文档与资源

View File

@@ -3,9 +3,10 @@ import { defineConfig } from 'vitepress'
// https://vitepress.dev/reference/site-config
export default defineConfig({
lang: 'zh-CN',
title: "Ceru Music",
base: process.env.BASE_URL ?? '/CeruMusic/',
description: "Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工具,一个跨平台的音乐播放器应用,支持基于合规插件获取公开音乐信息与播放功能。",
title: 'Ceru Music',
base: '/',
description:
'Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工具,一个跨平台的音乐播放器应用,支持基于合规插件获取公开音乐信息与播放功能。',
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
logo: '/logo.svg',
@@ -37,7 +38,6 @@ export default defineConfig({
{ icon: 'qq', link: 'https://qm.qq.com/q/IDpQnbGd06' },
{ icon: 'beatsbydre', link: 'https://shiqianjiang.cn' },
{ icon: 'bilibili', link: 'https://space.bilibili.com/696709986' }
],
footer: {
message: 'Released under the Apache License 2.0 License.',
@@ -51,6 +51,7 @@ export default defineConfig({
}
},
lastUpdated: true,
head: [['link', { rel: 'icon', href: (process.env.BASE_URL ?? '/CeruMusic/') + 'logo.svg' }]]
head: [['link', { rel: 'icon', href: '/logo.svg' }]]
})
console.log(process.env.BASE_URL_DOCS)
// Smooth scrolling functions

View File

@@ -3,7 +3,7 @@
}
::view-transition-new(*) {
animation: globalDark .5s ease-in;
animation: globalDark 0.5s ease-in;
}
@keyframes globalDark {

View File

@@ -1,7 +1,10 @@
import { nextTick, provide } from 'vue'
// 判断是否能使用 startViewTransition
const enableTransitions = () => {
return 'startViewTransition' in document && window.matchMedia('(prefers-reduced-motion: no-preference)').matches
return (
'startViewTransition' in document &&
window.matchMedia('(prefers-reduced-motion: no-preference)').matches
)
}
// 切换动画
export const toggleDark = (isDark: any) => {

View File

@@ -1,7 +1,7 @@
import DefaultTheme from 'vitepress/theme'
import './style.css'
import './dark.css'
import MyLayout from './MyLayout.vue';
import MyLayout from './MyLayout.vue'
// history.scrollRestoration = 'manual'
export default {
@@ -11,6 +11,3 @@ export default {
// ...
}
}

142
docs/alist-config.md Normal file
View File

@@ -0,0 +1,142 @@
# Alist 下载配置说明
## 概述
项目已从 GitHub 下载方式切换到 Alist API 下载方式,包括:
- 桌面应用的自动更新功能 (`src/main/autoUpdate.ts`)
- 官方网站的下载功能 (`website/script.js`)
## 配置步骤
### 1. 修改 Alist 域名
#### 桌面应用配置
`src/main/autoUpdate.ts` 文件中Alist 域名已配置为:
```typescript
const ALIST_BASE_URL = 'http://47.96.72.224:5244'
```
#### 网站配置
`website/script.js` 文件中Alist 域名已配置为:
```javascript
const ALIST_BASE_URL = 'http://47.96.72.224:5244'
```
如需修改域名,请同时更新这两个文件中的 `ALIST_BASE_URL` 配置。
### 2. 认证信息
已配置的认证信息:
- 用户名: `ceruupdata`
- 密码: `123456`
### 3. 文件路径格式
文件在 Alist 中的路径格式为:`/{version}/{文件名}`
例如:
- 版本 `v1.0.0` 的安装包 `app-setup.exe` 路径为:`/v1.0.0/app-setup.exe`
## 工作原理
### 桌面应用自动更新
1. **认证**: 使用配置的用户名和密码向 Alist API 获取认证 token
2. **获取文件信息**: 使用 token 调用 `/api/fs/get` 接口获取文件信息和签名
3. **下载**: 使用带签名的直接下载链接下载文件
4. **备用方案**: 如果 Alist 失败,自动回退到原始 URL 下载
### 网站下载功能
1. **获取版本列表**: 调用 `/api/fs/list` 获取根目录下的版本文件夹
2. **获取文件列表**: 获取最新版本文件夹中的所有文件
3. **平台匹配**: 根据用户平台自动匹配对应的安装包文件
4. **生成下载链接**: 获取文件的直接下载链接
5. **备用方案**: 如果 Alist 失败,自动回退到 GitHub API
## API 接口
### 认证接口
```
POST /api/auth/login
{
"username": "ceruupdata",
"password": "123456"
}
```
### 获取文件信息接口
```
POST /api/fs/get
Headers: Authorization: {token} # 注意:直接使用 token不需要 "Bearer " 前缀
{
"path": "/{version}/{fileName}"
}
```
### 获取文件列表接口
```
POST /api/fs/list
Headers: Authorization: {token} # 注意:直接使用 token不需要 "Bearer " 前缀
{
"path": "/",
"password": "",
"page": 1,
"per_page": 100,
"refresh": false
}
```
### 下载链接格式
```
{ALIST_BASE_URL}/d/{filePath}?sign={sign}
```
## 测试方法
项目包含了一个测试脚本来验证 Alist 连接:
```bash
node scripts/test-alist.js
```
该脚本会:
1. 测试服务器连通性
2. 测试用户认证
3. 测试文件列表获取
4. 测试文件信息获取
## 备用机制
两个组件都实现了备用机制:
### 桌面应用
- 主要:使用 Alist API 下载
- 备用:如果 Alist 失败,使用原始 URL 下载
### 网站
- 主要:使用 Alist API 获取版本和文件信息
- 备用:如果 Alist 失败,回退到 GitHub API
- 最终备用:跳转到 GitHub releases 页面
## 注意事项
1. 确保 Alist 服务器可以正常访问
2. 确保配置的用户名和密码有权限访问相应的文件路径
3. 文件必须按照指定的路径格式存放在 Alist 中
4. 网站会自动检测用户操作系统并推荐对应的下载版本
5. 所有下载都会显示文件大小信息

View File

@@ -0,0 +1,99 @@
# Alist 迁移完成总结
## 修改概述
项目已成功从 GitHub 下载方式迁移到 Alist API 下载方式。
## 修改的文件
### 1. 桌面应用自动更新 (`src/main/autoUpdate.ts`)
- ✅ 添加了 Alist API 配置
- ✅ 实现了 Alist 认证功能
- ✅ 实现了 Alist 文件下载功能
- ✅ 添加了备用机制Alist 失败时回退到原始 URL
- ✅ 修复了 Authorization 头格式(使用直接 token 而非 Bearer 格式)
### 2. 官方网站下载功能 (`website/script.js`)
- ✅ 添加了 Alist API 配置
- ✅ 实现了 Alist 认证功能
- ✅ 实现了版本列表获取功能
- ✅ 实现了文件列表获取功能
- ✅ 实现了平台文件匹配功能
- ✅ 添加了多层备用机制Alist → GitHub API → GitHub 页面)
- ✅ 修复了 Authorization 头格式
### 3. 配置文档
- ✅ 创建了详细的配置说明 (`docs/alist-config.md`)
- ✅ 创建了迁移总结文档 (`docs/alist-migration-summary.md`)
### 4. 测试脚本
- ✅ 创建了 Alist 连接测试脚本 (`scripts/test-alist.js`)
- ✅ 创建了认证格式测试脚本 (`scripts/auth-test.js`)
## 配置信息
### Alist 服务器配置
- **服务器地址**: `http://47.96.72.224:5244`
- **用户名**: `ceruupdate`
- **密码**: `123456`
- **文件路径格式**: `/{version}/{文件名}`
### Authorization 头格式
经过测试确认,正确的格式是:
```
Authorization: {token}
```
**注意**: 不需要 "Bearer " 前缀
## 功能特性
### 桌面应用
1. **智能下载**: 优先使用 Alist API失败时自动回退
2. **进度显示**: 支持下载进度显示和节流
3. **错误处理**: 完善的错误处理和日志记录
### 网站
1. **自动检测**: 自动检测用户操作系统并推荐对应版本
2. **版本信息**: 自动获取最新版本信息和文件大小
3. **多层备用**: Alist → GitHub API → GitHub 页面的三层备用机制
4. **用户体验**: 加载状态、成功通知、错误提示
## 测试结果
**Alist 连接测试**: 通过
**认证测试**: 通过
**文件列表获取**: 通过
**Authorization 头格式**: 已修复并验证
## 可用文件
测试显示 Alist 服务器当前包含以下文件:
- `v1.2.1/` (版本目录)
- `1111`
- `L3YxLjIuMS8tMS4yLjEtYXJtNjQtbWFjLnppcA==`
- `file2.msi`
- `file.msi`
## 后续维护
1. **添加新版本**: 在 Alist 中创建新的版本目录(如 `v1.2.2/`
2. **上传文件**: 将对应平台的安装包上传到版本目录中
3. **文件命名**: 确保文件名包含平台标识(如 `windows`, `mac`, `linux` 等)
## 备注
- 所有修改都保持了向后兼容性
- 实现了完善的错误处理和备用机制
- 用户体验不会因为迁移而受到影响
- 可以随时回退到 GitHub 下载方式

444
docs/songlist-api.md Normal file
View File

@@ -0,0 +1,444 @@
# 歌单管理 API 文档
本文档介绍了 CeruMusic 中歌单管理功能的使用方法,包括后端服务类和前端 API 接口。
## 概述
歌单管理系统提供了完整的歌单和歌曲管理功能,包括:
- 📁 **歌单管理**:创建、删除、编辑、搜索歌单
- 🎵 **歌曲管理**:添加、移除、搜索歌单中的歌曲
- 📊 **统计分析**:获取歌单和歌曲的统计信息
- 🔧 **数据维护**:验证和修复歌单数据完整性
-**批量操作**:支持批量删除和批量移除操作
## 架构设计
```
前端 (Renderer Process)
├── src/renderer/src/api/songList.ts # 前端 API 封装
├── src/renderer/src/examples/songListUsage.ts # 使用示例
└── src/types/songList.ts # TypeScript 类型定义
主进程 (Main Process)
├── src/main/events/songList.ts # IPC 事件处理
├── src/main/services/songList/ManageSongList.ts # 歌单管理服务
└── src/main/services/songList/PlayListSongs.ts # 歌曲管理基类
```
## 快速开始
### 1. 前端使用
```typescript
import songListAPI from '@/api/songList'
// 创建歌单
const result = await songListAPI.create('我的收藏', '我最喜欢的歌曲')
if (result.success) {
console.log('歌单创建成功ID:', result.data?.id)
}
// 获取所有歌单
const playlists = await songListAPI.getAll()
if (playlists.success) {
console.log('歌单列表:', playlists.data)
}
// 添加歌曲到歌单
const songs = [
/* 歌曲数据 */
]
await songListAPI.addSongs(playlistId, songs)
```
### 2. 类型安全
所有 API 都提供了完整的 TypeScript 类型支持:
```typescript
import type { IPCResponse, SongListStatistics } from '@/types/songList'
const stats: IPCResponse<SongListStatistics> = await songListAPI.getStatistics()
```
## API 参考
### 歌单管理
#### `create(name, description?, source?)`
创建新歌单
```typescript
const result = await songListAPI.create('我的收藏', '描述', 'local')
// 返回: { success: boolean, data?: { id: string }, error?: string }
```
#### `getAll()`
获取所有歌单
```typescript
const result = await songListAPI.getAll()
// 返回: { success: boolean, data?: SongList[], error?: string }
```
#### `getById(hashId)`
根据ID获取歌单
```typescript
const result = await songListAPI.getById('playlist-id')
// 返回: { success: boolean, data?: SongList | null, error?: string }
```
#### `delete(hashId)`
删除歌单
```typescript
const result = await songListAPI.delete('playlist-id')
// 返回: { success: boolean, error?: string }
```
#### `batchDelete(hashIds)`
批量删除歌单
```typescript
const result = await songListAPI.batchDelete(['id1', 'id2'])
// 返回: { success: boolean, data?: { success: string[], failed: string[] } }
```
#### `edit(hashId, updates)`
编辑歌单信息
```typescript
const result = await songListAPI.edit('playlist-id', {
name: '新名称',
description: '新描述'
})
```
#### `search(keyword, source?)`
搜索歌单
```typescript
const result = await songListAPI.search('关键词', 'local')
// 返回: { success: boolean, data?: SongList[], error?: string }
```
### 歌曲管理
#### `addSongs(hashId, songs)`
添加歌曲到歌单
```typescript
const songs: Songs[] = [
/* 歌曲数据 */
]
const result = await songListAPI.addSongs('playlist-id', songs)
```
#### `removeSong(hashId, songmid)`
移除单首歌曲
```typescript
const result = await songListAPI.removeSong('playlist-id', 'song-id')
// 返回: { success: boolean, data?: boolean, error?: string }
```
#### `removeSongs(hashId, songmids)`
批量移除歌曲
```typescript
const result = await songListAPI.removeSongs('playlist-id', ['song1', 'song2'])
// 返回: { success: boolean, data?: { removed: number, notFound: number } }
```
#### `getSongs(hashId)`
获取歌单中的歌曲
```typescript
const result = await songListAPI.getSongs('playlist-id')
// 返回: { success: boolean, data?: readonly Songs[], error?: string }
```
#### `searchSongs(hashId, keyword)`
搜索歌单中的歌曲
```typescript
const result = await songListAPI.searchSongs('playlist-id', '关键词')
// 返回: { success: boolean, data?: Songs[], error?: string }
```
### 统计信息
#### `getStatistics()`
获取歌单统计信息
```typescript
const result = await songListAPI.getStatistics()
// 返回: {
// success: boolean,
// data?: {
// total: number,
// bySource: Record<string, number>,
// lastUpdated: string
// }
// }
```
#### `getSongStatistics(hashId)`
获取歌单歌曲统计信息
```typescript
const result = await songListAPI.getSongStatistics('playlist-id')
// 返回: {
// success: boolean,
// data?: {
// total: number,
// bySinger: Record<string, number>,
// byAlbum: Record<string, number>,
// lastModified: string
// }
// }
```
### 数据维护
#### `validateIntegrity(hashId)`
验证歌单数据完整性
```typescript
const result = await songListAPI.validateIntegrity('playlist-id')
// 返回: { success: boolean, data?: { isValid: boolean, issues: string[] } }
```
#### `repairData(hashId)`
修复歌单数据
```typescript
const result = await songListAPI.repairData('playlist-id')
// 返回: { success: boolean, data?: { fixed: boolean, changes: string[] } }
```
### 便捷方法
#### `getPlaylistDetail(hashId)`
获取歌单详细信息(包含歌曲列表)
```typescript
const result = await songListAPI.getPlaylistDetail('playlist-id')
// 返回: {
// playlist: SongList | null,
// songs: readonly Songs[],
// success: boolean,
// error?: string
// }
```
#### `checkAndRepair(hashId)`
检查并修复歌单数据
```typescript
const result = await songListAPI.checkAndRepair('playlist-id')
// 返回: {
// needsRepair: boolean,
// repairResult?: RepairResult,
// success: boolean,
// error?: string
// }
```
## 错误处理
所有 API 都返回统一的响应格式:
```typescript
interface IPCResponse<T = any> {
success: boolean // 操作是否成功
data?: T // 返回的数据
error?: string // 错误信息
message?: string // 附加消息
code?: string // 错误码
}
```
### 错误码说明
| 错误码 | 说明 |
| -------------------- | ------------ |
| `INVALID_HASH_ID` | 无效的歌单ID |
| `PLAYLIST_NOT_FOUND` | 歌单不存在 |
| `EMPTY_NAME` | 歌单名称为空 |
| `CREATE_FAILED` | 创建失败 |
| `DELETE_FAILED` | 删除失败 |
| `EDIT_FAILED` | 编辑失败 |
| `READ_FAILED` | 读取失败 |
| `WRITE_FAILED` | 写入失败 |
## 使用示例
### 完整的歌单管理流程
```typescript
import songListAPI from '@/api/songList'
async function managePlaylist() {
try {
// 1. 创建歌单
const createResult = await songListAPI.create('我的收藏', '我最喜欢的歌曲')
if (!createResult.success) {
throw new Error(createResult.error)
}
const playlistId = createResult.data!.id
// 2. 添加歌曲
const songs = [
{
songmid: 'song1',
name: '歌曲1',
singer: '歌手1',
albumName: '专辑1',
albumId: 'album1',
duration: 240,
source: 'local'
}
]
await songListAPI.addSongs(playlistId, songs)
// 3. 获取歌单详情
const detail = await songListAPI.getPlaylistDetail(playlistId)
console.log('歌单信息:', detail.playlist)
console.log('歌曲列表:', detail.songs)
// 4. 搜索歌曲
const searchResult = await songListAPI.searchSongs(playlistId, '歌曲')
console.log('搜索结果:', searchResult.data)
// 5. 获取统计信息
const stats = await songListAPI.getSongStatistics(playlistId)
console.log('统计信息:', stats.data)
} catch (error) {
console.error('操作失败:', error)
}
}
```
### React 组件中的使用
```typescript
import React, { useState, useEffect } from 'react'
import songListAPI from '@/api/songList'
import type { SongList } from '@common/types/songList'
const PlaylistManager: React.FC = () => {
const [playlists, setPlaylists] = useState<SongList[]>([])
const [loading, setLoading] = useState(false)
// 加载歌单列表
const loadPlaylists = async () => {
setLoading(true)
try {
const result = await songListAPI.getAll()
if (result.success) {
setPlaylists(result.data || [])
}
} catch (error) {
console.error('加载歌单失败:', error)
} finally {
setLoading(false)
}
}
// 创建新歌单
const createPlaylist = async (name: string) => {
const result = await songListAPI.create(name)
if (result.success) {
await loadPlaylists() // 重新加载列表
}
}
// 删除歌单
const deletePlaylist = async (id: string) => {
const result = await songListAPI.safeDelete(id, async () => {
return confirm('确定要删除这个歌单吗?')
})
if (result.success) {
await loadPlaylists() // 重新加载列表
}
}
useEffect(() => {
loadPlaylists()
}, [])
return (
<div>
{loading ? (
<div>...</div>
) : (
<div>
{playlists.map(playlist => (
<div key={playlist.id}>
<h3>{playlist.name}</h3>
<p>{playlist.description}</p>
<button onClick={() => deletePlaylist(playlist.id)}>
</button>
</div>
))}
</div>
)}
</div>
)
}
```
## 性能优化建议
1. **批量操作**:使用 `batchDelete``removeSongs` 进行批量操作
2. **数据缓存**:在前端适当缓存歌单列表,避免频繁请求
3. **懒加载**:歌曲列表可以按需加载,不必一次性加载所有数据
4. **错误恢复**:使用 `checkAndRepair` 定期检查数据完整性
## 注意事项
1. 所有 API 都是异步的,需要使用 `await``.then()`
2. 歌单 ID (`hashId`) 是唯一标识符,不要与数组索引混淆
3. 歌曲 ID (`songmid`) 可能是字符串或数字类型
4. 删除操作是不可逆的,建议使用 `safeDelete` 方法
5. 大量数据操作时注意性能影响
## 更新日志
### v1.0.0 (2024-01-10)
- ✨ 初始版本发布
- ✨ 完整的歌单管理功能
- ✨ 批量操作支持
- ✨ 数据完整性检查
- ✨ TypeScript 类型支持
- ✨ 详细的使用文档和示例
---
如有问题或建议,请提交 Issue 或 Pull Request。

150
docs/webdav-sync-setup.md Normal file
View File

@@ -0,0 +1,150 @@
# WebDAV 同步配置指南
本项目包含两个 GitHub Actions 工作流,用于自动将 GitHub Releases 同步到 alistWebDAV 服务器)。
## 工作流说明
### 1. 手动同步工作流 (`sync-releases-to-webdav.yml`)
- **触发方式**: 手动触发 (workflow_dispatch)
- **功能**: 同步现有的所有版本或指定版本到 WebDAV
- **参数**:
- `tag_name`: 可选,指定要同步的版本标签(如 v1.0.0),留空则同步所有版本
### 2. 自动同步工作流 (集成在 `main.yml` 中)
- **触发方式**: 在 AutoBuild 完成后自动触发
- **功能**: 自动将新构建的版本同步到 WebDAV
- **参数**: 无需手动设置,自动获取发布信息
### 3. 独立自动同步工作流 (`auto-sync-release.yml`)
- **触发方式**: 当新版本发布时自动触发 (on release published)
- **功能**: 备用的自动同步机制
- **参数**: 无需手动设置,自动获取发布信息
## 配置要求
在 GitHub 仓库的 Settings > Secrets and variables > Actions 中添加以下密钥:
### 必需的 Secrets
1. **WEBDAV_BASE_URL**
- 描述: WebDAV 服务器的基础 URL
- 示例: `https://your-alist-domain.com/dav`
- 注意: 不要在末尾添加斜杠
2. **WEBDAV_USERNAME**
- 描述: WebDAV 服务器的用户名
- 示例: `admin`
3. **WEBDAV_PASSWORD**
- 描述: WebDAV 服务器的密码
- 示例: `your-password`
4. **GITHUB_TOKEN**
- 描述: GitHub 访问令牌(通常自动提供)
- 注意: 如果默认的 `GITHUB_TOKEN` 权限不足,可能需要创建个人访问令牌
## 使用方法
### 手动同步现有版本
1. 进入 GitHub 仓库的 Actions 页面
2. 选择 "Sync Existing Releases to WebDAV" 工作流
3. 点击 "Run workflow"
4. 可选择指定版本标签或留空同步所有版本
5. 点击 "Run workflow" 开始执行
### 自动同步新版本
现在有两种自动同步方式:
1. **集成同步** (推荐): 在主构建工作流 (`main.yml`) 中集成了 WebDAV 同步,当您推送 `v*` 标签时,会自动执行:
- 构建应用 → 创建 Release → 同步到 WebDAV
2. **独立同步**: 当您手动发布 Release 时,`auto-sync-release.yml` 工作流会自动触发
推荐使用集成同步方式,因为它确保了构建和同步的一致性。
## 文件结构
同步后的文件将按以下结构存储在 alist 中:
```
/yd/ceru/
├── v1.0.0/
│ ├── app-setup.exe
│ ├── app.dmg
│ └── app.AppImage
├── v1.1.0/
│ ├── app-setup.exe
│ ├── app.dmg
│ └── app.AppImage
└── ...
```
## 故障排除
### 常见问题
1. **上传失败**
- 检查 WebDAV 服务器是否正常运行
- 验证用户名和密码是否正确
- 确认 WebDAV URL 格式正确
2. **权限错误**
- 确保 WebDAV 用户有写入权限
- 检查目标目录是否存在且可写
3. **文件大小不匹配**
- 网络问题导致下载不完整
- GitHub API 限制或临时故障
4. **目录创建失败**
- WebDAV 服务器不支持 MKCOL 方法
- 权限不足或路径错误
### 调试步骤
1. 查看 Actions 运行日志
2. 检查 WebDAV 服务器日志
3. 验证所有 Secrets 配置正确
4. 测试 WebDAV 连接是否正常
## 安全注意事项
1. **密钥管理**
- 不要在代码中硬编码密码
- 定期更换 WebDAV 密码
- 使用强密码
2. **权限控制**
- 为 WebDAV 用户设置最小必要权限
- 考虑使用专用的同步账户
3. **网络安全**
- 建议使用 HTTPS 连接
- 考虑 IP 白名单限制
## 自定义配置
如需修改同步路径或其他配置,请编辑对应的工作流文件:
- 修改存储路径: 更改 `remote_path` 变量
- 调整重试逻辑: 修改错误处理部分
- 添加通知: 集成 Slack、邮件等通知服务
## 支持的文件类型
工作流支持同步所有类型的 Release 资源文件,包括但不限于:
- 可执行文件 (.exe, .dmg, .AppImage)
- 压缩包 (.zip, .tar.gz, .7z)
- 安装包 (.msi, .deb, .rpm)
- 其他二进制文件
## 版本兼容性
- GitHub Actions: 支持最新版本
- alist: 支持 WebDAV 协议的版本
- 操作系统: Ubuntu Latest (工作流运行环境)

View File

@@ -19,6 +19,7 @@ win:
- target: nsis
arch:
- x64
- ia32
# 简化版本信息设置避免rcedit错误
fileAssociations:
- ext: cerumusic
@@ -30,7 +31,7 @@ win:
# 或者使用证书存储
# certificateSubjectName: "Your Company Name"
nsis:
artifactName: ${name}-${version}-setup.${ext}
artifactName: ${name}-${version}-${arch}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always

View File

@@ -1,6 +1,6 @@
{
"name": "ceru-music",
"version": "1.1.9",
"version": "1.2.7",
"description": "一款简洁优雅的音乐播放器",
"main": "./out/main/index.js",
"author": "sqj,wldss,star",
@@ -18,7 +18,8 @@
"onlybuild": "electron-vite build && electron-builder --win --x64",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "yarn run build && electron-builder --dir",
"build:win": "yarn run build && electron-builder --win --x64 --config --publish never",
"build:win": "yarn run build && electron-builder --win --config --publish never",
"build:win32": "yarn run build && electron-builder --win --ia32 --config --publish never",
"build:mac": "yarn run build && electron-builder --mac --config --publish never",
"build:linux": "yarn run build && electron-builder --linux --config --publish never",
"build:deps": "electron-builder install-app-deps && yarn run build && electron-builder --win --x64 --config",
@@ -94,7 +95,8 @@
"vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-vue-devtools": "^8.0.0",
"vite-plugin-wasm": "^3.5.0",
"vue": "^3.5.17",
"vitepress": "^2.0.0-alpha.12",
"vue": "^3.5.21",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.0.3"
}

BIN
resources/default-cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 KiB

66
scripts/auth-test.js Normal file
View File

@@ -0,0 +1,66 @@
const axios = require('axios')
const ALIST_BASE_URL = 'http://47.96.72.224:5244'
const ALIST_USERNAME = 'ceruupdate'
const ALIST_PASSWORD = '123456'
async function test() {
// 认证
const auth = await axios.post(`${ALIST_BASE_URL}/api/auth/login`, {
username: ALIST_USERNAME,
password: ALIST_PASSWORD
})
const token = auth.data.data.token
console.log('Token received')
// 测试直接 token 格式
try {
const list = await axios.post(
`${ALIST_BASE_URL}/api/fs/list`,
{
path: '/',
password: '',
page: 1,
per_page: 30,
refresh: false
},
{
headers: { Authorization: token }
}
)
console.log('Direct token works:', list.data.code === 200)
if (list.data.code === 200) {
console.log(
'Files:',
list.data.data.content.map((f) => f.name)
)
}
} catch (e) {
console.log('Direct token failed')
}
// 测试 Bearer 格式
try {
const list2 = await axios.post(
`${ALIST_BASE_URL}/api/fs/list`,
{
path: '/',
password: '',
page: 1,
per_page: 30,
refresh: false
},
{
headers: { Authorization: `Bearer ${token}` }
}
)
console.log('Bearer format works:', list2.data.code === 200)
} catch (e) {
console.log('Bearer format failed')
}
}
test().catch(console.error)

148
scripts/test-alist.js Normal file
View File

@@ -0,0 +1,148 @@
const axios = require('axios')
// Alist API 配置
const ALIST_BASE_URL = 'http://47.96.72.224:5244'
const ALIST_USERNAME = 'ceruupdate'
const ALIST_PASSWORD = '123456'
async function testAlistConnection() {
console.log('Testing Alist connection...')
try {
// 0. 首先测试服务器是否可访问
console.log('0. Testing server accessibility...')
const pingResponse = await axios.get(`${ALIST_BASE_URL}/ping`, {
timeout: 5000
})
console.log('Server ping successful:', pingResponse.status)
// 1. 测试认证
console.log('1. Testing authentication...')
console.log(`Trying to authenticate with username: ${ALIST_USERNAME}`)
const authResponse = await axios.post(
`${ALIST_BASE_URL}/api/auth/login`,
{
username: ALIST_USERNAME,
password: ALIST_PASSWORD
},
{
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
}
)
console.log('Auth response:', authResponse.data)
if (authResponse.data.code !== 200) {
// 尝试获取公共访问权限
console.log('Authentication failed, trying public access...')
// 尝试不使用认证直接访问文件列表
const publicListResponse = await axios.post(
`${ALIST_BASE_URL}/api/fs/list`,
{
path: '/',
password: '',
page: 1,
per_page: 30,
refresh: false
},
{
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
}
)
console.log('Public access response:', publicListResponse.data)
if (publicListResponse.data.code === 200) {
console.log('✓ Public access successful')
return // 如果公共访问成功,就不需要认证
}
throw new Error(`Authentication failed: ${authResponse.data.message}`)
}
const token = authResponse.data.data.token
console.log('✓ Authentication successful')
// 2. 测试文件列表
console.log('2. Testing file listing...')
const listResponse = await axios.post(
`${ALIST_BASE_URL}/api/fs/list`,
{
path: '/',
password: '',
page: 1,
per_page: 30,
refresh: false
},
{
timeout: 10000,
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
)
console.log('List response:', listResponse.data)
if (listResponse.data.code === 200) {
console.log('✓ File listing successful')
console.log('Available directories/files:')
listResponse.data.data.content.forEach((item) => {
console.log(` - ${item.name} (${item.is_dir ? 'directory' : 'file'})`)
})
}
// 3. 测试获取特定文件信息(如果存在版本目录)
console.log('3. Testing file info retrieval...')
try {
const fileInfoResponse = await axios.post(
`${ALIST_BASE_URL}/api/fs/get`,
{
path: '/v1.0.0' // 测试版本目录
},
{
timeout: 10000,
headers: {
Authorization: token,
'Content-Type': 'application/json'
}
}
)
console.log('File info response:', fileInfoResponse.data)
if (fileInfoResponse.data.code === 200) {
console.log('✓ File info retrieval successful')
}
} catch (error) {
console.log(
' Version directory /v1.0.0 not found (this is expected if no updates are available)'
)
}
console.log('\n✅ Alist connection test completed successfully!')
} catch (error) {
console.error('❌ Alist connection test failed:', error.message)
if (error.response) {
console.error('Response status:', error.response.status)
console.error('Response data:', error.response.data)
} else if (error.request) {
console.error('No response received. Check if the Alist server is running and accessible.')
}
process.exit(1)
}
}
// 运行测试
testAlistConnection()

View File

@@ -0,0 +1,15 @@
export default interface PlayList {
songmid: string | number
hash?: string
singer: string
name: string
albumName: string
albumId: string | number
source: string
interval: string
img: string
lrc: null | string
types: string[]
_types: Record<string, any>
typeUrl: Record<string, any>
}

View File

@@ -0,0 +1,12 @@
import PlayList from './playList'
export type Songs = PlayList
export type SongList = {
id: string //hashId 对应歌单文件名.json
name: string // 歌单名
createTime: string
updateTime: string
description: string // 歌单描述
coverImgUrl: string //歌单封面 默认第一首歌的图片
source: 'local' | 'wy' | 'tx' | 'mg' | 'kg' | 'kw' // 来源
}

View File

@@ -1,6 +1,6 @@
// 业务工具方法
import { LX } from "../../types/global"
import { LX } from '../../types/global'
export const toNewMusicInfo = (oldMusicInfo: any): LX.Music.MusicInfo => {
const meta: Record<string, any> = {

View File

@@ -1,54 +1,160 @@
import { BrowserWindow, app, shell } from 'electron';
import axios from 'axios';
import fs from 'fs';
import path from 'node:path';
import { BrowserWindow, app, shell } from 'electron'
import axios from 'axios'
import fs from 'fs'
import path from 'node:path'
let mainWindow: BrowserWindow | null = null;
let currentUpdateInfo: UpdateInfo | null = null;
let downloadProgress = { percent: 0, transferred: 0, total: 0 };
let mainWindow: BrowserWindow | null = null
let currentUpdateInfo: UpdateInfo | null = null
let downloadProgress = { percent: 0, transferred: 0, total: 0 }
// 更新信息接口
interface UpdateInfo {
url: string;
name: string;
notes: string;
pub_date: string;
url: string
name: string
notes: string
pub_date: string
}
// 更新服务器配置
const UPDATE_SERVER = 'https://update.ceru.shiqianjiang.cn';
const UPDATE_API_URL = `${UPDATE_SERVER}/update/${process.platform}/${app.getVersion()}`;
const UPDATE_SERVER = 'https://update.ceru.shiqianjiang.cn'
const UPDATE_API_URL = `${UPDATE_SERVER}/update/${process.platform}/${app.getVersion()}`
// Alist API 配置
const ALIST_BASE_URL = 'https://alist.shiqianjiang.cn' // 请替换为实际的 alist 域名
const ALIST_USERNAME = 'ceruupdate'
const ALIST_PASSWORD = '123456'
// Alist 认证 token
let alistToken: string | null = null
// 获取 Alist 认证 token
async function getAlistToken(): Promise<string> {
if (alistToken) {
return alistToken
}
try {
console.log('Authenticating with Alist...')
const response = await axios.post(
`${ALIST_BASE_URL}/api/auth/login`,
{
username: ALIST_USERNAME,
password: ALIST_PASSWORD
},
{
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
}
)
console.log('Alist auth response:', response.data)
if (response.data.code === 200) {
alistToken = response.data.data.token
console.log('Alist authentication successful')
return alistToken! // 我们已经确认 token 存在
} else {
throw new Error(`Alist authentication failed: ${response.data.message || 'Unknown error'}`)
}
} catch (error: any) {
console.error('Alist authentication error:', error)
if (error.response) {
throw new Error(
`Failed to authenticate with Alist: HTTP ${error.response.status} - ${error.response.data?.message || error.response.statusText}`
)
} else {
throw new Error(`Failed to authenticate with Alist: ${error.message}`)
}
}
}
// 获取 Alist 文件下载链接
async function getAlistDownloadUrl(version: string, fileName: string): Promise<string> {
const token = await getAlistToken()
const filePath = `/${version}/${fileName}`
try {
console.log(`Getting file info for: ${filePath}`)
const response = await axios.post(
`${ALIST_BASE_URL}/api/fs/get`,
{
path: filePath
},
{
timeout: 10000,
headers: {
Authorization: token,
'Content-Type': 'application/json'
}
}
)
console.log('Alist file info response:', response.data)
if (response.data.code === 200) {
const fileInfo = response.data.data
// 检查文件是否存在且有下载链接
if (fileInfo && fileInfo.raw_url) {
console.log('Using raw_url for download:', fileInfo.raw_url)
return fileInfo.raw_url
} else if (fileInfo && fileInfo.sign) {
// 使用签名构建下载链接
const downloadUrl = `${ALIST_BASE_URL}/d${filePath}?sign=${fileInfo.sign}`
console.log('Using signed download URL:', downloadUrl)
return downloadUrl
} else {
// 尝试直接下载链接(无签名)
const directUrl = `${ALIST_BASE_URL}/d${filePath}`
console.log('Using direct download URL:', directUrl)
return directUrl
}
} else {
throw new Error(`Failed to get file info: ${response.data.message || 'Unknown error'}`)
}
} catch (error: any) {
console.error('Alist file info error:', error)
if (error.response) {
throw new Error(
`Failed to get download URL from Alist: HTTP ${error.response.status} - ${error.response.data?.message || error.response.statusText}`
)
} else {
throw new Error(`Failed to get download URL from Alist: ${error.message}`)
}
}
}
// 初始化自动更新器
export function initAutoUpdater(window: BrowserWindow) {
mainWindow = window;
console.log('Auto updater initialized');
mainWindow = window
console.log('Auto updater initialized')
}
// 检查更新
export async function checkForUpdates(window?: BrowserWindow) {
if (window) {
mainWindow = window;
mainWindow = window
}
try {
console.log('Checking for updates...');
mainWindow?.webContents.send('auto-updater:checking-for-update');
console.log('Checking for updates...')
mainWindow?.webContents.send('auto-updater:checking-for-update')
const updateInfo = await fetchUpdateInfo();
const updateInfo = await fetchUpdateInfo()
if (updateInfo && isNewerVersion(updateInfo.name, app.getVersion())) {
console.log('Update available:', updateInfo);
currentUpdateInfo = updateInfo;
mainWindow?.webContents.send('auto-updater:update-available', updateInfo);
console.log('Update available:', updateInfo)
currentUpdateInfo = updateInfo
mainWindow?.webContents.send('auto-updater:update-available', updateInfo)
} else {
console.log('No update available');
mainWindow?.webContents.send('auto-updater:update-not-available');
console.log('No update available')
mainWindow?.webContents.send('auto-updater:update-not-available')
}
} catch (error) {
console.error('Error checking for updates:', error);
mainWindow?.webContents.send('auto-updater:error', (error as Error).message);
console.error('Error checking for updates:', error)
mainWindow?.webContents.send('auto-updater:error', (error as Error).message)
}
}
@@ -58,26 +164,26 @@ async function fetchUpdateInfo(): Promise<UpdateInfo | null> {
const response = await axios.get(UPDATE_API_URL, {
timeout: 10000, // 10秒超时
validateStatus: (status) => status === 200 || status === 204 // 允许 200 和 204 状态码
});
})
if (response.status === 200) {
return response.data as UpdateInfo;
return response.data as UpdateInfo
} else if (response.status === 204) {
// 204 表示没有更新
return null;
return null
}
return null;
return null
} catch (error: any) {
if (error.response) {
// 服务器响应了错误状态码
throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`)
} else if (error.request) {
// 请求已发出但没有收到响应
throw new Error('Network error: No response received');
throw new Error('Network error: No response received')
} else {
// 其他错误
throw new Error(`Request failed: ${error.message}`);
throw new Error(`Request failed: ${error.message}`)
}
}
}
@@ -85,101 +191,119 @@ async function fetchUpdateInfo(): Promise<UpdateInfo | null> {
// 比较版本号
function isNewerVersion(remoteVersion: string, currentVersion: string): boolean {
const parseVersion = (version: string) => {
return version.replace(/^v/, '').split('.').map(num => parseInt(num, 10));
};
const remote = parseVersion(remoteVersion);
const current = parseVersion(currentVersion);
for (let i = 0; i < Math.max(remote.length, current.length); i++) {
const r = remote[i] || 0;
const c = current[i] || 0;
if (r > c) return true;
if (r < c) return false;
return version
.replace(/^v/, '')
.split('.')
.map((num) => parseInt(num, 10))
}
return false;
const remote = parseVersion(remoteVersion)
const current = parseVersion(currentVersion)
for (let i = 0; i < Math.max(remote.length, current.length); i++) {
const r = remote[i] || 0
const c = current[i] || 0
if (r > c) return true
if (r < c) return false
}
return false
}
// 下载更新
export async function downloadUpdate() {
if (!currentUpdateInfo) {
throw new Error('No update info available');
throw new Error('No update info available')
}
try {
console.log('Starting download:', currentUpdateInfo.url);
// 通知渲染进程开始下载
mainWindow?.webContents.send('auto-updater:download-started', currentUpdateInfo);
console.log('Starting download:', currentUpdateInfo.url)
const downloadPath = await downloadFile(currentUpdateInfo.url);
console.log('Download completed:', downloadPath);
// 通知渲染进程开始下载
mainWindow?.webContents.send('auto-updater:download-started', currentUpdateInfo)
const downloadPath = await downloadFile(currentUpdateInfo.url)
console.log('Download completed:', downloadPath)
mainWindow?.webContents.send('auto-updater:update-downloaded', {
downloadPath,
updateInfo: currentUpdateInfo
});
})
} catch (error) {
console.error('Download failed:', error);
mainWindow?.webContents.send('auto-updater:error', (error as Error).message);
console.error('Download failed:', error)
mainWindow?.webContents.send('auto-updater:error', (error as Error).message)
}
}
// 下载文件
async function downloadFile(url: string): Promise<string> {
const fileName = path.basename(url);
const downloadPath = path.join(app.getPath('temp'), fileName);
async function downloadFile(originalUrl: string): Promise<string> {
const fileName = path.basename(originalUrl)
const downloadPath = path.join(app.getPath('temp'), fileName)
// 进度节流变量
let lastProgressSent = 0;
let lastProgressTime = 0;
const PROGRESS_THROTTLE_INTERVAL = 500; // 500ms 发送一次进度
const PROGRESS_THRESHOLD = 1; // 进度变化超过1%才发送
let lastProgressSent = 0
let lastProgressTime = 0
const PROGRESS_THROTTLE_INTERVAL = 500 // 500ms 发送一次进度
const PROGRESS_THRESHOLD = 1 // 进度变化超过1%才发送
try {
let downloadUrl = originalUrl
try {
// 从当前更新信息中提取版本号
const version = currentUpdateInfo?.name || app.getVersion()
// 尝试使用 alist API 获取下载链接
downloadUrl = await getAlistDownloadUrl(version, fileName)
console.log('Using Alist download URL:', downloadUrl)
} catch (alistError) {
console.warn('Alist download failed, falling back to original URL:', alistError)
console.log('Using original download URL:', originalUrl)
downloadUrl = originalUrl
}
const response = await axios({
method: 'GET',
url: url,
url: downloadUrl,
responseType: 'stream',
timeout: 30000, // 30秒超时
onDownloadProgress: (progressEvent) => {
const { loaded, total } = progressEvent;
const percent = total ? (loaded / total) * 100 : 0;
const currentTime = Date.now();
const { loaded, total } = progressEvent
const percent = total ? (loaded / total) * 100 : 0
const currentTime = Date.now()
// 节流逻辑:只在进度变化显著或时间间隔足够时发送
const progressDiff = Math.abs(percent - lastProgressSent);
const timeDiff = currentTime - lastProgressTime;
const progressDiff = Math.abs(percent - lastProgressSent)
const timeDiff = currentTime - lastProgressTime
if (progressDiff >= PROGRESS_THRESHOLD || timeDiff >= PROGRESS_THROTTLE_INTERVAL) {
downloadProgress = {
percent,
transferred: loaded,
total: total || 0
};
}
mainWindow?.webContents.send('auto-updater:download-progress', downloadProgress);
lastProgressSent = percent;
lastProgressTime = currentTime;
mainWindow?.webContents.send('auto-updater:download-progress', downloadProgress)
lastProgressSent = percent
lastProgressTime = currentTime
}
}
});
})
// 发送初始进度
const totalSize = parseInt(response.headers['content-length'] || '0', 10);
const totalSize = parseInt(response.headers['content-length'] || '0', 10)
mainWindow?.webContents.send('auto-updater:download-progress', {
percent: 0,
transferred: 0,
total: totalSize
});
})
// 创建写入流
const writer = fs.createWriteStream(downloadPath);
const writer = fs.createWriteStream(downloadPath)
// 将响应数据流写入文件
response.data.pipe(writer);
response.data.pipe(writer)
return new Promise((resolve, reject) => {
writer.on('finish', () => {
@@ -188,37 +312,36 @@ async function downloadFile(url: string): Promise<string> {
percent: 100,
transferred: totalSize,
total: totalSize
});
})
console.log('File download completed:', downloadPath);
resolve(downloadPath);
});
console.log('File download completed:', downloadPath)
resolve(downloadPath)
})
writer.on('error', (error) => {
// 删除部分下载的文件
fs.unlink(downloadPath, () => {});
reject(error);
});
fs.unlink(downloadPath, () => {})
reject(error)
})
response.data.on('error', (error: Error) => {
writer.destroy();
fs.unlink(downloadPath, () => {});
reject(error);
});
});
writer.destroy()
fs.unlink(downloadPath, () => {})
reject(error)
})
})
} catch (error: any) {
// 删除可能创建的文件
if (fs.existsSync(downloadPath)) {
fs.unlink(downloadPath, () => {});
fs.unlink(downloadPath, () => {})
}
if (error.response) {
throw new Error(`Download failed: HTTP ${error.response.status} ${error.response.statusText}`);
throw new Error(`Download failed: HTTP ${error.response.status} ${error.response.statusText}`)
} else if (error.request) {
throw new Error('Download failed: Network error');
throw new Error('Download failed: Network error')
} else {
throw new Error(`Download failed: ${error.message}`);
throw new Error(`Download failed: ${error.message}`)
}
}
}
@@ -226,37 +349,37 @@ async function downloadFile(url: string): Promise<string> {
// 退出并安装
export function quitAndInstall() {
if (!currentUpdateInfo) {
console.error('No update info available for installation');
return;
console.error('No update info available for installation')
return
}
// 对于不同平台,处理方式不同
if (process.platform === 'win32') {
// Windows: 打开安装程序
const fileName = path.basename(currentUpdateInfo.url);
const downloadPath = path.join(app.getPath('temp'), fileName);
const fileName = path.basename(currentUpdateInfo.url)
const downloadPath = path.join(app.getPath('temp'), fileName)
if (fs.existsSync(downloadPath)) {
shell.openPath(downloadPath).then(() => {
app.quit();
});
app.quit()
})
} else {
console.error('Downloaded file not found:', downloadPath);
console.error('Downloaded file not found:', downloadPath)
}
} else if (process.platform === 'darwin') {
// macOS: 打开 dmg 或 zip 文件
const fileName = path.basename(currentUpdateInfo.url);
const downloadPath = path.join(app.getPath('temp'), fileName);
const fileName = path.basename(currentUpdateInfo.url)
const downloadPath = path.join(app.getPath('temp'), fileName)
if (fs.existsSync(downloadPath)) {
shell.openPath(downloadPath).then(() => {
app.quit();
});
app.quit()
})
} else {
console.error('Downloaded file not found:', downloadPath);
console.error('Downloaded file not found:', downloadPath)
}
} else {
// Linux: 打开下载文件夹
shell.showItemInFolder(path.join(app.getPath('temp'), path.basename(currentUpdateInfo.url)));
shell.showItemInFolder(path.join(app.getPath('temp'), path.basename(currentUpdateInfo.url)))
}
}

View File

@@ -1,28 +1,28 @@
import { ipcMain, BrowserWindow } from 'electron';
import { initAutoUpdater, checkForUpdates, downloadUpdate, quitAndInstall } from '../autoUpdate';
import { ipcMain, BrowserWindow } from 'electron'
import { initAutoUpdater, checkForUpdates, downloadUpdate, quitAndInstall } from '../autoUpdate'
// 注册自动更新相关的IPC事件
export function registerAutoUpdateEvents() {
// 检查更新
ipcMain.handle('auto-updater:check-for-updates', (event) => {
const window = BrowserWindow.fromWebContents(event.sender);
const window = BrowserWindow.fromWebContents(event.sender)
if (window) {
checkForUpdates(window);
checkForUpdates(window)
}
});
})
// 下载更新
ipcMain.handle('auto-updater:download-update', () => {
downloadUpdate();
});
downloadUpdate()
})
// 安装更新
ipcMain.handle('auto-updater:quit-and-install', () => {
quitAndInstall();
});
quitAndInstall()
})
}
// 初始化自动更新(在主窗口创建后调用)
export function initAutoUpdateForWindow(window: BrowserWindow) {
initAutoUpdater(window);
}
initAutoUpdater(window)
}

View File

@@ -30,4 +30,4 @@ ipcMain.handle('music-cache:get-size', async () => {
console.error('获取缓存大小失败:', error)
return 0
}
})
})

361
src/main/events/songList.ts Normal file
View File

@@ -0,0 +1,361 @@
import { ipcMain } from 'electron'
import ManageSongList, { SongListError } from '../services/songList/ManageSongList'
import type { SongList, Songs } from '@common/types/songList'
// 创建新歌单
ipcMain.handle(
'songlist:create',
async (_, name: string, description: string = '', source: SongList['source']) => {
try {
const result = ManageSongList.createPlaylist(name, description, source)
return { success: true, data: result, message: '歌单创建成功' }
} catch (error) {
console.error('创建歌单失败:', error)
const message = error instanceof SongListError ? error.message : '创建歌单失败'
return {
success: false,
error: message,
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
}
}
}
)
// 获取所有歌单
ipcMain.handle('songlist:get-all', async () => {
try {
const songLists = ManageSongList.Read()
return { success: true, data: songLists }
} catch (error) {
console.error('获取歌单列表失败:', error)
const message = error instanceof SongListError ? error.message : '获取歌单列表失败'
return {
success: false,
error: message,
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
}
}
})
// 根据ID获取歌单信息
ipcMain.handle('songlist:get-by-id', async (_, hashId: string) => {
try {
const songList = ManageSongList.getById(hashId)
return { success: true, data: songList }
} catch (error) {
console.error('获取歌单信息失败:', error)
return { success: false, error: '获取歌单信息失败' }
}
})
// 删除歌单
ipcMain.handle('songlist:delete', async (_, hashId: string) => {
try {
ManageSongList.deleteById(hashId)
return { success: true, message: '歌单删除成功' }
} catch (error) {
console.error('删除歌单失败:', error)
const message = error instanceof SongListError ? error.message : '删除歌单失败'
return {
success: false,
error: message,
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
}
}
})
// 批量删除歌单
ipcMain.handle('songlist:batch-delete', async (_, hashIds: string[]) => {
try {
const result = ManageSongList.batchDelete(hashIds)
return {
success: true,
data: result,
message: `成功删除 ${result.success.length} 个歌单,失败 ${result.failed.length}`
}
} catch (error) {
console.error('批量删除歌单失败:', error)
return { success: false, error: '批量删除歌单失败' }
}
})
// 编辑歌单信息
ipcMain.handle(
'songlist:edit',
async (_, hashId: string, updates: Partial<Omit<SongList, 'id' | 'createTime'>>) => {
try {
ManageSongList.editById(hashId, updates)
return { success: true, message: '歌单信息更新成功' }
} catch (error) {
console.error('编辑歌单失败:', error)
const message = error instanceof SongListError ? error.message : '编辑歌单失败'
return {
success: false,
error: message,
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
}
}
}
)
// 更新歌单封面
ipcMain.handle('songlist:update-cover', async (_, hashId: string, coverImgUrl: string) => {
try {
ManageSongList.updateCoverImgById(hashId, coverImgUrl)
return { success: true, message: '封面更新成功' }
} catch (error) {
console.error('更新封面失败:', error)
const message = error instanceof SongListError ? error.message : '更新封面失败'
return {
success: false,
error: message,
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
}
}
})
// 搜索歌单
ipcMain.handle('songlist:search', async (_, keyword: string, source?: SongList['source']) => {
try {
const results = ManageSongList.search(keyword, source)
return { success: true, data: results }
} catch (error) {
console.error('搜索歌单失败:', error)
return { success: false, error: '搜索歌单失败', data: [] }
}
})
// 获取歌单统计信息
ipcMain.handle('songlist:get-statistics', async () => {
try {
const statistics = ManageSongList.getStatistics()
return { success: true, data: statistics }
} catch (error) {
console.error('获取统计信息失败:', error)
return { success: false, error: '获取统计信息失败' }
}
})
// 检查歌单是否存在
ipcMain.handle('songlist:exists', async (_, hashId: string) => {
try {
const exists = ManageSongList.exists(hashId)
return { success: true, data: exists }
} catch (error) {
console.error('检查歌单存在性失败:', error)
return { success: false, error: '检查歌单存在性失败', data: false }
}
})
// === 歌曲管理相关 IPC 事件 ===
// 添加歌曲到歌单
ipcMain.handle('songlist:add-songs', async (_, hashId: string, songs: Songs[]) => {
try {
const instance = new ManageSongList(hashId)
instance.addSongs(songs)
return { success: true, message: `成功添加 ${songs.length} 首歌曲` }
} catch (error) {
console.error('添加歌曲失败:', error)
const message = error instanceof SongListError ? error.message : '添加歌曲失败'
return {
success: false,
error: message,
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
}
}
})
// 从歌单移除歌曲
ipcMain.handle('songlist:remove-song', async (_, hashId: string, songmid: string | number) => {
try {
const instance = new ManageSongList(hashId)
const removed = instance.removeSong(songmid)
return {
success: true,
data: removed,
message: removed ? '歌曲移除成功' : '歌曲不存在'
}
} catch (error) {
console.error('移除歌曲失败:', error)
const message = error instanceof SongListError ? error.message : '移除歌曲失败'
return {
success: false,
error: message,
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
}
}
})
// 批量移除歌曲
ipcMain.handle(
'songlist:remove-songs',
async (_, hashId: string, songmids: (string | number)[]) => {
try {
const instance = new ManageSongList(hashId)
const result = instance.removeSongs(songmids)
return {
success: true,
data: result,
message: `成功移除 ${result.removed} 首歌曲,${result.notFound} 首未找到`
}
} catch (error) {
console.error('批量移除歌曲失败:', error)
const message = error instanceof SongListError ? error.message : '批量移除歌曲失败'
return {
success: false,
error: message,
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
}
}
}
)
// 清空歌单
ipcMain.handle('songlist:clear-songs', async (_, hashId: string) => {
try {
const instance = new ManageSongList(hashId)
instance.clearSongs()
return { success: true, message: '歌单已清空' }
} catch (error) {
console.error('清空歌单失败:', error)
const message = error instanceof SongListError ? error.message : '清空歌单失败'
return {
success: false,
error: message,
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
}
}
})
// 获取歌单中的歌曲列表
ipcMain.handle('songlist:get-songs', async (_, hashId: string) => {
try {
const instance = new ManageSongList(hashId)
const songs = instance.getSongs()
return { success: true, data: songs }
} catch (error) {
console.error('获取歌曲列表失败:', error)
const message = error instanceof SongListError ? error.message : '获取歌曲列表失败'
return {
success: false,
error: message,
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
}
}
})
// 获取歌单歌曲数量
ipcMain.handle('songlist:get-song-count', async (_, hashId: string) => {
try {
const instance = new ManageSongList(hashId)
const count = instance.getCount()
return { success: true, data: count }
} catch (error) {
console.error('获取歌曲数量失败:', error)
const message = error instanceof SongListError ? error.message : '获取歌曲数量失败'
return {
success: false,
error: message,
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
}
}
})
// 检查歌曲是否在歌单中
ipcMain.handle('songlist:has-song', async (_, hashId: string, songmid: string | number) => {
try {
const instance = new ManageSongList(hashId)
const hasSong = instance.hasSong(songmid)
return { success: true, data: hasSong }
} catch (error) {
console.error('检查歌曲存在性失败:', error)
return { success: false, error: '检查歌曲存在性失败', data: false }
}
})
// 根据ID获取歌曲
ipcMain.handle('songlist:get-song', async (_, hashId: string, songmid: string | number) => {
try {
const instance = new ManageSongList(hashId)
const song = instance.getSong(songmid)
return { success: true, data: song }
} catch (error) {
console.error('获取歌曲失败:', error)
return { success: false, error: '获取歌曲失败', data: null }
}
})
// 搜索歌单中的歌曲
ipcMain.handle('songlist:search-songs', async (_, hashId: string, keyword: string) => {
try {
const instance = new ManageSongList(hashId)
const results = instance.searchSongs(keyword)
return { success: true, data: results }
} catch (error) {
console.error('搜索歌曲失败:', error)
return { success: false, error: '搜索歌曲失败', data: [] }
}
})
// 获取歌单歌曲统计信息
ipcMain.handle('songlist:get-song-statistics', async (_, hashId: string) => {
try {
const instance = new ManageSongList(hashId)
const statistics = instance.getStatistics()
return { success: true, data: statistics }
} catch (error) {
console.error('获取歌曲统计信息失败:', error)
return { success: false, error: '获取歌曲统计信息失败' }
}
})
// 验证歌单完整性
ipcMain.handle('songlist:validate-integrity', async (_, hashId: string) => {
try {
const instance = new ManageSongList(hashId)
const result = instance.validateIntegrity()
return { success: true, data: result }
} catch (error) {
console.error('验证歌单完整性失败:', error)
return { success: false, error: '验证歌单完整性失败' }
}
})
// 修复歌单数据
ipcMain.handle('songlist:repair-data', async (_, hashId: string) => {
try {
const instance = new ManageSongList(hashId)
const result = instance.repairData()
return {
success: true,
data: result,
message: result.fixed ? `数据修复完成: ${result.changes.join(', ')}` : '数据无需修复'
}
} catch (error) {
console.error('修复歌单数据失败:', error)
const message = error instanceof SongListError ? error.message : '修复歌单数据失败'
return {
success: false,
error: message,
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
}
}
})
// 强制保存歌单
ipcMain.handle('songlist:force-save', async (_, hashId: string) => {
try {
const instance = new ManageSongList(hashId)
instance.forceSave()
return { success: true, message: '歌单保存成功' }
} catch (error) {
console.error('强制保存歌单失败:', error)
const message = error instanceof SongListError ? error.message : '强制保存歌单失败'
return {
success: false,
error: message,
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
}
}
})

View File

@@ -8,6 +8,24 @@ import pluginService from './services/plugin'
import aiEvents from './events/ai'
import './services/musicSdk/index'
// 获取单实例锁
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
// 如果没有获得锁,说明已经有实例在运行,退出当前实例
app.quit()
} else {
// 当第二个实例尝试启动时,聚焦到第一个实例的窗口
app.on('second-instance', () => {
// 如果有窗口存在,聚焦到该窗口
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore()
if (!mainWindow.isVisible()) mainWindow.show()
mainWindow.focus()
}
})
}
// import wy from './utils/musicSdk/wy/index'
// import kg from './utils/musicSdk/kg/index'
// wy.hotSearch.getList().then((res) => {
@@ -40,6 +58,7 @@ function createTray(): void {
label: '播放/暂停',
click: () => {
// 这里可以添加播放控制逻辑
console.log('music-control')
mainWindow?.webContents.send('music-control')
}
},
@@ -75,7 +94,7 @@ function createWindow(): void {
mainWindow = new BrowserWindow({
width: 1100,
height: 750,
minWidth: 970,
minWidth: 1100,
minHeight: 670,
show: false,
center: true,
@@ -94,7 +113,6 @@ function createWindow(): void {
contextIsolation: false,
backgroundThrottling: false
}
})
if (process.platform == 'darwin') mainWindow.setWindowButtonVisibility(false)
@@ -199,6 +217,7 @@ ipcMain.handle('get-app-version', () => {
aiEvents(mainWindow)
import './events/musicCache'
import './events/songList'
import { registerAutoUpdateEvents, initAutoUpdateForWindow } from './events/autoUpdate'
// This method will be called when Electron has finished
@@ -217,7 +236,7 @@ app.whenReady().then(() => {
} catch (error) {
console.error('插件系统初始化失败:', error)
}
},1000)
}, 1000)
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.

View File

@@ -4,7 +4,6 @@ import * as fs from 'fs/promises'
import * as crypto from 'crypto'
import axios from 'axios'
export class MusicCacheService {
private cacheDir: string
private cacheIndex: Map<string, string> = new Map()
@@ -20,7 +19,7 @@ export class MusicCacheService {
try {
// 确保缓存目录存在
await fs.mkdir(this.cacheDir, { recursive: true })
// 加载缓存索引
await this.loadCacheIndex()
} catch (error) {
@@ -60,12 +59,12 @@ export class MusicCacheService {
async getCachedMusicUrl(songId: string, originalUrlPromise: Promise<string>): Promise<string> {
const cacheKey = this.generateCacheKey(songId)
console.log('hash',cacheKey)
console.log('hash', cacheKey)
// 检查是否已缓存
if (this.cacheIndex.has(cacheKey)) {
const cachedFilePath = this.cacheIndex.get(cacheKey)!
try {
// 验证文件是否存在
await fs.access(cachedFilePath)
@@ -86,7 +85,7 @@ export class MusicCacheService {
private async downloadAndCache(songId: string, url: string, cacheKey: string): Promise<string> {
try {
console.log(`开始下载歌曲: ${songId}`)
const response = await axios({
method: 'GET',
url: url,
@@ -108,7 +107,7 @@ export class MusicCacheService {
// 更新缓存索引
this.cacheIndex.set(cacheKey, cacheFilePath)
await this.saveCacheIndex()
console.log(`歌曲缓存完成: ${cacheFilePath}`)
resolve(`file://${cacheFilePath}`)
} catch (error) {
@@ -143,7 +142,7 @@ export class MusicCacheService {
// 清空缓存索引
this.cacheIndex.clear()
await this.saveCacheIndex()
console.log('音乐缓存已清空')
} catch (error) {
console.error('清空缓存失败:', error)
@@ -152,7 +151,7 @@ export class MusicCacheService {
async getCacheSize(): Promise<number> {
let totalSize = 0
for (const filePath of this.cacheIndex.values()) {
try {
const stats = await fs.stat(filePath)
@@ -161,14 +160,14 @@ export class MusicCacheService {
// 文件不存在,忽略
}
}
return totalSize
}
async getCacheInfo(): Promise<{ count: number; size: number; sizeFormatted: string }> {
const size = await this.getCacheSize()
const count = this.cacheIndex.size
const formatSize = (bytes: number): string => {
if (bytes === 0) return '0 B'
const k = 1024
@@ -186,4 +185,4 @@ export class MusicCacheService {
}
// 单例实例
export const musicCacheService = new MusicCacheService()
export const musicCacheService = new MusicCacheService()

View File

@@ -19,7 +19,7 @@ export function request<T extends keyof MainApi>(
return (Api[method] as (args: any) => any)(args)
}
throw new Error(`未知的方法: ${method}`)
}catch (error:any){
} catch (error: any) {
throw new Error(error.message)
}
}

View File

@@ -22,7 +22,6 @@ import { fileURLToPath } from 'url'
const fileLock: Record<string, boolean> = {}
function main(source: string) {
const Api = musicSdk[source]
return {
@@ -38,7 +37,6 @@ function main(source: string) {
// 获取原始URL
const originalUrlPromise = usePlugin.getMusicUrl(source, songInfo, quality)
// 生成歌曲唯一标识
const songId = `${songInfo.name}-${songInfo.singer}-${source}-${quality}`
@@ -161,6 +159,26 @@ function main(source: string) {
message: '下载成功',
path: songPath
}
},
async parsePlaylistId({ url }: { url: string }) {
try {
return await Api.songList.handleParseId(url)
} catch (e: any) {
return {
error: '解析歌单链接失败 ' + (e.error || e.message || e)
}
}
},
async getPlaylistDetailById(id: string, page: number = 1) {
try {
return await Api.songList.getListDetail(id, page)
} catch (e: any) {
return {
error: '获取歌单详情失败 ' + (e.error || e.message || e)
}
}
}
}
}

View File

@@ -92,4 +92,4 @@ export interface PlaylistDetailResult {
export interface DownloadSingleSongArgs extends GetMusicUrlArg {
path?: string
}
}

View File

@@ -0,0 +1,755 @@
import type { SongList, Songs } from '@common/types/songList'
import PlayListSongs from './PlayListSongs'
import fs from 'fs'
import path from 'path'
import crypto from 'crypto'
import { getAppDirPath } from '../../utils/path'
// 常量定义
const DEFAULT_COVER_IDENTIFIER = 'default-cover'
const SONGLIST_DIR = 'songList'
const INDEX_FILE = 'index.json'
// 错误类型定义
class SongListError extends Error {
constructor(
message: string,
public code?: string
) {
super(message)
this.name = 'SongListError'
}
}
// 工具函数类
class SongListUtils {
/**
* 获取默认封面标识符
*/
static getDefaultCoverUrl(): string {
return DEFAULT_COVER_IDENTIFIER
}
/**
* 获取歌单管理入口文件路径
*/
static getSongListIndexPath(): string {
return path.join(getAppDirPath('userData'), SONGLIST_DIR, INDEX_FILE)
}
/**
* 获取歌单文件路径
*/
static getSongListFilePath(hashId: string): string {
return path.join(getAppDirPath('userData'), SONGLIST_DIR, `${hashId}.json`)
}
/**
* 确保目录存在
*/
static ensureDirectoryExists(dirPath: string): void {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true })
}
}
/**
* 生成唯一hashId
*/
static generateUniqueId(name: string): string {
return crypto.createHash('md5').update(`${name}_${Date.now()}_${Math.random()}`).digest('hex')
}
/**
* 验证歌曲封面URL是否有效
*/
static isValidCoverUrl(url: string | undefined | null): boolean {
return Boolean(url && url.trim() !== '' && url !== DEFAULT_COVER_IDENTIFIER)
}
/**
* 验证hashId格式
*/
static isValidHashId(hashId: string): boolean {
return Boolean(hashId && typeof hashId === 'string' && hashId.trim().length > 0)
}
/**
* 安全的JSON解析
*/
static safeJsonParse<T>(content: string, defaultValue: T): T {
try {
return JSON.parse(content) as T
} catch {
return defaultValue
}
}
}
export default class ManageSongList extends PlayListSongs {
private readonly hashId: string
constructor(hashId: string) {
if (!SongListUtils.isValidHashId(hashId)) {
throw new SongListError('无效的歌单ID', 'INVALID_HASH_ID')
}
super(hashId)
this.hashId = hashId.trim()
}
/**
* 静态方法:创建新歌单
* @param name 歌单名称
* @param description 歌单描述
* @param source 歌单来源
* @returns 包含hashId的对象 (id字段就是hashId)
*/
static createPlaylist(
name: string,
description: string = '',
source: SongList['source']
): { id: string } {
// 参数验证
if (!name?.trim()) {
throw new SongListError('歌单名称不能为空', 'EMPTY_NAME')
}
if (!source) {
throw new SongListError('歌单来源不能为空', 'EMPTY_SOURCE')
}
try {
const id = SongListUtils.generateUniqueId(name)
const now = new Date().toISOString()
const songListInfo: SongList = {
id,
name: name.trim(),
createTime: now,
updateTime: now,
description: description?.trim() || '',
coverImgUrl: SongListUtils.getDefaultCoverUrl(),
source
}
// 创建歌单文件
ManageSongList.createSongListFile(id)
// 更新入口文件
ManageSongList.updateIndexFile(songListInfo, 'add')
// 验证歌单可以正常实例化
try {
new ManageSongList(id)
// 如果能成功创建实例,说明文件创建成功
} catch (verifyError) {
console.error('歌单创建验证失败:', verifyError)
// 清理已创建的文件
try {
const filePath = SongListUtils.getSongListFilePath(id)
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath)
}
} catch (cleanupError) {
console.error('清理失败的歌单文件时出错:', cleanupError)
}
throw new SongListError('歌单创建后验证失败', 'CREATION_VERIFICATION_FAILED')
}
return { id }
} catch (error) {
console.error('创建歌单失败:', error)
if (error instanceof SongListError) {
throw error
}
throw new SongListError(
`创建歌单失败: ${error instanceof Error ? error.message : '未知错误'}`,
'CREATE_FAILED'
)
}
}
/**
* 创建歌单文件
* @param hashId 歌单hashId
*/
private static createSongListFile(hashId: string): void {
const songListFilePath = SongListUtils.getSongListFilePath(hashId)
const dir = path.dirname(songListFilePath)
SongListUtils.ensureDirectoryExists(dir)
try {
// 使用原子性写入确保文件完整性
const tempPath = `${songListFilePath}.tmp`
const content = JSON.stringify([], null, 2)
fs.writeFileSync(tempPath, content)
fs.renameSync(tempPath, songListFilePath)
// 确保文件确实存在且可读
if (!fs.existsSync(songListFilePath)) {
throw new Error('文件创建后验证失败')
}
// 验证文件内容
const verifyContent = fs.readFileSync(songListFilePath, 'utf-8')
JSON.parse(verifyContent) // 确保内容是有效的JSON
} catch (error) {
throw new SongListError(
`创建歌单文件失败: ${error instanceof Error ? error.message : '未知错误'}`,
'FILE_CREATE_FAILED'
)
}
}
/**
* 删除当前歌单
*/
delete(): void {
const hashId = this.getHashId()
try {
// 检查歌单是否存在
if (!ManageSongList.exists(hashId)) {
throw new SongListError('歌单不存在', 'PLAYLIST_NOT_FOUND')
}
// 删除歌单文件
const filePath = SongListUtils.getSongListFilePath(hashId)
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath)
}
// 从入口文件中移除
ManageSongList.updateIndexFile({ id: hashId } as SongList, 'remove')
} catch (error) {
console.error('删除歌单失败:', error)
if (error instanceof SongListError) {
throw error
}
throw new SongListError(
`删除歌单失败: ${error instanceof Error ? error.message : '未知错误'}`,
'DELETE_FAILED'
)
}
}
/**
* 修改当前歌单信息
* @param updates 要更新的字段
*/
edit(updates: Partial<Omit<SongList, 'id' | 'createTime'>>): void {
if (!updates || Object.keys(updates).length === 0) {
throw new SongListError('更新内容不能为空', 'EMPTY_UPDATES')
}
const hashId = this.getHashId()
try {
const songLists = ManageSongList.readIndexFile()
const index = songLists.findIndex((item) => item.id === hashId)
if (index === -1) {
throw new SongListError('歌单不存在', 'PLAYLIST_NOT_FOUND')
}
// 验证和清理更新数据
const cleanUpdates = ManageSongList.validateAndCleanUpdates(updates)
// 更新歌单信息
songLists[index] = {
...songLists[index],
...cleanUpdates,
updateTime: new Date().toISOString()
}
// 保存到入口文件
ManageSongList.writeIndexFile(songLists)
} catch (error) {
console.error('修改歌单失败:', error)
if (error instanceof SongListError) {
throw error
}
throw new SongListError(
`修改歌单失败: ${error instanceof Error ? error.message : '未知错误'}`,
'EDIT_FAILED'
)
}
}
/**
* 获取当前歌单的hashId
* @returns hashId
*/
private getHashId(): string {
return this.hashId
}
/**
* 验证和清理更新数据
* @param updates 原始更新数据
* @returns 清理后的更新数据
*/
private static validateAndCleanUpdates(
updates: Partial<Omit<SongList, 'id' | 'createTime'>>
): Partial<Omit<SongList, 'id' | 'createTime'>> {
const cleanUpdates: Partial<Omit<SongList, 'id' | 'createTime'>> = {}
// 验证歌单名称
if (updates.name !== undefined) {
const trimmedName = updates.name.trim()
if (!trimmedName) {
throw new SongListError('歌单名称不能为空', 'EMPTY_NAME')
}
cleanUpdates.name = trimmedName
}
// 处理描述
if (updates.description !== undefined) {
cleanUpdates.description = updates.description?.trim() || ''
}
// 处理封面URL
if (updates.coverImgUrl !== undefined) {
cleanUpdates.coverImgUrl = updates.coverImgUrl || SongListUtils.getDefaultCoverUrl()
}
// 处理来源
if (updates.source !== undefined) {
if (!updates.source) {
throw new SongListError('歌单来源不能为空', 'EMPTY_SOURCE')
}
cleanUpdates.source = updates.source
}
return cleanUpdates
}
/**
* 读取歌单列表
* @returns 歌单列表数组
*/
static Read(): SongList[] {
try {
return ManageSongList.readIndexFile()
} catch (error) {
console.error('读取歌单列表失败:', error)
if (error instanceof SongListError) {
throw error
}
throw new SongListError(
`读取歌单列表失败: ${error instanceof Error ? error.message : '未知错误'}`,
'READ_FAILED'
)
}
}
/**
* 根据hashId获取单个歌单信息
* @param hashId 歌单hashId
* @returns 歌单信息或null
*/
static getById(hashId: string): SongList | null {
if (!SongListUtils.isValidHashId(hashId)) {
return null
}
try {
const songLists = ManageSongList.readIndexFile()
return songLists.find((item) => item.id === hashId) || null
} catch (error) {
console.error('获取歌单信息失败:', error)
return null
}
}
/**
* 读取入口文件
* @returns 歌单列表数组
*/
private static readIndexFile(): SongList[] {
const indexPath = SongListUtils.getSongListIndexPath()
if (!fs.existsSync(indexPath)) {
ManageSongList.initializeIndexFile()
return []
}
try {
const content = fs.readFileSync(indexPath, 'utf-8')
const parsed = SongListUtils.safeJsonParse<unknown>(content, [])
// 验证数据格式
if (!Array.isArray(parsed)) {
console.warn('入口文件格式错误,重新初始化')
ManageSongList.initializeIndexFile()
return []
}
return parsed as SongList[]
} catch (error) {
console.error('解析入口文件失败:', error)
// 备份损坏的文件并重新初始化
ManageSongList.backupCorruptedFile(indexPath)
ManageSongList.initializeIndexFile()
return []
}
}
/**
* 备份损坏的文件
* @param filePath 文件路径
*/
private static backupCorruptedFile(filePath: string): void {
try {
const backupPath = `${filePath}.backup.${Date.now()}`
fs.copyFileSync(filePath, backupPath)
console.log(`已备份损坏的文件到: ${backupPath}`)
} catch (error) {
console.error('备份损坏文件失败:', error)
}
}
/**
* 初始化入口文件
*/
private static initializeIndexFile(): void {
const indexPath = SongListUtils.getSongListIndexPath()
const dir = path.dirname(indexPath)
SongListUtils.ensureDirectoryExists(dir)
try {
fs.writeFileSync(indexPath, JSON.stringify([], null, 2))
} catch (error) {
throw new SongListError(
`初始化入口文件失败: ${error instanceof Error ? error.message : '未知错误'}`,
'INIT_FAILED'
)
}
}
/**
* 写入入口文件
* @param songLists 歌单列表
*/
private static writeIndexFile(songLists: SongList[]): void {
if (!Array.isArray(songLists)) {
throw new SongListError('歌单列表必须是数组格式', 'INVALID_DATA_FORMAT')
}
const indexPath = SongListUtils.getSongListIndexPath()
const dir = path.dirname(indexPath)
SongListUtils.ensureDirectoryExists(dir)
try {
// 先写入临时文件,再重命名,确保原子性操作
const tempPath = `${indexPath}.tmp`
fs.writeFileSync(tempPath, JSON.stringify(songLists, null, 2))
fs.renameSync(tempPath, indexPath)
} catch (error) {
console.error('写入入口文件失败:', error)
throw new SongListError(
`写入入口文件失败: ${error instanceof Error ? error.message : '未知错误'}`,
'WRITE_FAILED'
)
}
}
/**
* 更新入口文件
* @param songListInfo 歌单信息
* @param action 操作类型
*/
private static updateIndexFile(songListInfo: SongList, action: 'add' | 'remove'): void {
const songLists = ManageSongList.readIndexFile()
switch (action) {
case 'add':
// 检查是否已存在,避免重复添加
if (!songLists.some((item) => item.id === songListInfo.id)) {
songLists.push(songListInfo)
}
break
case 'remove':
const index = songLists.findIndex((item) => item.id === songListInfo.id)
if (index !== -1) {
songLists.splice(index, 1)
}
break
default:
throw new SongListError(`不支持的操作类型: ${action}`, 'INVALID_ACTION')
}
ManageSongList.writeIndexFile(songLists)
}
/**
* 更新当前歌单封面图片URL
* @param coverImgUrl 封面图片URL
*/
updateCoverImg(coverImgUrl: string): void {
try {
const finalCoverUrl = coverImgUrl || SongListUtils.getDefaultCoverUrl()
this.edit({ coverImgUrl: finalCoverUrl })
} catch (error) {
console.error('更新封面失败:', error)
if (error instanceof SongListError) {
throw error
}
throw new SongListError(
`更新封面失败: ${error instanceof Error ? error.message : '未知错误'}`,
'UPDATE_COVER_FAILED'
)
}
}
/**
* 重写父类的addSongs方法添加自动设置封面功能
* @param songs 要添加的歌曲列表
*/
addSongs(songs: Songs[]): void {
if (!Array.isArray(songs) || songs.length === 0) {
return
}
// 调用父类方法添加歌曲
super.addSongs(songs)
// 异步更新封面,不阻塞主要功能
setImmediate(() => {
this.updateCoverIfNeeded(songs)
})
}
/**
* 检查并更新封面图片
* @param newSongs 新添加的歌曲列表
*/
private updateCoverIfNeeded(newSongs: Songs[]): void {
try {
const currentPlaylist = ManageSongList.getById(this.hashId)
if (!currentPlaylist) {
console.warn(`歌单 ${this.hashId} 不存在,跳过封面更新`)
return
}
const shouldUpdateCover = this.shouldUpdateCover(currentPlaylist.coverImgUrl)
if (shouldUpdateCover) {
const validCoverUrl = this.findValidCoverFromSongs(newSongs)
if (validCoverUrl) {
this.updateCoverImg(validCoverUrl)
} else if (
!currentPlaylist.coverImgUrl ||
currentPlaylist.coverImgUrl === SongListUtils.getDefaultCoverUrl()
) {
// 如果没有找到有效封面且当前也没有封面,设置默认封面
this.updateCoverImg(SongListUtils.getDefaultCoverUrl())
}
}
} catch (error) {
console.error('更新封面失败:', error)
// 不抛出错误,避免影响添加歌曲的主要功能
}
}
/**
* 判断是否应该更新封面
* @param currentCoverUrl 当前封面URL
* @returns 是否应该更新
*/
private shouldUpdateCover(currentCoverUrl: string): boolean {
return !currentCoverUrl || currentCoverUrl === SongListUtils.getDefaultCoverUrl()
}
/**
* 从歌曲列表中查找有效的封面图片
* @param songs 歌曲列表
* @returns 有效的封面URL或null
*/
private findValidCoverFromSongs(songs: Songs[]): string | null {
// 优先检查新添加的歌曲
for (const song of songs) {
if (SongListUtils.isValidCoverUrl(song.img)) {
return song.img
}
}
// 如果新添加的歌曲都没有封面,检查当前歌单中的所有歌曲
try {
for (const song of this.list) {
if (SongListUtils.isValidCoverUrl(song.img)) {
return song.img
}
}
} catch (error) {
console.error('获取歌单歌曲列表失败:', error)
}
return null
}
/**
* 检查歌单是否存在
* @param hashId 歌单hashId
* @returns 是否存在
*/
static exists(hashId: string): boolean {
if (!SongListUtils.isValidHashId(hashId)) {
return false
}
try {
const songLists = ManageSongList.readIndexFile()
return songLists.some((item) => item.id === hashId)
} catch (error) {
console.error('检查歌单存在性失败:', error)
return false
}
}
/**
* 获取歌单统计信息
* @returns 统计信息
*/
static getStatistics(): { total: number; bySource: Record<string, number>; lastUpdated: string } {
try {
const songLists = ManageSongList.readIndexFile()
const bySource: Record<string, number> = {}
songLists.forEach((playlist) => {
const source = playlist.source || 'unknown'
bySource[source] = (bySource[source] || 0) + 1
})
return {
total: songLists.length,
bySource,
lastUpdated: new Date().toISOString()
}
} catch (error) {
console.error('获取统计信息失败:', error)
return {
total: 0,
bySource: {},
lastUpdated: new Date().toISOString()
}
}
}
/**
* 获取当前歌单信息
* @returns 歌单信息或null
*/
getPlaylistInfo(): SongList | null {
return ManageSongList.getById(this.hashId)
}
/**
* 批量操作:删除多个歌单
* @param hashIds 歌单ID数组
* @returns 操作结果
*/
static batchDelete(hashIds: string[]): { success: string[]; failed: string[] } {
const result = { success: [] as string[], failed: [] as string[] }
for (const hashId of hashIds) {
try {
ManageSongList.deleteById(hashId)
result.success.push(hashId)
} catch (error) {
console.error(`删除歌单 ${hashId} 失败:`, error)
result.failed.push(hashId)
}
}
return result
}
/**
* 搜索歌单
* @param keyword 搜索关键词
* @param source 可选的来源筛选
* @returns 匹配的歌单列表
*/
static search(keyword: string, source?: SongList['source']): SongList[] {
if (!keyword?.trim()) {
return []
}
try {
const songLists = ManageSongList.readIndexFile()
const lowerKeyword = keyword.toLowerCase()
return songLists.filter((playlist) => {
const matchesKeyword =
playlist.name.toLowerCase().includes(lowerKeyword) ||
playlist.description.toLowerCase().includes(lowerKeyword)
const matchesSource = !source || playlist.source === source
return matchesKeyword && matchesSource
})
} catch (error) {
console.error('搜索歌单失败:', error)
return []
}
}
// 静态方法别名用于删除和编辑指定hashId的歌单
/**
* 静态方法:删除指定歌单
* @param hashId 歌单hashId
*/
static deleteById(hashId: string): void {
if (!SongListUtils.isValidHashId(hashId)) {
throw new SongListError('无效的歌单ID', 'INVALID_HASH_ID')
}
const instance = new ManageSongList(hashId)
instance.delete()
}
/**
* 静态方法:编辑指定歌单
* @param hashId 歌单hashId
* @param updates 要更新的字段
*/
static editById(hashId: string, updates: Partial<Omit<SongList, 'id' | 'createTime'>>): void {
if (!SongListUtils.isValidHashId(hashId)) {
throw new SongListError('无效的歌单ID', 'INVALID_HASH_ID')
}
const instance = new ManageSongList(hashId)
instance.edit(updates)
}
/**
* 静态方法:更新指定歌单封面
* @param hashId 歌单hashId
* @param coverImgUrl 封面图片URL
*/
static updateCoverImgById(hashId: string, coverImgUrl: string): void {
if (!SongListUtils.isValidHashId(hashId)) {
throw new SongListError('无效的歌单ID', 'INVALID_HASH_ID')
}
const instance = new ManageSongList(hashId)
instance.updateCoverImg(coverImgUrl)
}
// 保持向后兼容的别名方法
static Delete = ManageSongList.deleteById
static Edit = ManageSongList.editById
static read = ManageSongList.Read
}
// 导出错误类供外部使用
export { SongListError }

View File

@@ -0,0 +1,452 @@
import type { Songs as SongItem } from '@common/types/songList'
import fs from 'fs'
import path from 'path'
import { getAppDirPath } from '../../utils/path'
// 错误类定义
class PlayListError extends Error {
constructor(
message: string,
public code?: string
) {
super(message)
this.name = 'PlayListError'
}
}
// 工具函数类
class PlayListUtils {
/**
* 获取歌单文件路径
*/
static getFilePath(hashId: string): string {
if (!hashId || typeof hashId !== 'string' || !hashId.trim()) {
throw new PlayListError('无效的歌单ID', 'INVALID_HASH_ID')
}
return path.join(getAppDirPath('userData'), 'songList', `${hashId.trim()}.json`)
}
/**
* 确保目录存在
*/
static ensureDirectoryExists(dirPath: string): void {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true })
}
}
/**
* 安全的JSON解析
*/
static safeJsonParse<T>(content: string, defaultValue: T): T {
try {
const parsed = JSON.parse(content)
return parsed as T
} catch {
return defaultValue
}
}
/**
* 安全的JSON解析专门用于数组
*/
static safeJsonParseArray<T>(content: string, defaultValue: T[]): T[] {
try {
const parsed = JSON.parse(content)
return Array.isArray(parsed) ? parsed : defaultValue
} catch {
return defaultValue
}
}
/**
* 验证歌曲对象
*/
static isValidSong(song: any): song is SongItem {
return (
song &&
typeof song === 'object' &&
(typeof song.songmid === 'string' || typeof song.songmid === 'number') &&
String(song.songmid).trim().length > 0
)
}
/**
* 去重歌曲列表
*/
static deduplicateSongs(songs: SongItem[]): SongItem[] {
const seen = new Set<string>()
return songs.filter((song) => {
const songmidStr = String(song.songmid)
if (seen.has(songmidStr)) {
return false
}
seen.add(songmidStr)
return true
})
}
}
export default class PlayListSongs {
protected readonly filePath: string
protected list: SongItem[]
private isDirty: boolean = false
constructor(hashId: string) {
this.filePath = PlayListUtils.getFilePath(hashId)
this.list = []
this.initList()
}
/**
* 初始化歌单列表
*/
private initList(): void {
// 增加重试机制,处理文件创建的时序问题
const maxRetries = 3
const retryDelay = 100 // 100ms
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
if (!fs.existsSync(this.filePath)) {
if (attempt < maxRetries - 1) {
// 等待一段时间后重试
const start = Date.now()
while (Date.now() - start < retryDelay) {
// 简单的同步等待
}
continue
}
throw new PlayListError('歌单文件不存在', 'FILE_NOT_FOUND')
}
const content = fs.readFileSync(this.filePath, 'utf-8')
const parsed = PlayListUtils.safeJsonParseArray<SongItem>(content, [])
// 验证和清理数据
this.list = parsed.filter(PlayListUtils.isValidSong)
// 如果数据被清理过,标记为需要保存
if (this.list.length !== parsed.length) {
this.isDirty = true
console.warn(
`歌单文件包含无效数据,已自动清理 ${parsed.length - this.list.length} 条无效记录`
)
}
// 成功读取,退出重试循环
return
} catch (error) {
if (attempt < maxRetries - 1) {
console.warn(`读取歌单文件失败,第 ${attempt + 1} 次重试:`, error)
// 等待一段时间后重试
const start = Date.now()
while (Date.now() - start < retryDelay) {
// 简单的同步等待
}
continue
}
console.error('读取歌单文件失败:', error)
throw new PlayListError(
`读取歌单失败: ${error instanceof Error ? error.message : '未知错误'}`,
'READ_FAILED'
)
}
}
}
/**
* 检查歌单文件是否存在
*/
static hasListFile(hashId: string): boolean {
try {
const filePath = PlayListUtils.getFilePath(hashId)
return fs.existsSync(filePath)
} catch {
return false
}
}
/**
* 添加歌曲到歌单
*/
addSongs(songs: SongItem[]): void {
if (!Array.isArray(songs) || songs.length === 0) {
return
}
// 验证和过滤有效歌曲
const validSongs = songs.filter(PlayListUtils.isValidSong)
if (validSongs.length === 0) {
console.warn('没有有效的歌曲可添加')
return
}
// 使用 Set 提高查重性能,统一转换为字符串进行比较
const existingSongMids = new Set(this.list.map((song) => String(song.songmid)))
// 添加不重复的歌曲
const newSongs = validSongs.filter((song) => !existingSongMids.has(String(song.songmid)))
if (newSongs.length > 0) {
this.list.push(...newSongs)
this.isDirty = true
this.saveToFile()
console.log(
`成功添加 ${newSongs.length} 首歌曲,跳过 ${validSongs.length - newSongs.length} 首重复歌曲`
)
} else {
console.log('所有歌曲都已存在,未添加任何歌曲')
}
}
/**
* 从歌单中移除歌曲
*/
removeSong(songmid: string | number): boolean {
if (!songmid && songmid !== 0) {
throw new PlayListError('无效的歌曲ID', 'INVALID_SONG_ID')
}
const songmidStr = String(songmid)
const index = this.list.findIndex((item) => String(item.songmid) === songmidStr)
if (index !== -1) {
this.list.splice(index, 1)
this.isDirty = true
this.saveToFile()
return true
}
return false
}
/**
* 批量移除歌曲
*/
removeSongs(songmids: (string | number)[]): { removed: number; notFound: number } {
if (!Array.isArray(songmids) || songmids.length === 0) {
return { removed: 0, notFound: 0 }
}
const validSongMids = songmids.filter(
(id) => (id || id === 0) && (typeof id === 'string' || typeof id === 'number')
)
const songMidSet = new Set(validSongMids.map((id) => String(id)))
const initialLength = this.list.length
this.list = this.list.filter((song) => !songMidSet.has(String(song.songmid)))
const removedCount = initialLength - this.list.length
const notFoundCount = validSongMids.length - removedCount
if (removedCount > 0) {
this.isDirty = true
this.saveToFile()
}
return { removed: removedCount, notFound: notFoundCount }
}
/**
* 清空歌单
*/
clearSongs(): void {
if (this.list.length > 0) {
this.list = []
this.isDirty = true
this.saveToFile()
}
}
/**
* 保存到文件
*/
private saveToFile(): void {
if (!this.isDirty) {
return
}
try {
const dir = path.dirname(this.filePath)
PlayListUtils.ensureDirectoryExists(dir)
// 原子性写入:先写临时文件,再重命名
const tempPath = `${this.filePath}.tmp`
const content = JSON.stringify(this.list, null, 2)
fs.writeFileSync(tempPath, content)
fs.renameSync(tempPath, this.filePath)
this.isDirty = false
} catch (error) {
console.error('保存歌单文件失败:', error)
throw new PlayListError(
`保存歌单失败: ${error instanceof Error ? error.message : '未知错误'}`,
'SAVE_FAILED'
)
}
}
/**
* 强制保存到文件
*/
forceSave(): void {
this.isDirty = true
this.saveToFile()
}
/**
* 获取歌曲列表
*/
getSongs(): readonly SongItem[] {
return Object.freeze([...this.list])
}
/**
* 获取歌曲数量
*/
getCount(): number {
return this.list.length
}
/**
* 检查歌曲是否存在
*/
hasSong(songmid: string | number): boolean {
if (!songmid && songmid !== 0) {
return false
}
const songmidStr = String(songmid)
return this.list.some((song) => String(song.songmid) === songmidStr)
}
/**
* 根据songmid获取歌曲
*/
getSong(songmid: string | number): SongItem | null {
if (!songmid && songmid !== 0) {
return null
}
const songmidStr = String(songmid)
return this.list.find((song) => String(song.songmid) === songmidStr) || null
}
/**
* 搜索歌曲
*/
searchSongs(keyword: string): SongItem[] {
if (!keyword || typeof keyword !== 'string') {
return []
}
const lowerKeyword = keyword.toLowerCase()
return this.list.filter(
(song) =>
song.name?.toLowerCase().includes(lowerKeyword) ||
song.singer?.toLowerCase().includes(lowerKeyword) ||
song.albumName?.toLowerCase().includes(lowerKeyword)
)
}
/**
* 获取歌单统计信息
*/
getStatistics(): {
total: number
bySinger: Record<string, number>
byAlbum: Record<string, number>
lastModified: string
} {
const bySinger: Record<string, number> = {}
const byAlbum: Record<string, number> = {}
this.list.forEach((song) => {
// 统计歌手
if (song.singer) {
const singerName = String(song.singer)
bySinger[singerName] = (bySinger[singerName] || 0) + 1
}
// 统计专辑
if (song.albumName) {
const albumName = String(song.albumName)
byAlbum[albumName] = (byAlbum[albumName] || 0) + 1
}
})
return {
total: this.list.length,
bySinger,
byAlbum,
lastModified: new Date().toISOString()
}
}
/**
* 验证歌单完整性
*/
validateIntegrity(): { isValid: boolean; issues: string[] } {
const issues: string[] = []
// 检查文件是否存在
if (!fs.existsSync(this.filePath)) {
issues.push('歌单文件不存在')
}
// 检查数据完整性
const invalidSongs = this.list.filter((song) => !PlayListUtils.isValidSong(song))
if (invalidSongs.length > 0) {
issues.push(`发现 ${invalidSongs.length} 首无效歌曲`)
}
// 检查重复歌曲
const songMids = this.list.map((song) => String(song.songmid))
const uniqueSongMids = new Set(songMids)
if (songMids.length !== uniqueSongMids.size) {
issues.push(`发现 ${songMids.length - uniqueSongMids.size} 首重复歌曲`)
}
return {
isValid: issues.length === 0,
issues
}
}
/**
* 修复歌单数据
*/
repairData(): { fixed: boolean; changes: string[] } {
const changes: string[] = []
let hasChanges = false
// 移除无效歌曲
const validSongs = this.list.filter(PlayListUtils.isValidSong)
if (validSongs.length !== this.list.length) {
changes.push(`移除了 ${this.list.length - validSongs.length} 首无效歌曲`)
this.list = validSongs
hasChanges = true
}
// 去重
const deduplicatedSongs = PlayListUtils.deduplicateSongs(this.list)
if (deduplicatedSongs.length !== this.list.length) {
changes.push(`移除了 ${this.list.length - deduplicatedSongs.length} 首重复歌曲`)
this.list = deduplicatedSongs
hasChanges = true
}
if (hasChanges) {
this.isDirty = true
this.saveToFile()
}
return {
fixed: hasChanges,
changes
}
}
}
// 导出错误类供外部使用
export { PlayListError }

View File

@@ -407,7 +407,6 @@ export default {
return result.list[0].global_collection_id
},
async getUserListDetailByLink({ info }, link) {
let listInfo = info['0']
let total = listInfo.count

View File

@@ -153,14 +153,14 @@ export default {
_types.flac = {
size
}
case 320000:
size = item.h ? sizeFormate(item.h.size) : null
types.push({ type: '320k', size })
_types['320k'] = {
size
}
case 192000:
case 128000:
size = item.l ? sizeFormate(item.l.size) : null

View File

@@ -1,22 +1,25 @@
import electron from 'electron'
function getAppDirPath(name?: "home"
| "appData"
| "assets"
| "userData"
| "sessionData"
| "temp"
| "exe"
| "module"
| "desktop"
| "documents"
| "downloads"
| "music"
| "pictures"
| "videos"
| "recent"
| "logs"
| "crashDumps") {
function getAppDirPath(
name?:
| 'home'
| 'appData'
| 'assets'
| 'userData'
| 'sessionData'
| 'temp'
| 'exe'
| 'module'
| 'desktop'
| 'documents'
| 'downloads'
| 'music'
| 'pictures'
| 'videos'
| 'recent'
| 'logs'
| 'crashDumps'
) {
let dirPath: string = electron.app.getPath(name ?? 'userData')
return dirPath
}

View File

@@ -8,7 +8,7 @@ interface CustomAPI {
close: () => void
setMiniMode: (isMini: boolean) => void
toggleFullscreen: () => void
onMusicCtrl: (callback: (event: Event, args: any) => void) => void
onMusicCtrl: (callback: (event: Event, args: any) => void) => () => void
music: {
request: (api: string, args: any) => Promise<any>
@@ -24,7 +24,37 @@ interface CustomAPI {
getInfo: () => Promise<any>
clear: () => Promise
getSize: () => Promise<string>
},
}
// 歌单管理 API
songList: {
// === 歌单管理 ===
create: (name: string, description?: string, source?: string) => Promise<any>
getAll: () => Promise<any>
getById: (hashId: string) => Promise<any>
delete: (hashId: string) => Promise<any>
batchDelete: (hashIds: string[]) => Promise<any>
edit: (hashId: string, updates: any) => Promise<any>
updateCover: (hashId: string, coverImgUrl: string) => Promise<any>
search: (keyword: string, source?: string) => Promise<any>
getStatistics: () => Promise<any>
exists: (hashId: string) => Promise<any>
// === 歌曲管理 ===
addSongs: (hashId: string, songs: any[]) => Promise<any>
removeSong: (hashId: string, songmid: string | number) => Promise<any>
removeSongs: (hashId: string, songmids: (string | number)[]) => Promise<any>
clearSongs: (hashId: string) => Promise<any>
getSongs: (hashId: string) => Promise<any>
getSongCount: (hashId: string) => Promise<any>
hasSong: (hashId: string, songmid: string | number) => Promise<any>
getSong: (hashId: string, songmid: string | number) => Promise<any>
searchSongs: (hashId: string, keyword: string) => Promise<any>
getSongStatistics: (hashId: string) => Promise<any>
validateIntegrity: (hashId: string) => Promise<any>
repairData: (hashId: string) => Promise<any>
forceSave: (hashId: string) => Promise<any>
}
ai: {
ask: (prompt: string) => Promise<any>
@@ -46,7 +76,7 @@ interface CustomAPI {
}
ping: (callback: Function<any>) => undefined
pingService: {
start: () => undefined,
start: () => undefined
stop: () => undefined
}
// 用户配置API

View File

@@ -21,8 +21,11 @@ const api = {
ipcRenderer.send('window-mini-mode', isMini)
},
toggleFullscreen: () => ipcRenderer.send('window-toggle-fullscreen'),
onMusicCtrl: (callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void) =>
ipcRenderer.on('music-control', callback),
onMusicCtrl: (callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
const handler = (event: Electron.IpcRendererEvent) => callback(event)
ipcRenderer.on('music-control', handler)
return () => ipcRenderer.removeListener('music-control', handler)
},
music: {
request: (api: string, args: any) => ipcRenderer.invoke('service-music-request', api, args),
@@ -67,6 +70,47 @@ const api = {
getSize: () => ipcRenderer.invoke('music-cache:get-size')
},
// 歌单管理 API
songList: {
// === 歌单管理 ===
create: (name: string, description?: string, source?: string) =>
ipcRenderer.invoke('songlist:create', name, description, source),
getAll: () => ipcRenderer.invoke('songlist:get-all'),
getById: (hashId: string) => ipcRenderer.invoke('songlist:get-by-id', hashId),
delete: (hashId: string) => ipcRenderer.invoke('songlist:delete', hashId),
batchDelete: (hashIds: string[]) => ipcRenderer.invoke('songlist:batch-delete', hashIds),
edit: (hashId: string, updates: any) => ipcRenderer.invoke('songlist:edit', hashId, updates),
updateCover: (hashId: string, coverImgUrl: string) =>
ipcRenderer.invoke('songlist:update-cover', hashId, coverImgUrl),
search: (keyword: string, source?: string) =>
ipcRenderer.invoke('songlist:search', keyword, source),
getStatistics: () => ipcRenderer.invoke('songlist:get-statistics'),
exists: (hashId: string) => ipcRenderer.invoke('songlist:exists', hashId),
// === 歌曲管理 ===
addSongs: (hashId: string, songs: any[]) =>
ipcRenderer.invoke('songlist:add-songs', hashId, songs),
removeSong: (hashId: string, songmid: string | number) =>
ipcRenderer.invoke('songlist:remove-song', hashId, songmid),
removeSongs: (hashId: string, songmids: (string | number)[]) =>
ipcRenderer.invoke('songlist:remove-songs', hashId, songmids),
clearSongs: (hashId: string) => ipcRenderer.invoke('songlist:clear-songs', hashId),
getSongs: (hashId: string) => ipcRenderer.invoke('songlist:get-songs', hashId),
getSongCount: (hashId: string) => ipcRenderer.invoke('songlist:get-song-count', hashId),
hasSong: (hashId: string, songmid: string | number) =>
ipcRenderer.invoke('songlist:has-song', hashId, songmid),
getSong: (hashId: string, songmid: string | number) =>
ipcRenderer.invoke('songlist:get-song', hashId, songmid),
searchSongs: (hashId: string, keyword: string) =>
ipcRenderer.invoke('songlist:search-songs', hashId, keyword),
getSongStatistics: (hashId: string) =>
ipcRenderer.invoke('songlist:get-song-statistics', hashId),
validateIntegrity: (hashId: string) =>
ipcRenderer.invoke('songlist:validate-integrity', hashId),
repairData: (hashId: string) => ipcRenderer.invoke('songlist:repair-data', hashId),
forceSave: (hashId: string) => ipcRenderer.invoke('songlist:force-save', hashId)
},
getUserConfig: () => ipcRenderer.invoke('get-user-config'),
// 自动更新相关
@@ -77,42 +121,47 @@ const api = {
// 监听更新事件
onCheckingForUpdate: (callback: () => void) => {
ipcRenderer.on('auto-updater:checking-for-update', callback);
ipcRenderer.on('auto-updater:checking-for-update', callback)
},
onUpdateAvailable: (callback: () => void) => {
ipcRenderer.on('auto-updater:update-available', callback);
ipcRenderer.on('auto-updater:update-available', callback)
},
onUpdateNotAvailable: (callback: () => void) => {
ipcRenderer.on('auto-updater:update-not-available', callback);
ipcRenderer.on('auto-updater:update-not-available', callback)
},
onDownloadProgress: (callback: (progress: any) => void) => {
ipcRenderer.on('auto-updater:download-progress', (_, progress) => callback(progress));
ipcRenderer.on('auto-updater:download-progress', (_, progress) => callback(progress))
},
onUpdateDownloaded: (callback: () => void) => {
ipcRenderer.on('auto-updater:update-downloaded', callback);
ipcRenderer.on('auto-updater:update-downloaded', callback)
},
onError: (callback: (error: string) => void) => {
ipcRenderer.on('auto-updater:error', (_, error) => callback(error));
ipcRenderer.on('auto-updater:error', (_, error) => callback(error))
},
onDownloadStarted: (callback: (updateInfo: any) => void) => {
ipcRenderer.on('auto-updater:download-started', (_, updateInfo) => callback(updateInfo));
ipcRenderer.on('auto-updater:download-started', (_, updateInfo) => callback(updateInfo))
},
// 移除所有监听器
removeAllListeners: () => {
ipcRenderer.removeAllListeners('auto-updater:checking-for-update');
ipcRenderer.removeAllListeners('auto-updater:update-available');
ipcRenderer.removeAllListeners('auto-updater:update-not-available');
ipcRenderer.removeAllListeners('auto-updater:download-started');
ipcRenderer.removeAllListeners('auto-updater:download-progress');
ipcRenderer.removeAllListeners('auto-updater:update-downloaded');
ipcRenderer.removeAllListeners('auto-updater:error');
ipcRenderer.removeAllListeners('auto-updater:checking-for-update')
ipcRenderer.removeAllListeners('auto-updater:update-available')
ipcRenderer.removeAllListeners('auto-updater:update-not-available')
ipcRenderer.removeAllListeners('auto-updater:download-started')
ipcRenderer.removeAllListeners('auto-updater:download-progress')
ipcRenderer.removeAllListeners('auto-updater:update-downloaded')
ipcRenderer.removeAllListeners('auto-updater:error')
}
},
ping: (callbaack: Function) => ipcRenderer.on('song-ended', () => callbaack()),
pingService: {
start: () => { ipcRenderer.send('startPing'); console.log('eventStart') },
stop: () => { ipcRenderer.send('stopPing') }
start: () => {
ipcRenderer.send('startPing')
console.log('eventStart')
},
stop: () => {
ipcRenderer.send('stopPing')
}
}
}

View File

@@ -9,6 +9,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
AIFloatBallSettings: typeof import('./src/components/Settings/AIFloatBallSettings.vue')['default']
AudioVisualizer: typeof import('./src/components/Play/AudioVisualizer.vue')['default']
FloatBall: typeof import('./src/components/AI/FloatBall.vue')['default']
FullPlay: typeof import('./src/components/Play/FullPlay.vue')['default']
GlobalAudio: typeof import('./src/components/Play/GlobalAudio.vue')['default']
@@ -21,8 +22,23 @@ declare module 'vue' {
SearchComponent: typeof import('./src/components/Search/SearchComponent.vue')['default']
ShaderBackground: typeof import('./src/components/Play/ShaderBackground.vue')['default']
SongVirtualList: typeof import('./src/components/Music/SongVirtualList.vue')['default']
TAlert: typeof import('tdesign-vue-next')['Alert']
TAside: typeof import('tdesign-vue-next')['Aside']
TBadge: typeof import('tdesign-vue-next')['Badge']
TButton: typeof import('tdesign-vue-next')['Button']
TContent: typeof import('tdesign-vue-next')['Content']
TDialog: typeof import('tdesign-vue-next')['Dialog']
TDropdown: typeof import('tdesign-vue-next')['Dropdown']
TForm: typeof import('tdesign-vue-next')['Form']
TFormItem: typeof import('tdesign-vue-next')['FormItem']
ThemeSelector: typeof import('./src/components/ThemeSelector.vue')['default']
TImage: typeof import('tdesign-vue-next')['Image']
TInput: typeof import('tdesign-vue-next')['Input']
TitleBarControls: typeof import('./src/components/TitleBarControls.vue')['default']
TLayout: typeof import('tdesign-vue-next')['Layout']
TLoading: typeof import('tdesign-vue-next')['Loading']
TTextarea: typeof import('tdesign-vue-next')['Textarea']
TTooltip: typeof import('tdesign-vue-next')['Tooltip']
UpdateExample: typeof import('./src/components/UpdateExample.vue')['default']
UpdateProgress: typeof import('./src/components/UpdateProgress.vue')['default']
UpdateSettings: typeof import('./src/components/Settings/UpdateSettings.vue')['default']

View File

@@ -2,7 +2,7 @@
<html>
<head lang="zh-CN">
<meta charset="UTF-8" />
<title>Electron</title>
<title>澜音 Music</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- <meta
http-equiv="Content-Security-Policy"

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

View File

@@ -35,7 +35,7 @@ const themes = [
const loadSavedTheme = () => {
const savedTheme = localStorage.getItem('selected-theme')
if (savedTheme && themes.some(t => t.name === savedTheme)) {
if (savedTheme && themes.some((t) => t.name === savedTheme)) {
applyTheme(savedTheme)
}
}
@@ -59,9 +59,11 @@ const applyTheme = (themeName) => {
<template>
<div class="page">
<router-view v-slot="{ Component }">
<Transition :enter-active-class="`animate__animated animate__fadeIn pagesApp`"
:leave-active-class="`animate__animated animate__fadeOut pagesApp`">
<component :is="Component" />
<Transition
:enter-active-class="`animate__animated animate__fadeIn pagesApp`"
:leave-active-class="`animate__animated animate__fadeOut pagesApp`"
>
<component :is="Component" />
</Transition>
</router-view>
<GlobalAudio />
@@ -74,4 +76,4 @@ const applyTheme = (themeName) => {
width: 100vw;
position: fixed;
}
</style>
</style>

View File

@@ -0,0 +1,478 @@
import type {
SongListAPI,
IPCResponse,
BatchOperationResult,
RemoveSongsResult,
SongListStatistics,
SongStatistics,
IntegrityCheckResult,
RepairResult
} from '../../../types/songList'
import type { SongList, Songs } from '@common/types/songList'
// 检查是否在 Electron 环境中
const isElectron = typeof window !== 'undefined' && window.api && window.api.songList
/**
* 歌单管理 API 封装类
*/
class SongListService implements SongListAPI {
private get songListAPI() {
if (!isElectron) {
throw new Error('当前环境不支持 Electron API 调用')
}
return window.api.songList
}
// === 歌单管理方法 ===
/**
* 创建新歌单
*/
async create(
name: string,
description: string = '',
source: SongList['source'] = 'local'
): Promise<IPCResponse<{ id: string }>> {
try {
return await this.songListAPI.create(name, description, source)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '创建歌单失败'
}
}
}
/**
* 获取所有歌单
*/
async getAll(): Promise<IPCResponse<SongList[]>> {
try {
return await this.songListAPI.getAll()
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '获取歌单列表失败'
}
}
}
/**
* 根据ID获取歌单信息
*/
async getById(hashId: string): Promise<IPCResponse<SongList | null>> {
try {
return await this.songListAPI.getById(hashId)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '获取歌单信息失败'
}
}
}
/**
* 删除歌单
*/
async delete(hashId: string): Promise<IPCResponse> {
try {
return await this.songListAPI.delete(hashId)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '删除歌单失败'
}
}
}
/**
* 批量删除歌单
*/
async batchDelete(hashIds: string[]): Promise<IPCResponse<BatchOperationResult>> {
try {
return await this.songListAPI.batchDelete(hashIds)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '批量删除歌单失败'
}
}
}
/**
* 编辑歌单信息
*/
async edit(
hashId: string,
updates: Partial<Omit<SongList, 'id' | 'createTime'>>
): Promise<IPCResponse> {
try {
return await this.songListAPI.edit(hashId, updates)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '编辑歌单失败'
}
}
}
/**
* 更新歌单封面
*/
async updateCover(hashId: string, coverImgUrl: string): Promise<IPCResponse> {
try {
return await this.songListAPI.updateCover(hashId, coverImgUrl)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '更新封面失败'
}
}
}
/**
* 搜索歌单
*/
async search(keyword: string, source?: SongList['source']): Promise<IPCResponse<SongList[]>> {
try {
return await this.songListAPI.search(keyword, source)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '搜索歌单失败'
}
}
}
/**
* 获取歌单统计信息
*/
async getStatistics(): Promise<IPCResponse<SongListStatistics>> {
try {
return await this.songListAPI.getStatistics()
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '获取统计信息失败'
}
}
}
/**
* 检查歌单是否存在
*/
async exists(hashId: string): Promise<IPCResponse<boolean>> {
try {
return await this.songListAPI.exists(hashId)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '检查歌单存在性失败'
}
}
}
// === 歌曲管理方法 ===
/**
* 添加歌曲到歌单
*/
async addSongs(hashId: string, songs: Songs[]): Promise<IPCResponse> {
try {
return await this.songListAPI.addSongs(hashId, songs)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '添加歌曲失败'
}
}
}
/**
* 从歌单移除歌曲
*/
async removeSong(hashId: string, songmid: string | number): Promise<IPCResponse<boolean>> {
try {
return await this.songListAPI.removeSong(hashId, songmid)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '移除歌曲失败'
}
}
}
/**
* 批量移除歌曲
*/
async removeSongs(
hashId: string,
songmids: (string | number)[]
): Promise<IPCResponse<RemoveSongsResult>> {
try {
return await this.songListAPI.removeSongs(hashId, songmids)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '批量移除歌曲失败'
}
}
}
/**
* 清空歌单
*/
async clearSongs(hashId: string): Promise<IPCResponse> {
try {
return await this.songListAPI.clearSongs(hashId)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '清空歌单失败'
}
}
}
/**
* 获取歌单中的歌曲列表
*/
async getSongs(hashId: string): Promise<IPCResponse<readonly Songs[]>> {
try {
return await this.songListAPI.getSongs(hashId)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '获取歌曲列表失败'
}
}
}
/**
* 获取歌单歌曲数量
*/
async getSongCount(hashId: string): Promise<IPCResponse<number>> {
try {
return await this.songListAPI.getSongCount(hashId)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '获取歌曲数量失败'
}
}
}
/**
* 检查歌曲是否在歌单中
*/
async hasSong(hashId: string, songmid: string | number): Promise<IPCResponse<boolean>> {
try {
return await this.songListAPI.hasSong(hashId, songmid)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '检查歌曲存在性失败'
}
}
}
/**
* 根据ID获取歌曲
*/
async getSong(hashId: string, songmid: string | number): Promise<IPCResponse<Songs | null>> {
try {
return await this.songListAPI.getSong(hashId, songmid)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '获取歌曲信息失败'
}
}
}
/**
* 搜索歌单中的歌曲
*/
async searchSongs(hashId: string, keyword: string): Promise<IPCResponse<Songs[]>> {
try {
return await this.songListAPI.searchSongs(hashId, keyword)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '搜索歌曲失败'
}
}
}
/**
* 获取歌单歌曲统计信息
*/
async getSongStatistics(hashId: string): Promise<IPCResponse<SongStatistics>> {
try {
return await this.songListAPI.getSongStatistics(hashId)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '获取歌曲统计信息失败'
}
}
}
/**
* 验证歌单完整性
*/
async validateIntegrity(hashId: string): Promise<IPCResponse<IntegrityCheckResult>> {
try {
return await this.songListAPI.validateIntegrity(hashId)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '验证数据完整性失败'
}
}
}
/**
* 修复歌单数据
*/
async repairData(hashId: string): Promise<IPCResponse<RepairResult>> {
try {
return await this.songListAPI.repairData(hashId)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '修复数据失败'
}
}
}
/**
* 强制保存歌单
*/
async forceSave(hashId: string): Promise<IPCResponse> {
try {
return await this.songListAPI.forceSave(hashId)
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '强制保存失败'
}
}
}
// === 便捷方法 ===
/**
* 创建本地歌单的便捷方法
*/
async createLocal(name: string, description?: string): Promise<IPCResponse<{ id: string }>> {
return this.create(name, description, 'local')
}
/**
* 获取歌单详细信息(包含歌曲列表)
*/
async getPlaylistDetail(hashId: string): Promise<{
playlist: SongList | null
songs: readonly Songs[]
success: boolean
error?: string
}> {
try {
const [playlistRes, songsRes] = await Promise.all([
this.getById(hashId),
this.getSongs(hashId)
])
if (!playlistRes.success) {
return {
playlist: null,
songs: [],
success: false,
error: playlistRes.error
}
}
return {
playlist: playlistRes.data || null,
songs: songsRes.success ? songsRes.data || [] : [],
success: true
}
} catch (error) {
return {
playlist: null,
songs: [],
success: false,
error: error instanceof Error ? error.message : '获取歌单详情失败'
}
}
}
/**
* 安全删除歌单(带确认)
*/
async safeDelete(hashId: string, confirmCallback?: () => Promise<boolean>): Promise<IPCResponse> {
if (confirmCallback) {
const confirmed = await confirmCallback()
if (!confirmed) {
return {
success: false,
error: '用户取消删除操作'
}
}
}
return this.delete(hashId)
}
/**
* 检查并修复歌单数据
*/
async checkAndRepair(hashId: string): Promise<{
needsRepair: boolean
repairResult?: RepairResult
success: boolean
error?: string
}> {
try {
const integrityRes = await this.validateIntegrity(hashId)
if (!integrityRes.success) {
return {
needsRepair: false,
success: false,
error: integrityRes.error
}
}
const { isValid } = integrityRes.data!
if (isValid) {
return {
needsRepair: false,
success: true
}
}
const repairRes = await this.repairData(hashId)
return {
needsRepair: true,
repairResult: repairRes.data,
success: repairRes.success,
error: repairRes.error
}
} catch (error) {
return {
needsRepair: false,
success: false,
error: error instanceof Error ? error.message : '检查修复失败'
}
}
}
}
// 创建单例实例
export const songListAPI = new SongListService()
// 默认导出
export default songListAPI
// 导出类型
export type { SongListAPI, IPCResponse }

View File

@@ -1,8 +1,12 @@
@import './icon_font/iconfont.css';
:root {
--play-bottom-height: max(min(10vh, 86px), 70px);
--play-bottom-height: max(min(10vh, 80px), 70px);
}
@font-face {
font-family: 'lyricfont';
src: url('./lyricfont.ttf');
font-weight: 500;
}
*,
*::before,
*::after {

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

View File

@@ -1,4 +1,3 @@
:root,
:root[theme-mode='light'] {
--td-brand-color-1: #e2fae2;

View File

@@ -1,355 +1,379 @@
:root[theme-mode="blue"] {
--td-brand-color-1: #ecf4ff;
--td-brand-color-2: #cde5ff;
--td-brand-color-3: #9aceff;
--td-brand-color-4: #57b4ff;
--td-brand-color-5: #3198e2;
--td-brand-color-6: #007dc5;
--td-brand-color-7: #00629c;
--td-brand-color-8: #004a77;
--td-brand-color-9: #003355;
--td-brand-color-10: #00213a;
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-3);
--td-brand-color: var(--td-brand-color-4);
--td-brand-color-active: var(--td-brand-color-5);
--td-warning-color-1: #fef3e6;
--td-warning-color-2: #f9e0c7;
--td-warning-color-3: #f7c797;
--td-warning-color-4: #f2995f;
--td-warning-color-5: #ed7b2f;
--td-warning-color-6: #d35a21;
--td-warning-color-7: #ba431b;
--td-warning-color-8: #9e3610;
--td-warning-color-9: #842b0b;
--td-warning-color-10: #5a1907;
--td-warning-color: var(--td-warning-color-5);
--td-warning-color-hover: var(--td-warning-color-4);
--td-warning-color-focus: var(--td-warning-color-2);
--td-warning-color-active: var(--td-warning-color-6);
--td-warning-color-disabled: var(--td-warning-color-3);
--td-warning-color-light: var(--td-warning-color-1);
--td-error-color-1: #fdecee;
--td-error-color-2: #f9d7d9;
--td-error-color-3: #f8b9be;
--td-error-color-4: #f78d94;
--td-error-color-5: #f36d78;
--td-error-color-6: #e34d59;
--td-error-color-7: #c9353f;
--td-error-color-8: #b11f26;
--td-error-color-9: #951114;
--td-error-color-10: #680506;
--td-error-color: var(--td-error-color-6);
--td-error-color-hover: var(--td-error-color-5);
--td-error-color-focus: var(--td-error-color-2);
--td-error-color-active: var(--td-error-color-7);
--td-error-color-disabled: var(--td-error-color-3);
--td-error-color-light: var(--td-error-color-1);
--td-success-color-1: #e8f8f2;
--td-success-color-2: #bcebdc;
--td-success-color-3: #85dbbe;
--td-success-color-4: #48c79c;
--td-success-color-5: #00a870;
--td-success-color-6: #078d5c;
--td-success-color-7: #067945;
--td-success-color-8: #056334;
--td-success-color-9: #044f2a;
--td-success-color-10: #033017;
--td-success-color: var(--td-success-color-5);
--td-success-color-hover: var(--td-success-color-4);
--td-success-color-focus: var(--td-success-color-2);
--td-success-color-active: var(--td-success-color-6);
--td-success-color-disabled: var(--td-success-color-3);
--td-success-color-light: var(--td-success-color-1);
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-container: #fff;
--td-bg-color-container-select: #fff;
--td-bg-color-page: var(--td-gray-color-2);
--td-bg-color-container-hover: var(--td-gray-color-1);
--td-bg-color-container-active: var(--td-gray-color-3);
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
--td-bg-color-component: var(--td-gray-color-3);
--td-bg-color-component-hover: var(--td-gray-color-4);
--td-bg-color-component-active: var(--td-gray-color-6);
--td-bg-color-component-disabled: var(--td-gray-color-2);
--td-component-stroke: var(--td-gray-color-3);
--td-component-border: var(--td-gray-color-4);
--td-font-white-1: #ffffff;
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-gray-1);
--td-text-color-secondary: var(--td-font-gray-2);
--td-text-color-placeholder: var(--td-font-gray-3);
--td-text-color-disabled: var(--td-font-gray-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-brand-color-light-hover: var(--td-brand-color-2);
--td-warning-color-light-hover: var(--td-warning-color-2);
--td-error-color-light-hover: var(--td-error-color-2);
--td-success-color-light-hover: var(--td-success-color-2);
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
--td-table-shadow-color: rgba(0, 0, 0, 8%);
--td-scrollbar-color: rgba(0, 0, 0, 10%);
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
--td-scroll-track-color: #fff;
--td-bg-color-specialcomponent: #fff;
--td-border-level-1-color: var(--td-gray-color-3);
--td-border-level-2-color: var(--td-gray-color-4);
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%), 0 5px 5px -3px rgba(0, 0, 0, 10%);
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%), 0 8px 10px -5px rgba(0, 0, 0, 8%);
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
--td-mask-active: rgba(0, 0, 0, 0.6);
--td-mask-disabled: rgba(255, 255, 255, 0.6);
:root[theme-mode='blue'] {
--td-brand-color-1: #ecf4ff;
--td-brand-color-2: #cde5ff;
--td-brand-color-3: #9aceff;
--td-brand-color-4: #57b4ff;
--td-brand-color-5: #3198e2;
--td-brand-color-6: #007dc5;
--td-brand-color-7: #00629c;
--td-brand-color-8: #004a77;
--td-brand-color-9: #003355;
--td-brand-color-10: #00213a;
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-3);
--td-brand-color: var(--td-brand-color-4);
--td-brand-color-active: var(--td-brand-color-5);
--td-warning-color-1: #fef3e6;
--td-warning-color-2: #f9e0c7;
--td-warning-color-3: #f7c797;
--td-warning-color-4: #f2995f;
--td-warning-color-5: #ed7b2f;
--td-warning-color-6: #d35a21;
--td-warning-color-7: #ba431b;
--td-warning-color-8: #9e3610;
--td-warning-color-9: #842b0b;
--td-warning-color-10: #5a1907;
--td-warning-color: var(--td-warning-color-5);
--td-warning-color-hover: var(--td-warning-color-4);
--td-warning-color-focus: var(--td-warning-color-2);
--td-warning-color-active: var(--td-warning-color-6);
--td-warning-color-disabled: var(--td-warning-color-3);
--td-warning-color-light: var(--td-warning-color-1);
--td-error-color-1: #fdecee;
--td-error-color-2: #f9d7d9;
--td-error-color-3: #f8b9be;
--td-error-color-4: #f78d94;
--td-error-color-5: #f36d78;
--td-error-color-6: #e34d59;
--td-error-color-7: #c9353f;
--td-error-color-8: #b11f26;
--td-error-color-9: #951114;
--td-error-color-10: #680506;
--td-error-color: var(--td-error-color-6);
--td-error-color-hover: var(--td-error-color-5);
--td-error-color-focus: var(--td-error-color-2);
--td-error-color-active: var(--td-error-color-7);
--td-error-color-disabled: var(--td-error-color-3);
--td-error-color-light: var(--td-error-color-1);
--td-success-color-1: #e8f8f2;
--td-success-color-2: #bcebdc;
--td-success-color-3: #85dbbe;
--td-success-color-4: #48c79c;
--td-success-color-5: #00a870;
--td-success-color-6: #078d5c;
--td-success-color-7: #067945;
--td-success-color-8: #056334;
--td-success-color-9: #044f2a;
--td-success-color-10: #033017;
--td-success-color: var(--td-success-color-5);
--td-success-color-hover: var(--td-success-color-4);
--td-success-color-focus: var(--td-success-color-2);
--td-success-color-active: var(--td-success-color-6);
--td-success-color-disabled: var(--td-success-color-3);
--td-success-color-light: var(--td-success-color-1);
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-container: #fff;
--td-bg-color-container-select: #fff;
--td-bg-color-page: var(--td-gray-color-2);
--td-bg-color-container-hover: var(--td-gray-color-1);
--td-bg-color-container-active: var(--td-gray-color-3);
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
--td-bg-color-component: var(--td-gray-color-3);
--td-bg-color-component-hover: var(--td-gray-color-4);
--td-bg-color-component-active: var(--td-gray-color-6);
--td-bg-color-component-disabled: var(--td-gray-color-2);
--td-component-stroke: var(--td-gray-color-3);
--td-component-border: var(--td-gray-color-4);
--td-font-white-1: #ffffff;
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-gray-1);
--td-text-color-secondary: var(--td-font-gray-2);
--td-text-color-placeholder: var(--td-font-gray-3);
--td-text-color-disabled: var(--td-font-gray-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-brand-color-light-hover: var(--td-brand-color-2);
--td-warning-color-light-hover: var(--td-warning-color-2);
--td-error-color-light-hover: var(--td-error-color-2);
--td-success-color-light-hover: var(--td-success-color-2);
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
--td-table-shadow-color: rgba(0, 0, 0, 8%);
--td-scrollbar-color: rgba(0, 0, 0, 10%);
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
--td-scroll-track-color: #fff;
--td-bg-color-specialcomponent: #fff;
--td-border-level-1-color: var(--td-gray-color-3);
--td-border-level-2-color: var(--td-gray-color-4);
--td-shadow-1:
0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
--td-shadow-2:
0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%),
0 5px 5px -3px rgba(0, 0, 0, 10%);
--td-shadow-3:
0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%),
0 8px 10px -5px rgba(0, 0, 0, 8%);
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
--td-mask-active: rgba(0, 0, 0, 0.6);
--td-mask-disabled: rgba(255, 255, 255, 0.6);
}
:root[theme-mode="dark"] {
--td-brand-color-1: #3198e220;
--td-brand-color-2: #003355;
--td-brand-color-3: #004a77;
--td-brand-color-4: #00629c;
--td-brand-color-5: #007dc5;
--td-brand-color-6: #3198e2;
--td-brand-color-7: #53b1fd;
--td-brand-color-8: #9aceff;
--td-brand-color-9: #cde5ff;
--td-brand-color-10: #ecf4ff;
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-5);
--td-brand-color: var(--td-brand-color-6);
--td-brand-color-active: var(--td-brand-color-7);
--td-warning-color-1: #4f2a1d;
--td-warning-color-2: #582f21;
--td-warning-color-3: #733c23;
--td-warning-color-4: #a75d2b;
--td-warning-color-5: #cf6e2d;
--td-warning-color-6: #dc7633;
--td-warning-color-7: #e8935c;
--td-warning-color-8: #ecbf91;
--td-warning-color-9: #eed7bf;
--td-warning-color-10: #f3e9dc;
--td-error-color-1: #472324;
--td-error-color-2: #5e2a2d;
--td-error-color-3: #703439;
--td-error-color-4: #83383e;
--td-error-color-5: #a03f46;
--td-error-color-6: #c64751;
--td-error-color-7: #de6670;
--td-error-color-8: #ec888e;
--td-error-color-9: #edb1b6;
--td-error-color-10: #eeced0;
--td-success-color-1: #193a2a;
--td-success-color-2: #1a4230;
--td-success-color-3: #17533d;
--td-success-color-4: #0d7a55;
--td-success-color-5: #059465;
--td-success-color-6: #43af8a;
--td-success-color-7: #46bf96;
--td-success-color-8: #80d2b6;
--td-success-color-9: #b4e1d3;
--td-success-color-10: #deede8;
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-page: var(--td-gray-color-14);
--td-bg-color-container: var(--td-gray-color-13);
--td-bg-color-container-hover: var(--td-gray-color-12);
--td-bg-color-container-active: var(--td-gray-color-10);
--td-bg-color-container-select: var(--td-gray-color-9);
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
--td-bg-color-component: var(--td-gray-color-11);
--td-bg-color-component-hover: var(--td-gray-color-10);
--td-bg-color-component-active: var(--td-gray-color-9);
--td-bg-color-component-disabled: var(--td-gray-color-12);
--td-component-stroke: var(--td-gray-color-11);
--td-component-border: var(--td-gray-color-9);
--td-font-white-1: rgba(255, 255, 255, 0.9);
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-white-1);
--td-text-color-secondary: var(--td-font-white-2);
--td-text-color-placeholder: var(--td-font-white-3);
--td-text-color-disabled: var(--td-font-white-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-shadow-1: 0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
--td-shadow-2: 0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.10), 0 5px 5px rgba(0, 0, 0, 0.16);
--td-shadow-3: 0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.20);
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
--td-table-shadow-color: rgba(0, 0, 0, 55%);
--td-scrollbar-color: rgba(255, 255, 255, 10%);
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
--td-scroll-track-color: #333;
--td-bg-color-specialcomponent: transparent;
--td-border-level-1-color: var(--td-gray-color-11);
--td-border-level-2-color: var(--td-gray-color-9);
--td-mask-active: rgba(0, 0, 0, 0.4);
--td-mask-disabled: rgba(0, 0, 0, 0.6);
:root[theme-mode='dark'] {
--td-brand-color-1: #3198e220;
--td-brand-color-2: #003355;
--td-brand-color-3: #004a77;
--td-brand-color-4: #00629c;
--td-brand-color-5: #007dc5;
--td-brand-color-6: #3198e2;
--td-brand-color-7: #53b1fd;
--td-brand-color-8: #9aceff;
--td-brand-color-9: #cde5ff;
--td-brand-color-10: #ecf4ff;
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-5);
--td-brand-color: var(--td-brand-color-6);
--td-brand-color-active: var(--td-brand-color-7);
--td-warning-color-1: #4f2a1d;
--td-warning-color-2: #582f21;
--td-warning-color-3: #733c23;
--td-warning-color-4: #a75d2b;
--td-warning-color-5: #cf6e2d;
--td-warning-color-6: #dc7633;
--td-warning-color-7: #e8935c;
--td-warning-color-8: #ecbf91;
--td-warning-color-9: #eed7bf;
--td-warning-color-10: #f3e9dc;
--td-error-color-1: #472324;
--td-error-color-2: #5e2a2d;
--td-error-color-3: #703439;
--td-error-color-4: #83383e;
--td-error-color-5: #a03f46;
--td-error-color-6: #c64751;
--td-error-color-7: #de6670;
--td-error-color-8: #ec888e;
--td-error-color-9: #edb1b6;
--td-error-color-10: #eeced0;
--td-success-color-1: #193a2a;
--td-success-color-2: #1a4230;
--td-success-color-3: #17533d;
--td-success-color-4: #0d7a55;
--td-success-color-5: #059465;
--td-success-color-6: #43af8a;
--td-success-color-7: #46bf96;
--td-success-color-8: #80d2b6;
--td-success-color-9: #b4e1d3;
--td-success-color-10: #deede8;
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-page: var(--td-gray-color-14);
--td-bg-color-container: var(--td-gray-color-13);
--td-bg-color-container-hover: var(--td-gray-color-12);
--td-bg-color-container-active: var(--td-gray-color-10);
--td-bg-color-container-select: var(--td-gray-color-9);
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
--td-bg-color-component: var(--td-gray-color-11);
--td-bg-color-component-hover: var(--td-gray-color-10);
--td-bg-color-component-active: var(--td-gray-color-9);
--td-bg-color-component-disabled: var(--td-gray-color-12);
--td-component-stroke: var(--td-gray-color-11);
--td-component-border: var(--td-gray-color-9);
--td-font-white-1: rgba(255, 255, 255, 0.9);
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-white-1);
--td-text-color-secondary: var(--td-font-white-2);
--td-text-color-placeholder: var(--td-font-white-3);
--td-text-color-disabled: var(--td-font-white-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-shadow-1:
0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
--td-shadow-2:
0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.1), 0 5px 5px rgba(0, 0, 0, 0.16);
--td-shadow-3:
0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.2);
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
--td-table-shadow-color: rgba(0, 0, 0, 55%);
--td-scrollbar-color: rgba(255, 255, 255, 10%);
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
--td-scroll-track-color: #333;
--td-bg-color-specialcomponent: transparent;
--td-border-level-1-color: var(--td-gray-color-11);
--td-border-level-2-color: var(--td-gray-color-9);
--td-mask-active: rgba(0, 0, 0, 0.4);
--td-mask-disabled: rgba(0, 0, 0, 0.6);
}
:root {
--td-font-family: pingfang sc, microsoft yahei, arial regular;
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
--td-font-size-link-small: 12px;
--td-font-size-link-medium: 14px;
--td-font-size-link-large: 16px;
--td-font-size-mark-small: 12px;
--td-font-size-mark-medium: 14px;
--td-font-size-body-small: 12px;
--td-font-size-body-medium: 14px;
--td-font-size-body-large: 16px;
--td-font-size-title-small: 14px;
--td-font-size-title-medium: 16px;
--td-font-size-title-large: 20px;
--td-font-size-headline-small: 24px;
--td-font-size-headline-medium: 28px;
--td-font-size-headline-large: 36px;
--td-font-size-display-medium: 48px;
--td-font-size-display-large: 64px;
--td-line-height-link-small: 20px;
--td-line-height-link-medium: 22px;
--td-line-height-link-large: 24px;
--td-line-height-mark-small: 20px;
--td-line-height-mark-medium: 22px;
--td-line-height-body-small: 20px;
--td-line-height-body-medium: 22px;
--td-line-height-body-large: 24px;
--td-line-height-title-small: 22px;
--td-line-height-title-medium: 24px;
--td-line-height-title-large: 28px;
--td-line-height-headline-small: 32px;
--td-line-height-headline-medium: 36px;
--td-line-height-headline-large: 44px;
--td-line-height-display-medium: 56px;
--td-line-height-display-large: 72px;
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small) var(--td-font-family);
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium) var(--td-font-family);
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large) var(--td-font-family);
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small) var(--td-font-family);
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium) var(--td-font-family);
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small) var(--td-font-family);
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium) var(--td-font-family);
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large) var(--td-font-family);
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small) var(--td-font-family);
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium) var(--td-font-family);
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large) var(--td-font-family);
--td-font-headline-small: 600 var(--td-font-size-headline-small) / var(--td-line-height-headline-small) var(--td-font-family);
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) / var(--td-line-height-headline-medium) var(--td-font-family);
--td-font-headline-large: 600 var(--td-font-size-headline-large) / var(--td-line-height-headline-large) var(--td-font-family);
--td-font-display-medium: 600 var(--td-font-size-display-medium) / var(--td-line-height-display-medium) var(--td-font-family);
--td-font-display-large: 600 var(--td-font-size-display-large) / var(--td-line-height-display-large) var(--td-font-family);
--td-radius-small: 2px;
--td-radius-default: 3px;
--td-radius-medium: 6px;
--td-radius-large: 9px;
--td-radius-extraLarge: 12px;
--td-radius-round: 999px;
--td-radius-circle: 50%;
--td-size-1: 2px;
--td-size-2: 4px;
--td-size-3: 6px;
--td-size-4: 8px;
--td-size-5: 12px;
--td-size-6: 16px;
--td-size-7: 20px;
--td-size-8: 24px;
--td-size-9: 28px;
--td-size-10: 32px;
--td-size-11: 36px;
--td-size-12: 40px;
--td-size-13: 48px;
--td-size-14: 56px;
--td-size-15: 64px;
--td-size-16: 72px;
--td-comp-size-xxxs: var(--td-size-6);
--td-comp-size-xxs: var(--td-size-7);
--td-comp-size-xs: var(--td-size-8);
--td-comp-size-s: var(--td-size-9);
--td-comp-size-m: var(--td-size-10);
--td-comp-size-l: var(--td-size-11);
--td-comp-size-xl: var(--td-size-12);
--td-comp-size-xxl: var(--td-size-13);
--td-comp-size-xxxl: var(--td-size-14);
--td-comp-size-xxxxl: var(--td-size-15);
--td-comp-size-xxxxxl: var(--td-size-16);
--td-pop-padding-s: var(--td-size-2);
--td-pop-padding-m: var(--td-size-3);
--td-pop-padding-l: var(--td-size-4);
--td-pop-padding-xl: var(--td-size-5);
--td-pop-padding-xxl: var(--td-size-6);
--td-comp-paddingLR-xxs: var(--td-size-1);
--td-comp-paddingLR-xs: var(--td-size-2);
--td-comp-paddingLR-s: var(--td-size-4);
--td-comp-paddingLR-m: var(--td-size-5);
--td-comp-paddingLR-l: var(--td-size-6);
--td-comp-paddingLR-xl: var(--td-size-8);
--td-comp-paddingLR-xxl: var(--td-size-10);
--td-comp-paddingTB-xxs: var(--td-size-1);
--td-comp-paddingTB-xs: var(--td-size-2);
--td-comp-paddingTB-s: var(--td-size-4);
--td-comp-paddingTB-m: var(--td-size-5);
--td-comp-paddingTB-l: var(--td-size-6);
--td-comp-paddingTB-xl: var(--td-size-8);
--td-comp-paddingTB-xxl: var(--td-size-10);
--td-comp-margin-xxs: var(--td-size-1);
--td-comp-margin-xs: var(--td-size-2);
--td-comp-margin-s: var(--td-size-4);
--td-comp-margin-m: var(--td-size-5);
--td-comp-margin-l: var(--td-size-6);
--td-comp-margin-xl: var(--td-size-7);
--td-comp-margin-xxl: var(--td-size-8);
--td-comp-margin-xxxl: var(--td-size-10);
--td-comp-margin-xxxxl: var(--td-size-12);
}
--td-font-family: pingfang sc, microsoft yahei, arial regular;
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
--td-font-size-link-small: 12px;
--td-font-size-link-medium: 14px;
--td-font-size-link-large: 16px;
--td-font-size-mark-small: 12px;
--td-font-size-mark-medium: 14px;
--td-font-size-body-small: 12px;
--td-font-size-body-medium: 14px;
--td-font-size-body-large: 16px;
--td-font-size-title-small: 14px;
--td-font-size-title-medium: 16px;
--td-font-size-title-large: 20px;
--td-font-size-headline-small: 24px;
--td-font-size-headline-medium: 28px;
--td-font-size-headline-large: 36px;
--td-font-size-display-medium: 48px;
--td-font-size-display-large: 64px;
--td-line-height-link-small: 20px;
--td-line-height-link-medium: 22px;
--td-line-height-link-large: 24px;
--td-line-height-mark-small: 20px;
--td-line-height-mark-medium: 22px;
--td-line-height-body-small: 20px;
--td-line-height-body-medium: 22px;
--td-line-height-body-large: 24px;
--td-line-height-title-small: 22px;
--td-line-height-title-medium: 24px;
--td-line-height-title-large: 28px;
--td-line-height-headline-small: 32px;
--td-line-height-headline-medium: 36px;
--td-line-height-headline-large: 44px;
--td-line-height-display-medium: 56px;
--td-line-height-display-large: 72px;
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small)
var(--td-font-family);
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium)
var(--td-font-family);
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large)
var(--td-font-family);
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small)
var(--td-font-family);
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium)
var(--td-font-family);
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small)
var(--td-font-family);
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium)
var(--td-font-family);
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large)
var(--td-font-family);
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small)
var(--td-font-family);
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium)
var(--td-font-family);
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large)
var(--td-font-family);
--td-font-headline-small: 600 var(--td-font-size-headline-small) /
var(--td-line-height-headline-small) var(--td-font-family);
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) /
var(--td-line-height-headline-medium) var(--td-font-family);
--td-font-headline-large: 600 var(--td-font-size-headline-large) /
var(--td-line-height-headline-large) var(--td-font-family);
--td-font-display-medium: 600 var(--td-font-size-display-medium) /
var(--td-line-height-display-medium) var(--td-font-family);
--td-font-display-large: 600 var(--td-font-size-display-large) /
var(--td-line-height-display-large) var(--td-font-family);
--td-radius-small: 2px;
--td-radius-default: 3px;
--td-radius-medium: 6px;
--td-radius-large: 9px;
--td-radius-extraLarge: 12px;
--td-radius-round: 999px;
--td-radius-circle: 50%;
--td-size-1: 2px;
--td-size-2: 4px;
--td-size-3: 6px;
--td-size-4: 8px;
--td-size-5: 12px;
--td-size-6: 16px;
--td-size-7: 20px;
--td-size-8: 24px;
--td-size-9: 28px;
--td-size-10: 32px;
--td-size-11: 36px;
--td-size-12: 40px;
--td-size-13: 48px;
--td-size-14: 56px;
--td-size-15: 64px;
--td-size-16: 72px;
--td-comp-size-xxxs: var(--td-size-6);
--td-comp-size-xxs: var(--td-size-7);
--td-comp-size-xs: var(--td-size-8);
--td-comp-size-s: var(--td-size-9);
--td-comp-size-m: var(--td-size-10);
--td-comp-size-l: var(--td-size-11);
--td-comp-size-xl: var(--td-size-12);
--td-comp-size-xxl: var(--td-size-13);
--td-comp-size-xxxl: var(--td-size-14);
--td-comp-size-xxxxl: var(--td-size-15);
--td-comp-size-xxxxxl: var(--td-size-16);
--td-pop-padding-s: var(--td-size-2);
--td-pop-padding-m: var(--td-size-3);
--td-pop-padding-l: var(--td-size-4);
--td-pop-padding-xl: var(--td-size-5);
--td-pop-padding-xxl: var(--td-size-6);
--td-comp-paddingLR-xxs: var(--td-size-1);
--td-comp-paddingLR-xs: var(--td-size-2);
--td-comp-paddingLR-s: var(--td-size-4);
--td-comp-paddingLR-m: var(--td-size-5);
--td-comp-paddingLR-l: var(--td-size-6);
--td-comp-paddingLR-xl: var(--td-size-8);
--td-comp-paddingLR-xxl: var(--td-size-10);
--td-comp-paddingTB-xxs: var(--td-size-1);
--td-comp-paddingTB-xs: var(--td-size-2);
--td-comp-paddingTB-s: var(--td-size-4);
--td-comp-paddingTB-m: var(--td-size-5);
--td-comp-paddingTB-l: var(--td-size-6);
--td-comp-paddingTB-xl: var(--td-size-8);
--td-comp-paddingTB-xxl: var(--td-size-10);
--td-comp-margin-xxs: var(--td-size-1);
--td-comp-margin-xs: var(--td-size-2);
--td-comp-margin-s: var(--td-size-4);
--td-comp-margin-m: var(--td-size-5);
--td-comp-margin-l: var(--td-size-6);
--td-comp-margin-xl: var(--td-size-7);
--td-comp-margin-xxl: var(--td-size-8);
--td-comp-margin-xxxl: var(--td-size-10);
--td-comp-margin-xxxxl: var(--td-size-12);
}

View File

@@ -1,355 +1,379 @@
:root[theme-mode="cyan"] {
--td-brand-color-1: #e3fcf8;
--td-brand-color-2: #beefe9;
--td-brand-color-3: #86dad1;
--td-brand-color-4: #3ac2b8;
--td-brand-color-5: #00a59b;
--td-brand-color-6: #00877e;
--td-brand-color-7: #006b64;
--td-brand-color-8: #00524c;
--td-brand-color-9: #003b36;
--td-brand-color-10: #002724;
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-3);
--td-brand-color: var(--td-brand-color-4);
--td-brand-color-active: var(--td-brand-color-5);
--td-warning-color-1: #fef3e6;
--td-warning-color-2: #f9e0c7;
--td-warning-color-3: #f7c797;
--td-warning-color-4: #f2995f;
--td-warning-color-5: #ed7b2f;
--td-warning-color-6: #d35a21;
--td-warning-color-7: #ba431b;
--td-warning-color-8: #9e3610;
--td-warning-color-9: #842b0b;
--td-warning-color-10: #5a1907;
--td-warning-color: var(--td-warning-color-5);
--td-warning-color-hover: var(--td-warning-color-4);
--td-warning-color-focus: var(--td-warning-color-2);
--td-warning-color-active: var(--td-warning-color-6);
--td-warning-color-disabled: var(--td-warning-color-3);
--td-warning-color-light: var(--td-warning-color-1);
--td-error-color-1: #fdecee;
--td-error-color-2: #f9d7d9;
--td-error-color-3: #f8b9be;
--td-error-color-4: #f78d94;
--td-error-color-5: #f36d78;
--td-error-color-6: #e34d59;
--td-error-color-7: #c9353f;
--td-error-color-8: #b11f26;
--td-error-color-9: #951114;
--td-error-color-10: #680506;
--td-error-color: var(--td-error-color-6);
--td-error-color-hover: var(--td-error-color-5);
--td-error-color-focus: var(--td-error-color-2);
--td-error-color-active: var(--td-error-color-7);
--td-error-color-disabled: var(--td-error-color-3);
--td-error-color-light: var(--td-error-color-1);
--td-success-color-1: #e8f8f2;
--td-success-color-2: #bcebdc;
--td-success-color-3: #85dbbe;
--td-success-color-4: #48c79c;
--td-success-color-5: #00a870;
--td-success-color-6: #078d5c;
--td-success-color-7: #067945;
--td-success-color-8: #056334;
--td-success-color-9: #044f2a;
--td-success-color-10: #033017;
--td-success-color: var(--td-success-color-5);
--td-success-color-hover: var(--td-success-color-4);
--td-success-color-focus: var(--td-success-color-2);
--td-success-color-active: var(--td-success-color-6);
--td-success-color-disabled: var(--td-success-color-3);
--td-success-color-light: var(--td-success-color-1);
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-container: #fff;
--td-bg-color-container-select: #fff;
--td-bg-color-page: var(--td-gray-color-2);
--td-bg-color-container-hover: var(--td-gray-color-1);
--td-bg-color-container-active: var(--td-gray-color-3);
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
--td-bg-color-component: var(--td-gray-color-3);
--td-bg-color-component-hover: var(--td-gray-color-4);
--td-bg-color-component-active: var(--td-gray-color-6);
--td-bg-color-component-disabled: var(--td-gray-color-2);
--td-component-stroke: var(--td-gray-color-3);
--td-component-border: var(--td-gray-color-4);
--td-font-white-1: #ffffff;
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-gray-1);
--td-text-color-secondary: var(--td-font-gray-2);
--td-text-color-placeholder: var(--td-font-gray-3);
--td-text-color-disabled: var(--td-font-gray-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-brand-color-light-hover: var(--td-brand-color-2);
--td-warning-color-light-hover: var(--td-warning-color-2);
--td-error-color-light-hover: var(--td-error-color-2);
--td-success-color-light-hover: var(--td-success-color-2);
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
--td-table-shadow-color: rgba(0, 0, 0, 8%);
--td-scrollbar-color: rgba(0, 0, 0, 10%);
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
--td-scroll-track-color: #fff;
--td-bg-color-specialcomponent: #fff;
--td-border-level-1-color: var(--td-gray-color-3);
--td-border-level-2-color: var(--td-gray-color-4);
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%), 0 5px 5px -3px rgba(0, 0, 0, 10%);
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%), 0 8px 10px -5px rgba(0, 0, 0, 8%);
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
--td-mask-active: rgba(0, 0, 0, 0.6);
--td-mask-disabled: rgba(255, 255, 255, 0.6);
:root[theme-mode='cyan'] {
--td-brand-color-1: #e3fcf8;
--td-brand-color-2: #beefe9;
--td-brand-color-3: #86dad1;
--td-brand-color-4: #3ac2b8;
--td-brand-color-5: #00a59b;
--td-brand-color-6: #00877e;
--td-brand-color-7: #006b64;
--td-brand-color-8: #00524c;
--td-brand-color-9: #003b36;
--td-brand-color-10: #002724;
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-3);
--td-brand-color: var(--td-brand-color-4);
--td-brand-color-active: var(--td-brand-color-5);
--td-warning-color-1: #fef3e6;
--td-warning-color-2: #f9e0c7;
--td-warning-color-3: #f7c797;
--td-warning-color-4: #f2995f;
--td-warning-color-5: #ed7b2f;
--td-warning-color-6: #d35a21;
--td-warning-color-7: #ba431b;
--td-warning-color-8: #9e3610;
--td-warning-color-9: #842b0b;
--td-warning-color-10: #5a1907;
--td-warning-color: var(--td-warning-color-5);
--td-warning-color-hover: var(--td-warning-color-4);
--td-warning-color-focus: var(--td-warning-color-2);
--td-warning-color-active: var(--td-warning-color-6);
--td-warning-color-disabled: var(--td-warning-color-3);
--td-warning-color-light: var(--td-warning-color-1);
--td-error-color-1: #fdecee;
--td-error-color-2: #f9d7d9;
--td-error-color-3: #f8b9be;
--td-error-color-4: #f78d94;
--td-error-color-5: #f36d78;
--td-error-color-6: #e34d59;
--td-error-color-7: #c9353f;
--td-error-color-8: #b11f26;
--td-error-color-9: #951114;
--td-error-color-10: #680506;
--td-error-color: var(--td-error-color-6);
--td-error-color-hover: var(--td-error-color-5);
--td-error-color-focus: var(--td-error-color-2);
--td-error-color-active: var(--td-error-color-7);
--td-error-color-disabled: var(--td-error-color-3);
--td-error-color-light: var(--td-error-color-1);
--td-success-color-1: #e8f8f2;
--td-success-color-2: #bcebdc;
--td-success-color-3: #85dbbe;
--td-success-color-4: #48c79c;
--td-success-color-5: #00a870;
--td-success-color-6: #078d5c;
--td-success-color-7: #067945;
--td-success-color-8: #056334;
--td-success-color-9: #044f2a;
--td-success-color-10: #033017;
--td-success-color: var(--td-success-color-5);
--td-success-color-hover: var(--td-success-color-4);
--td-success-color-focus: var(--td-success-color-2);
--td-success-color-active: var(--td-success-color-6);
--td-success-color-disabled: var(--td-success-color-3);
--td-success-color-light: var(--td-success-color-1);
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-container: #fff;
--td-bg-color-container-select: #fff;
--td-bg-color-page: var(--td-gray-color-2);
--td-bg-color-container-hover: var(--td-gray-color-1);
--td-bg-color-container-active: var(--td-gray-color-3);
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
--td-bg-color-component: var(--td-gray-color-3);
--td-bg-color-component-hover: var(--td-gray-color-4);
--td-bg-color-component-active: var(--td-gray-color-6);
--td-bg-color-component-disabled: var(--td-gray-color-2);
--td-component-stroke: var(--td-gray-color-3);
--td-component-border: var(--td-gray-color-4);
--td-font-white-1: #ffffff;
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-gray-1);
--td-text-color-secondary: var(--td-font-gray-2);
--td-text-color-placeholder: var(--td-font-gray-3);
--td-text-color-disabled: var(--td-font-gray-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-brand-color-light-hover: var(--td-brand-color-2);
--td-warning-color-light-hover: var(--td-warning-color-2);
--td-error-color-light-hover: var(--td-error-color-2);
--td-success-color-light-hover: var(--td-success-color-2);
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
--td-table-shadow-color: rgba(0, 0, 0, 8%);
--td-scrollbar-color: rgba(0, 0, 0, 10%);
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
--td-scroll-track-color: #fff;
--td-bg-color-specialcomponent: #fff;
--td-border-level-1-color: var(--td-gray-color-3);
--td-border-level-2-color: var(--td-gray-color-4);
--td-shadow-1:
0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
--td-shadow-2:
0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%),
0 5px 5px -3px rgba(0, 0, 0, 10%);
--td-shadow-3:
0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%),
0 8px 10px -5px rgba(0, 0, 0, 8%);
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
--td-mask-active: rgba(0, 0, 0, 0.6);
--td-mask-disabled: rgba(255, 255, 255, 0.6);
}
:root[theme-mode="dark"] {
--td-brand-color-1: #00a59b20;
--td-brand-color-2: #003b36;
--td-brand-color-3: #00524c;
--td-brand-color-4: #006b64;
--td-brand-color-5: #00877e;
--td-brand-color-6: #00a59b;
--td-brand-color-7: #0ed6ca;
--td-brand-color-8: #86dad1;
--td-brand-color-9: #beefe9;
--td-brand-color-10: #e3fcf8;
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-5);
--td-brand-color: var(--td-brand-color-6);
--td-brand-color-active: var(--td-brand-color-7);
--td-warning-color-1: #4f2a1d;
--td-warning-color-2: #582f21;
--td-warning-color-3: #733c23;
--td-warning-color-4: #a75d2b;
--td-warning-color-5: #cf6e2d;
--td-warning-color-6: #dc7633;
--td-warning-color-7: #e8935c;
--td-warning-color-8: #ecbf91;
--td-warning-color-9: #eed7bf;
--td-warning-color-10: #f3e9dc;
--td-error-color-1: #472324;
--td-error-color-2: #5e2a2d;
--td-error-color-3: #703439;
--td-error-color-4: #83383e;
--td-error-color-5: #a03f46;
--td-error-color-6: #c64751;
--td-error-color-7: #de6670;
--td-error-color-8: #ec888e;
--td-error-color-9: #edb1b6;
--td-error-color-10: #eeced0;
--td-success-color-1: #193a2a;
--td-success-color-2: #1a4230;
--td-success-color-3: #17533d;
--td-success-color-4: #0d7a55;
--td-success-color-5: #059465;
--td-success-color-6: #43af8a;
--td-success-color-7: #46bf96;
--td-success-color-8: #80d2b6;
--td-success-color-9: #b4e1d3;
--td-success-color-10: #deede8;
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-page: var(--td-gray-color-14);
--td-bg-color-container: var(--td-gray-color-13);
--td-bg-color-container-hover: var(--td-gray-color-12);
--td-bg-color-container-active: var(--td-gray-color-10);
--td-bg-color-container-select: var(--td-gray-color-9);
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
--td-bg-color-component: var(--td-gray-color-11);
--td-bg-color-component-hover: var(--td-gray-color-10);
--td-bg-color-component-active: var(--td-gray-color-9);
--td-bg-color-component-disabled: var(--td-gray-color-12);
--td-component-stroke: var(--td-gray-color-11);
--td-component-border: var(--td-gray-color-9);
--td-font-white-1: rgba(255, 255, 255, 0.9);
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-white-1);
--td-text-color-secondary: var(--td-font-white-2);
--td-text-color-placeholder: var(--td-font-white-3);
--td-text-color-disabled: var(--td-font-white-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-shadow-1: 0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
--td-shadow-2: 0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.10), 0 5px 5px rgba(0, 0, 0, 0.16);
--td-shadow-3: 0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.20);
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
--td-table-shadow-color: rgba(0, 0, 0, 55%);
--td-scrollbar-color: rgba(255, 255, 255, 10%);
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
--td-scroll-track-color: #333;
--td-bg-color-specialcomponent: transparent;
--td-border-level-1-color: var(--td-gray-color-11);
--td-border-level-2-color: var(--td-gray-color-9);
--td-mask-active: rgba(0, 0, 0, 0.4);
--td-mask-disabled: rgba(0, 0, 0, 0.6);
:root[theme-mode='dark'] {
--td-brand-color-1: #00a59b20;
--td-brand-color-2: #003b36;
--td-brand-color-3: #00524c;
--td-brand-color-4: #006b64;
--td-brand-color-5: #00877e;
--td-brand-color-6: #00a59b;
--td-brand-color-7: #0ed6ca;
--td-brand-color-8: #86dad1;
--td-brand-color-9: #beefe9;
--td-brand-color-10: #e3fcf8;
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-5);
--td-brand-color: var(--td-brand-color-6);
--td-brand-color-active: var(--td-brand-color-7);
--td-warning-color-1: #4f2a1d;
--td-warning-color-2: #582f21;
--td-warning-color-3: #733c23;
--td-warning-color-4: #a75d2b;
--td-warning-color-5: #cf6e2d;
--td-warning-color-6: #dc7633;
--td-warning-color-7: #e8935c;
--td-warning-color-8: #ecbf91;
--td-warning-color-9: #eed7bf;
--td-warning-color-10: #f3e9dc;
--td-error-color-1: #472324;
--td-error-color-2: #5e2a2d;
--td-error-color-3: #703439;
--td-error-color-4: #83383e;
--td-error-color-5: #a03f46;
--td-error-color-6: #c64751;
--td-error-color-7: #de6670;
--td-error-color-8: #ec888e;
--td-error-color-9: #edb1b6;
--td-error-color-10: #eeced0;
--td-success-color-1: #193a2a;
--td-success-color-2: #1a4230;
--td-success-color-3: #17533d;
--td-success-color-4: #0d7a55;
--td-success-color-5: #059465;
--td-success-color-6: #43af8a;
--td-success-color-7: #46bf96;
--td-success-color-8: #80d2b6;
--td-success-color-9: #b4e1d3;
--td-success-color-10: #deede8;
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-page: var(--td-gray-color-14);
--td-bg-color-container: var(--td-gray-color-13);
--td-bg-color-container-hover: var(--td-gray-color-12);
--td-bg-color-container-active: var(--td-gray-color-10);
--td-bg-color-container-select: var(--td-gray-color-9);
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
--td-bg-color-component: var(--td-gray-color-11);
--td-bg-color-component-hover: var(--td-gray-color-10);
--td-bg-color-component-active: var(--td-gray-color-9);
--td-bg-color-component-disabled: var(--td-gray-color-12);
--td-component-stroke: var(--td-gray-color-11);
--td-component-border: var(--td-gray-color-9);
--td-font-white-1: rgba(255, 255, 255, 0.9);
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-white-1);
--td-text-color-secondary: var(--td-font-white-2);
--td-text-color-placeholder: var(--td-font-white-3);
--td-text-color-disabled: var(--td-font-white-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-shadow-1:
0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
--td-shadow-2:
0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.1), 0 5px 5px rgba(0, 0, 0, 0.16);
--td-shadow-3:
0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.2);
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
--td-table-shadow-color: rgba(0, 0, 0, 55%);
--td-scrollbar-color: rgba(255, 255, 255, 10%);
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
--td-scroll-track-color: #333;
--td-bg-color-specialcomponent: transparent;
--td-border-level-1-color: var(--td-gray-color-11);
--td-border-level-2-color: var(--td-gray-color-9);
--td-mask-active: rgba(0, 0, 0, 0.4);
--td-mask-disabled: rgba(0, 0, 0, 0.6);
}
:root {
--td-font-family: pingfang sc, microsoft yahei, arial regular;
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
--td-font-size-link-small: 12px;
--td-font-size-link-medium: 14px;
--td-font-size-link-large: 16px;
--td-font-size-mark-small: 12px;
--td-font-size-mark-medium: 14px;
--td-font-size-body-small: 12px;
--td-font-size-body-medium: 14px;
--td-font-size-body-large: 16px;
--td-font-size-title-small: 14px;
--td-font-size-title-medium: 16px;
--td-font-size-title-large: 20px;
--td-font-size-headline-small: 24px;
--td-font-size-headline-medium: 28px;
--td-font-size-headline-large: 36px;
--td-font-size-display-medium: 48px;
--td-font-size-display-large: 64px;
--td-line-height-link-small: 20px;
--td-line-height-link-medium: 22px;
--td-line-height-link-large: 24px;
--td-line-height-mark-small: 20px;
--td-line-height-mark-medium: 22px;
--td-line-height-body-small: 20px;
--td-line-height-body-medium: 22px;
--td-line-height-body-large: 24px;
--td-line-height-title-small: 22px;
--td-line-height-title-medium: 24px;
--td-line-height-title-large: 28px;
--td-line-height-headline-small: 32px;
--td-line-height-headline-medium: 36px;
--td-line-height-headline-large: 44px;
--td-line-height-display-medium: 56px;
--td-line-height-display-large: 72px;
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small) var(--td-font-family);
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium) var(--td-font-family);
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large) var(--td-font-family);
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small) var(--td-font-family);
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium) var(--td-font-family);
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small) var(--td-font-family);
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium) var(--td-font-family);
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large) var(--td-font-family);
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small) var(--td-font-family);
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium) var(--td-font-family);
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large) var(--td-font-family);
--td-font-headline-small: 600 var(--td-font-size-headline-small) / var(--td-line-height-headline-small) var(--td-font-family);
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) / var(--td-line-height-headline-medium) var(--td-font-family);
--td-font-headline-large: 600 var(--td-font-size-headline-large) / var(--td-line-height-headline-large) var(--td-font-family);
--td-font-display-medium: 600 var(--td-font-size-display-medium) / var(--td-line-height-display-medium) var(--td-font-family);
--td-font-display-large: 600 var(--td-font-size-display-large) / var(--td-line-height-display-large) var(--td-font-family);
--td-radius-small: 2px;
--td-radius-default: 3px;
--td-radius-medium: 6px;
--td-radius-large: 9px;
--td-radius-extraLarge: 12px;
--td-radius-round: 999px;
--td-radius-circle: 50%;
--td-size-1: 2px;
--td-size-2: 4px;
--td-size-3: 6px;
--td-size-4: 8px;
--td-size-5: 12px;
--td-size-6: 16px;
--td-size-7: 20px;
--td-size-8: 24px;
--td-size-9: 28px;
--td-size-10: 32px;
--td-size-11: 36px;
--td-size-12: 40px;
--td-size-13: 48px;
--td-size-14: 56px;
--td-size-15: 64px;
--td-size-16: 72px;
--td-comp-size-xxxs: var(--td-size-6);
--td-comp-size-xxs: var(--td-size-7);
--td-comp-size-xs: var(--td-size-8);
--td-comp-size-s: var(--td-size-9);
--td-comp-size-m: var(--td-size-10);
--td-comp-size-l: var(--td-size-11);
--td-comp-size-xl: var(--td-size-12);
--td-comp-size-xxl: var(--td-size-13);
--td-comp-size-xxxl: var(--td-size-14);
--td-comp-size-xxxxl: var(--td-size-15);
--td-comp-size-xxxxxl: var(--td-size-16);
--td-pop-padding-s: var(--td-size-2);
--td-pop-padding-m: var(--td-size-3);
--td-pop-padding-l: var(--td-size-4);
--td-pop-padding-xl: var(--td-size-5);
--td-pop-padding-xxl: var(--td-size-6);
--td-comp-paddingLR-xxs: var(--td-size-1);
--td-comp-paddingLR-xs: var(--td-size-2);
--td-comp-paddingLR-s: var(--td-size-4);
--td-comp-paddingLR-m: var(--td-size-5);
--td-comp-paddingLR-l: var(--td-size-6);
--td-comp-paddingLR-xl: var(--td-size-8);
--td-comp-paddingLR-xxl: var(--td-size-10);
--td-comp-paddingTB-xxs: var(--td-size-1);
--td-comp-paddingTB-xs: var(--td-size-2);
--td-comp-paddingTB-s: var(--td-size-4);
--td-comp-paddingTB-m: var(--td-size-5);
--td-comp-paddingTB-l: var(--td-size-6);
--td-comp-paddingTB-xl: var(--td-size-8);
--td-comp-paddingTB-xxl: var(--td-size-10);
--td-comp-margin-xxs: var(--td-size-1);
--td-comp-margin-xs: var(--td-size-2);
--td-comp-margin-s: var(--td-size-4);
--td-comp-margin-m: var(--td-size-5);
--td-comp-margin-l: var(--td-size-6);
--td-comp-margin-xl: var(--td-size-7);
--td-comp-margin-xxl: var(--td-size-8);
--td-comp-margin-xxxl: var(--td-size-10);
--td-comp-margin-xxxxl: var(--td-size-12);
}
--td-font-family: pingfang sc, microsoft yahei, arial regular;
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
--td-font-size-link-small: 12px;
--td-font-size-link-medium: 14px;
--td-font-size-link-large: 16px;
--td-font-size-mark-small: 12px;
--td-font-size-mark-medium: 14px;
--td-font-size-body-small: 12px;
--td-font-size-body-medium: 14px;
--td-font-size-body-large: 16px;
--td-font-size-title-small: 14px;
--td-font-size-title-medium: 16px;
--td-font-size-title-large: 20px;
--td-font-size-headline-small: 24px;
--td-font-size-headline-medium: 28px;
--td-font-size-headline-large: 36px;
--td-font-size-display-medium: 48px;
--td-font-size-display-large: 64px;
--td-line-height-link-small: 20px;
--td-line-height-link-medium: 22px;
--td-line-height-link-large: 24px;
--td-line-height-mark-small: 20px;
--td-line-height-mark-medium: 22px;
--td-line-height-body-small: 20px;
--td-line-height-body-medium: 22px;
--td-line-height-body-large: 24px;
--td-line-height-title-small: 22px;
--td-line-height-title-medium: 24px;
--td-line-height-title-large: 28px;
--td-line-height-headline-small: 32px;
--td-line-height-headline-medium: 36px;
--td-line-height-headline-large: 44px;
--td-line-height-display-medium: 56px;
--td-line-height-display-large: 72px;
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small)
var(--td-font-family);
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium)
var(--td-font-family);
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large)
var(--td-font-family);
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small)
var(--td-font-family);
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium)
var(--td-font-family);
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small)
var(--td-font-family);
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium)
var(--td-font-family);
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large)
var(--td-font-family);
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small)
var(--td-font-family);
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium)
var(--td-font-family);
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large)
var(--td-font-family);
--td-font-headline-small: 600 var(--td-font-size-headline-small) /
var(--td-line-height-headline-small) var(--td-font-family);
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) /
var(--td-line-height-headline-medium) var(--td-font-family);
--td-font-headline-large: 600 var(--td-font-size-headline-large) /
var(--td-line-height-headline-large) var(--td-font-family);
--td-font-display-medium: 600 var(--td-font-size-display-medium) /
var(--td-line-height-display-medium) var(--td-font-family);
--td-font-display-large: 600 var(--td-font-size-display-large) /
var(--td-line-height-display-large) var(--td-font-family);
--td-radius-small: 2px;
--td-radius-default: 3px;
--td-radius-medium: 6px;
--td-radius-large: 9px;
--td-radius-extraLarge: 12px;
--td-radius-round: 999px;
--td-radius-circle: 50%;
--td-size-1: 2px;
--td-size-2: 4px;
--td-size-3: 6px;
--td-size-4: 8px;
--td-size-5: 12px;
--td-size-6: 16px;
--td-size-7: 20px;
--td-size-8: 24px;
--td-size-9: 28px;
--td-size-10: 32px;
--td-size-11: 36px;
--td-size-12: 40px;
--td-size-13: 48px;
--td-size-14: 56px;
--td-size-15: 64px;
--td-size-16: 72px;
--td-comp-size-xxxs: var(--td-size-6);
--td-comp-size-xxs: var(--td-size-7);
--td-comp-size-xs: var(--td-size-8);
--td-comp-size-s: var(--td-size-9);
--td-comp-size-m: var(--td-size-10);
--td-comp-size-l: var(--td-size-11);
--td-comp-size-xl: var(--td-size-12);
--td-comp-size-xxl: var(--td-size-13);
--td-comp-size-xxxl: var(--td-size-14);
--td-comp-size-xxxxl: var(--td-size-15);
--td-comp-size-xxxxxl: var(--td-size-16);
--td-pop-padding-s: var(--td-size-2);
--td-pop-padding-m: var(--td-size-3);
--td-pop-padding-l: var(--td-size-4);
--td-pop-padding-xl: var(--td-size-5);
--td-pop-padding-xxl: var(--td-size-6);
--td-comp-paddingLR-xxs: var(--td-size-1);
--td-comp-paddingLR-xs: var(--td-size-2);
--td-comp-paddingLR-s: var(--td-size-4);
--td-comp-paddingLR-m: var(--td-size-5);
--td-comp-paddingLR-l: var(--td-size-6);
--td-comp-paddingLR-xl: var(--td-size-8);
--td-comp-paddingLR-xxl: var(--td-size-10);
--td-comp-paddingTB-xxs: var(--td-size-1);
--td-comp-paddingTB-xs: var(--td-size-2);
--td-comp-paddingTB-s: var(--td-size-4);
--td-comp-paddingTB-m: var(--td-size-5);
--td-comp-paddingTB-l: var(--td-size-6);
--td-comp-paddingTB-xl: var(--td-size-8);
--td-comp-paddingTB-xxl: var(--td-size-10);
--td-comp-margin-xxs: var(--td-size-1);
--td-comp-margin-xs: var(--td-size-2);
--td-comp-margin-s: var(--td-size-4);
--td-comp-margin-m: var(--td-size-5);
--td-comp-margin-l: var(--td-size-6);
--td-comp-margin-xl: var(--td-size-7);
--td-comp-margin-xxl: var(--td-size-8);
--td-comp-margin-xxxl: var(--td-size-10);
--td-comp-margin-xxxxl: var(--td-size-12);
}

View File

@@ -1,355 +1,379 @@
:root[theme-mode="orange"] {
--td-brand-color-1: #fff1ea;
--td-brand-color-2: #ffd9c5;
--td-brand-color-3: #ffb991;
--td-brand-color-4: #fb9458;
--td-brand-color-5: #e47228;
--td-brand-color-6: #c05708;
--td-brand-color-7: #9a4200;
--td-brand-color-8: #753000;
--td-brand-color-9: #552100;
--td-brand-color-10: #3d1600;
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-3);
--td-brand-color: var(--td-brand-color-4);
--td-brand-color-active: var(--td-brand-color-5);
--td-warning-color-1: #fef3e6;
--td-warning-color-2: #f9e0c7;
--td-warning-color-3: #f7c797;
--td-warning-color-4: #f2995f;
--td-warning-color-5: #ed7b2f;
--td-warning-color-6: #d35a21;
--td-warning-color-7: #ba431b;
--td-warning-color-8: #9e3610;
--td-warning-color-9: #842b0b;
--td-warning-color-10: #5a1907;
--td-warning-color: var(--td-warning-color-5);
--td-warning-color-hover: var(--td-warning-color-4);
--td-warning-color-focus: var(--td-warning-color-2);
--td-warning-color-active: var(--td-warning-color-6);
--td-warning-color-disabled: var(--td-warning-color-3);
--td-warning-color-light: var(--td-warning-color-1);
--td-error-color-1: #fdecee;
--td-error-color-2: #f9d7d9;
--td-error-color-3: #f8b9be;
--td-error-color-4: #f78d94;
--td-error-color-5: #f36d78;
--td-error-color-6: #e34d59;
--td-error-color-7: #c9353f;
--td-error-color-8: #b11f26;
--td-error-color-9: #951114;
--td-error-color-10: #680506;
--td-error-color: var(--td-error-color-6);
--td-error-color-hover: var(--td-error-color-5);
--td-error-color-focus: var(--td-error-color-2);
--td-error-color-active: var(--td-error-color-7);
--td-error-color-disabled: var(--td-error-color-3);
--td-error-color-light: var(--td-error-color-1);
--td-success-color-1: #e8f8f2;
--td-success-color-2: #bcebdc;
--td-success-color-3: #85dbbe;
--td-success-color-4: #48c79c;
--td-success-color-5: #00a870;
--td-success-color-6: #078d5c;
--td-success-color-7: #067945;
--td-success-color-8: #056334;
--td-success-color-9: #044f2a;
--td-success-color-10: #033017;
--td-success-color: var(--td-success-color-5);
--td-success-color-hover: var(--td-success-color-4);
--td-success-color-focus: var(--td-success-color-2);
--td-success-color-active: var(--td-success-color-6);
--td-success-color-disabled: var(--td-success-color-3);
--td-success-color-light: var(--td-success-color-1);
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-container: #fff;
--td-bg-color-container-select: #fff;
--td-bg-color-page: var(--td-gray-color-2);
--td-bg-color-container-hover: var(--td-gray-color-1);
--td-bg-color-container-active: var(--td-gray-color-3);
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
--td-bg-color-component: var(--td-gray-color-3);
--td-bg-color-component-hover: var(--td-gray-color-4);
--td-bg-color-component-active: var(--td-gray-color-6);
--td-bg-color-component-disabled: var(--td-gray-color-2);
--td-component-stroke: var(--td-gray-color-3);
--td-component-border: var(--td-gray-color-4);
--td-font-white-1: #ffffff;
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-gray-1);
--td-text-color-secondary: var(--td-font-gray-2);
--td-text-color-placeholder: var(--td-font-gray-3);
--td-text-color-disabled: var(--td-font-gray-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-brand-color-light-hover: var(--td-brand-color-2);
--td-warning-color-light-hover: var(--td-warning-color-2);
--td-error-color-light-hover: var(--td-error-color-2);
--td-success-color-light-hover: var(--td-success-color-2);
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
--td-table-shadow-color: rgba(0, 0, 0, 8%);
--td-scrollbar-color: rgba(0, 0, 0, 10%);
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
--td-scroll-track-color: #fff;
--td-bg-color-specialcomponent: #fff;
--td-border-level-1-color: var(--td-gray-color-3);
--td-border-level-2-color: var(--td-gray-color-4);
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%), 0 5px 5px -3px rgba(0, 0, 0, 10%);
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%), 0 8px 10px -5px rgba(0, 0, 0, 8%);
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
--td-mask-active: rgba(0, 0, 0, 0.6);
--td-mask-disabled: rgba(255, 255, 255, 0.6);
:root[theme-mode='orange'] {
--td-brand-color-1: #fff1ea;
--td-brand-color-2: #ffd9c5;
--td-brand-color-3: #ffb991;
--td-brand-color-4: #fb9458;
--td-brand-color-5: #e47228;
--td-brand-color-6: #c05708;
--td-brand-color-7: #9a4200;
--td-brand-color-8: #753000;
--td-brand-color-9: #552100;
--td-brand-color-10: #3d1600;
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-3);
--td-brand-color: var(--td-brand-color-4);
--td-brand-color-active: var(--td-brand-color-5);
--td-warning-color-1: #fef3e6;
--td-warning-color-2: #f9e0c7;
--td-warning-color-3: #f7c797;
--td-warning-color-4: #f2995f;
--td-warning-color-5: #ed7b2f;
--td-warning-color-6: #d35a21;
--td-warning-color-7: #ba431b;
--td-warning-color-8: #9e3610;
--td-warning-color-9: #842b0b;
--td-warning-color-10: #5a1907;
--td-warning-color: var(--td-warning-color-5);
--td-warning-color-hover: var(--td-warning-color-4);
--td-warning-color-focus: var(--td-warning-color-2);
--td-warning-color-active: var(--td-warning-color-6);
--td-warning-color-disabled: var(--td-warning-color-3);
--td-warning-color-light: var(--td-warning-color-1);
--td-error-color-1: #fdecee;
--td-error-color-2: #f9d7d9;
--td-error-color-3: #f8b9be;
--td-error-color-4: #f78d94;
--td-error-color-5: #f36d78;
--td-error-color-6: #e34d59;
--td-error-color-7: #c9353f;
--td-error-color-8: #b11f26;
--td-error-color-9: #951114;
--td-error-color-10: #680506;
--td-error-color: var(--td-error-color-6);
--td-error-color-hover: var(--td-error-color-5);
--td-error-color-focus: var(--td-error-color-2);
--td-error-color-active: var(--td-error-color-7);
--td-error-color-disabled: var(--td-error-color-3);
--td-error-color-light: var(--td-error-color-1);
--td-success-color-1: #e8f8f2;
--td-success-color-2: #bcebdc;
--td-success-color-3: #85dbbe;
--td-success-color-4: #48c79c;
--td-success-color-5: #00a870;
--td-success-color-6: #078d5c;
--td-success-color-7: #067945;
--td-success-color-8: #056334;
--td-success-color-9: #044f2a;
--td-success-color-10: #033017;
--td-success-color: var(--td-success-color-5);
--td-success-color-hover: var(--td-success-color-4);
--td-success-color-focus: var(--td-success-color-2);
--td-success-color-active: var(--td-success-color-6);
--td-success-color-disabled: var(--td-success-color-3);
--td-success-color-light: var(--td-success-color-1);
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-container: #fff;
--td-bg-color-container-select: #fff;
--td-bg-color-page: var(--td-gray-color-2);
--td-bg-color-container-hover: var(--td-gray-color-1);
--td-bg-color-container-active: var(--td-gray-color-3);
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
--td-bg-color-component: var(--td-gray-color-3);
--td-bg-color-component-hover: var(--td-gray-color-4);
--td-bg-color-component-active: var(--td-gray-color-6);
--td-bg-color-component-disabled: var(--td-gray-color-2);
--td-component-stroke: var(--td-gray-color-3);
--td-component-border: var(--td-gray-color-4);
--td-font-white-1: #ffffff;
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-gray-1);
--td-text-color-secondary: var(--td-font-gray-2);
--td-text-color-placeholder: var(--td-font-gray-3);
--td-text-color-disabled: var(--td-font-gray-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-brand-color-light-hover: var(--td-brand-color-2);
--td-warning-color-light-hover: var(--td-warning-color-2);
--td-error-color-light-hover: var(--td-error-color-2);
--td-success-color-light-hover: var(--td-success-color-2);
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
--td-table-shadow-color: rgba(0, 0, 0, 8%);
--td-scrollbar-color: rgba(0, 0, 0, 10%);
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
--td-scroll-track-color: #fff;
--td-bg-color-specialcomponent: #fff;
--td-border-level-1-color: var(--td-gray-color-3);
--td-border-level-2-color: var(--td-gray-color-4);
--td-shadow-1:
0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
--td-shadow-2:
0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%),
0 5px 5px -3px rgba(0, 0, 0, 10%);
--td-shadow-3:
0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%),
0 8px 10px -5px rgba(0, 0, 0, 8%);
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
--td-mask-active: rgba(0, 0, 0, 0.6);
--td-mask-disabled: rgba(255, 255, 255, 0.6);
}
:root[theme-mode="dark"] {
--td-brand-color-1: #e4722820;
--td-brand-color-2: #552100;
--td-brand-color-3: #753000;
--td-brand-color-4: #9a4200;
--td-brand-color-5: #c05708;
--td-brand-color-6: #e47228;
--td-brand-color-7: #fd853a;
--td-brand-color-8: #ffb991;
--td-brand-color-9: #ffd9c5;
--td-brand-color-10: #fff1ea;
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-5);
--td-brand-color: var(--td-brand-color-6);
--td-brand-color-active: var(--td-brand-color-7);
--td-warning-color-1: #4f2a1d;
--td-warning-color-2: #582f21;
--td-warning-color-3: #733c23;
--td-warning-color-4: #a75d2b;
--td-warning-color-5: #cf6e2d;
--td-warning-color-6: #dc7633;
--td-warning-color-7: #e8935c;
--td-warning-color-8: #ecbf91;
--td-warning-color-9: #eed7bf;
--td-warning-color-10: #f3e9dc;
--td-error-color-1: #472324;
--td-error-color-2: #5e2a2d;
--td-error-color-3: #703439;
--td-error-color-4: #83383e;
--td-error-color-5: #a03f46;
--td-error-color-6: #c64751;
--td-error-color-7: #de6670;
--td-error-color-8: #ec888e;
--td-error-color-9: #edb1b6;
--td-error-color-10: #eeced0;
--td-success-color-1: #193a2a;
--td-success-color-2: #1a4230;
--td-success-color-3: #17533d;
--td-success-color-4: #0d7a55;
--td-success-color-5: #059465;
--td-success-color-6: #43af8a;
--td-success-color-7: #46bf96;
--td-success-color-8: #80d2b6;
--td-success-color-9: #b4e1d3;
--td-success-color-10: #deede8;
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-page: var(--td-gray-color-14);
--td-bg-color-container: var(--td-gray-color-13);
--td-bg-color-container-hover: var(--td-gray-color-12);
--td-bg-color-container-active: var(--td-gray-color-10);
--td-bg-color-container-select: var(--td-gray-color-9);
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
--td-bg-color-component: var(--td-gray-color-11);
--td-bg-color-component-hover: var(--td-gray-color-10);
--td-bg-color-component-active: var(--td-gray-color-9);
--td-bg-color-component-disabled: var(--td-gray-color-12);
--td-component-stroke: var(--td-gray-color-11);
--td-component-border: var(--td-gray-color-9);
--td-font-white-1: rgba(255, 255, 255, 0.9);
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-white-1);
--td-text-color-secondary: var(--td-font-white-2);
--td-text-color-placeholder: var(--td-font-white-3);
--td-text-color-disabled: var(--td-font-white-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-shadow-1: 0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
--td-shadow-2: 0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.10), 0 5px 5px rgba(0, 0, 0, 0.16);
--td-shadow-3: 0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.20);
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
--td-table-shadow-color: rgba(0, 0, 0, 55%);
--td-scrollbar-color: rgba(255, 255, 255, 10%);
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
--td-scroll-track-color: #333;
--td-bg-color-specialcomponent: transparent;
--td-border-level-1-color: var(--td-gray-color-11);
--td-border-level-2-color: var(--td-gray-color-9);
--td-mask-active: rgba(0, 0, 0, 0.4);
--td-mask-disabled: rgba(0, 0, 0, 0.6);
:root[theme-mode='dark'] {
--td-brand-color-1: #e4722820;
--td-brand-color-2: #552100;
--td-brand-color-3: #753000;
--td-brand-color-4: #9a4200;
--td-brand-color-5: #c05708;
--td-brand-color-6: #e47228;
--td-brand-color-7: #fd853a;
--td-brand-color-8: #ffb991;
--td-brand-color-9: #ffd9c5;
--td-brand-color-10: #fff1ea;
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-5);
--td-brand-color: var(--td-brand-color-6);
--td-brand-color-active: var(--td-brand-color-7);
--td-warning-color-1: #4f2a1d;
--td-warning-color-2: #582f21;
--td-warning-color-3: #733c23;
--td-warning-color-4: #a75d2b;
--td-warning-color-5: #cf6e2d;
--td-warning-color-6: #dc7633;
--td-warning-color-7: #e8935c;
--td-warning-color-8: #ecbf91;
--td-warning-color-9: #eed7bf;
--td-warning-color-10: #f3e9dc;
--td-error-color-1: #472324;
--td-error-color-2: #5e2a2d;
--td-error-color-3: #703439;
--td-error-color-4: #83383e;
--td-error-color-5: #a03f46;
--td-error-color-6: #c64751;
--td-error-color-7: #de6670;
--td-error-color-8: #ec888e;
--td-error-color-9: #edb1b6;
--td-error-color-10: #eeced0;
--td-success-color-1: #193a2a;
--td-success-color-2: #1a4230;
--td-success-color-3: #17533d;
--td-success-color-4: #0d7a55;
--td-success-color-5: #059465;
--td-success-color-6: #43af8a;
--td-success-color-7: #46bf96;
--td-success-color-8: #80d2b6;
--td-success-color-9: #b4e1d3;
--td-success-color-10: #deede8;
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-page: var(--td-gray-color-14);
--td-bg-color-container: var(--td-gray-color-13);
--td-bg-color-container-hover: var(--td-gray-color-12);
--td-bg-color-container-active: var(--td-gray-color-10);
--td-bg-color-container-select: var(--td-gray-color-9);
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
--td-bg-color-component: var(--td-gray-color-11);
--td-bg-color-component-hover: var(--td-gray-color-10);
--td-bg-color-component-active: var(--td-gray-color-9);
--td-bg-color-component-disabled: var(--td-gray-color-12);
--td-component-stroke: var(--td-gray-color-11);
--td-component-border: var(--td-gray-color-9);
--td-font-white-1: rgba(255, 255, 255, 0.9);
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-white-1);
--td-text-color-secondary: var(--td-font-white-2);
--td-text-color-placeholder: var(--td-font-white-3);
--td-text-color-disabled: var(--td-font-white-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-shadow-1:
0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
--td-shadow-2:
0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.1), 0 5px 5px rgba(0, 0, 0, 0.16);
--td-shadow-3:
0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.2);
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
--td-table-shadow-color: rgba(0, 0, 0, 55%);
--td-scrollbar-color: rgba(255, 255, 255, 10%);
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
--td-scroll-track-color: #333;
--td-bg-color-specialcomponent: transparent;
--td-border-level-1-color: var(--td-gray-color-11);
--td-border-level-2-color: var(--td-gray-color-9);
--td-mask-active: rgba(0, 0, 0, 0.4);
--td-mask-disabled: rgba(0, 0, 0, 0.6);
}
:root {
--td-font-family: pingfang sc, microsoft yahei, arial regular;
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
--td-font-size-link-small: 12px;
--td-font-size-link-medium: 14px;
--td-font-size-link-large: 16px;
--td-font-size-mark-small: 12px;
--td-font-size-mark-medium: 14px;
--td-font-size-body-small: 12px;
--td-font-size-body-medium: 14px;
--td-font-size-body-large: 16px;
--td-font-size-title-small: 14px;
--td-font-size-title-medium: 16px;
--td-font-size-title-large: 20px;
--td-font-size-headline-small: 24px;
--td-font-size-headline-medium: 28px;
--td-font-size-headline-large: 36px;
--td-font-size-display-medium: 48px;
--td-font-size-display-large: 64px;
--td-line-height-link-small: 20px;
--td-line-height-link-medium: 22px;
--td-line-height-link-large: 24px;
--td-line-height-mark-small: 20px;
--td-line-height-mark-medium: 22px;
--td-line-height-body-small: 20px;
--td-line-height-body-medium: 22px;
--td-line-height-body-large: 24px;
--td-line-height-title-small: 22px;
--td-line-height-title-medium: 24px;
--td-line-height-title-large: 28px;
--td-line-height-headline-small: 32px;
--td-line-height-headline-medium: 36px;
--td-line-height-headline-large: 44px;
--td-line-height-display-medium: 56px;
--td-line-height-display-large: 72px;
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small) var(--td-font-family);
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium) var(--td-font-family);
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large) var(--td-font-family);
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small) var(--td-font-family);
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium) var(--td-font-family);
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small) var(--td-font-family);
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium) var(--td-font-family);
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large) var(--td-font-family);
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small) var(--td-font-family);
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium) var(--td-font-family);
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large) var(--td-font-family);
--td-font-headline-small: 600 var(--td-font-size-headline-small) / var(--td-line-height-headline-small) var(--td-font-family);
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) / var(--td-line-height-headline-medium) var(--td-font-family);
--td-font-headline-large: 600 var(--td-font-size-headline-large) / var(--td-line-height-headline-large) var(--td-font-family);
--td-font-display-medium: 600 var(--td-font-size-display-medium) / var(--td-line-height-display-medium) var(--td-font-family);
--td-font-display-large: 600 var(--td-font-size-display-large) / var(--td-line-height-display-large) var(--td-font-family);
--td-radius-small: 2px;
--td-radius-default: 3px;
--td-radius-medium: 6px;
--td-radius-large: 9px;
--td-radius-extraLarge: 12px;
--td-radius-round: 999px;
--td-radius-circle: 50%;
--td-size-1: 2px;
--td-size-2: 4px;
--td-size-3: 6px;
--td-size-4: 8px;
--td-size-5: 12px;
--td-size-6: 16px;
--td-size-7: 20px;
--td-size-8: 24px;
--td-size-9: 28px;
--td-size-10: 32px;
--td-size-11: 36px;
--td-size-12: 40px;
--td-size-13: 48px;
--td-size-14: 56px;
--td-size-15: 64px;
--td-size-16: 72px;
--td-comp-size-xxxs: var(--td-size-6);
--td-comp-size-xxs: var(--td-size-7);
--td-comp-size-xs: var(--td-size-8);
--td-comp-size-s: var(--td-size-9);
--td-comp-size-m: var(--td-size-10);
--td-comp-size-l: var(--td-size-11);
--td-comp-size-xl: var(--td-size-12);
--td-comp-size-xxl: var(--td-size-13);
--td-comp-size-xxxl: var(--td-size-14);
--td-comp-size-xxxxl: var(--td-size-15);
--td-comp-size-xxxxxl: var(--td-size-16);
--td-pop-padding-s: var(--td-size-2);
--td-pop-padding-m: var(--td-size-3);
--td-pop-padding-l: var(--td-size-4);
--td-pop-padding-xl: var(--td-size-5);
--td-pop-padding-xxl: var(--td-size-6);
--td-comp-paddingLR-xxs: var(--td-size-1);
--td-comp-paddingLR-xs: var(--td-size-2);
--td-comp-paddingLR-s: var(--td-size-4);
--td-comp-paddingLR-m: var(--td-size-5);
--td-comp-paddingLR-l: var(--td-size-6);
--td-comp-paddingLR-xl: var(--td-size-8);
--td-comp-paddingLR-xxl: var(--td-size-10);
--td-comp-paddingTB-xxs: var(--td-size-1);
--td-comp-paddingTB-xs: var(--td-size-2);
--td-comp-paddingTB-s: var(--td-size-4);
--td-comp-paddingTB-m: var(--td-size-5);
--td-comp-paddingTB-l: var(--td-size-6);
--td-comp-paddingTB-xl: var(--td-size-8);
--td-comp-paddingTB-xxl: var(--td-size-10);
--td-comp-margin-xxs: var(--td-size-1);
--td-comp-margin-xs: var(--td-size-2);
--td-comp-margin-s: var(--td-size-4);
--td-comp-margin-m: var(--td-size-5);
--td-comp-margin-l: var(--td-size-6);
--td-comp-margin-xl: var(--td-size-7);
--td-comp-margin-xxl: var(--td-size-8);
--td-comp-margin-xxxl: var(--td-size-10);
--td-comp-margin-xxxxl: var(--td-size-12);
}
--td-font-family: pingfang sc, microsoft yahei, arial regular;
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
--td-font-size-link-small: 12px;
--td-font-size-link-medium: 14px;
--td-font-size-link-large: 16px;
--td-font-size-mark-small: 12px;
--td-font-size-mark-medium: 14px;
--td-font-size-body-small: 12px;
--td-font-size-body-medium: 14px;
--td-font-size-body-large: 16px;
--td-font-size-title-small: 14px;
--td-font-size-title-medium: 16px;
--td-font-size-title-large: 20px;
--td-font-size-headline-small: 24px;
--td-font-size-headline-medium: 28px;
--td-font-size-headline-large: 36px;
--td-font-size-display-medium: 48px;
--td-font-size-display-large: 64px;
--td-line-height-link-small: 20px;
--td-line-height-link-medium: 22px;
--td-line-height-link-large: 24px;
--td-line-height-mark-small: 20px;
--td-line-height-mark-medium: 22px;
--td-line-height-body-small: 20px;
--td-line-height-body-medium: 22px;
--td-line-height-body-large: 24px;
--td-line-height-title-small: 22px;
--td-line-height-title-medium: 24px;
--td-line-height-title-large: 28px;
--td-line-height-headline-small: 32px;
--td-line-height-headline-medium: 36px;
--td-line-height-headline-large: 44px;
--td-line-height-display-medium: 56px;
--td-line-height-display-large: 72px;
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small)
var(--td-font-family);
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium)
var(--td-font-family);
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large)
var(--td-font-family);
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small)
var(--td-font-family);
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium)
var(--td-font-family);
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small)
var(--td-font-family);
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium)
var(--td-font-family);
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large)
var(--td-font-family);
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small)
var(--td-font-family);
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium)
var(--td-font-family);
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large)
var(--td-font-family);
--td-font-headline-small: 600 var(--td-font-size-headline-small) /
var(--td-line-height-headline-small) var(--td-font-family);
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) /
var(--td-line-height-headline-medium) var(--td-font-family);
--td-font-headline-large: 600 var(--td-font-size-headline-large) /
var(--td-line-height-headline-large) var(--td-font-family);
--td-font-display-medium: 600 var(--td-font-size-display-medium) /
var(--td-line-height-display-medium) var(--td-font-family);
--td-font-display-large: 600 var(--td-font-size-display-large) /
var(--td-line-height-display-large) var(--td-font-family);
--td-radius-small: 2px;
--td-radius-default: 3px;
--td-radius-medium: 6px;
--td-radius-large: 9px;
--td-radius-extraLarge: 12px;
--td-radius-round: 999px;
--td-radius-circle: 50%;
--td-size-1: 2px;
--td-size-2: 4px;
--td-size-3: 6px;
--td-size-4: 8px;
--td-size-5: 12px;
--td-size-6: 16px;
--td-size-7: 20px;
--td-size-8: 24px;
--td-size-9: 28px;
--td-size-10: 32px;
--td-size-11: 36px;
--td-size-12: 40px;
--td-size-13: 48px;
--td-size-14: 56px;
--td-size-15: 64px;
--td-size-16: 72px;
--td-comp-size-xxxs: var(--td-size-6);
--td-comp-size-xxs: var(--td-size-7);
--td-comp-size-xs: var(--td-size-8);
--td-comp-size-s: var(--td-size-9);
--td-comp-size-m: var(--td-size-10);
--td-comp-size-l: var(--td-size-11);
--td-comp-size-xl: var(--td-size-12);
--td-comp-size-xxl: var(--td-size-13);
--td-comp-size-xxxl: var(--td-size-14);
--td-comp-size-xxxxl: var(--td-size-15);
--td-comp-size-xxxxxl: var(--td-size-16);
--td-pop-padding-s: var(--td-size-2);
--td-pop-padding-m: var(--td-size-3);
--td-pop-padding-l: var(--td-size-4);
--td-pop-padding-xl: var(--td-size-5);
--td-pop-padding-xxl: var(--td-size-6);
--td-comp-paddingLR-xxs: var(--td-size-1);
--td-comp-paddingLR-xs: var(--td-size-2);
--td-comp-paddingLR-s: var(--td-size-4);
--td-comp-paddingLR-m: var(--td-size-5);
--td-comp-paddingLR-l: var(--td-size-6);
--td-comp-paddingLR-xl: var(--td-size-8);
--td-comp-paddingLR-xxl: var(--td-size-10);
--td-comp-paddingTB-xxs: var(--td-size-1);
--td-comp-paddingTB-xs: var(--td-size-2);
--td-comp-paddingTB-s: var(--td-size-4);
--td-comp-paddingTB-m: var(--td-size-5);
--td-comp-paddingTB-l: var(--td-size-6);
--td-comp-paddingTB-xl: var(--td-size-8);
--td-comp-paddingTB-xxl: var(--td-size-10);
--td-comp-margin-xxs: var(--td-size-1);
--td-comp-margin-xs: var(--td-size-2);
--td-comp-margin-s: var(--td-size-4);
--td-comp-margin-m: var(--td-size-5);
--td-comp-margin-l: var(--td-size-6);
--td-comp-margin-xl: var(--td-size-7);
--td-comp-margin-xxl: var(--td-size-8);
--td-comp-margin-xxxl: var(--td-size-10);
--td-comp-margin-xxxxl: var(--td-size-12);
}

View File

@@ -1,355 +1,379 @@
:root[theme-mode="pink"] {
--td-brand-color-1: #fff0f1;
--td-brand-color-2: #ffd8dd;
--td-brand-color-3: #ffb7c1;
--td-brand-color-4: #ff8fa2;
--td-brand-color-5: #fc5e7e;
--td-brand-color-6: #db3d62;
--td-brand-color-7: #b2294b;
--td-brand-color-8: #8d1135;
--td-brand-color-9: #690021;
--td-brand-color-10: #480014;
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-4);
--td-brand-color: var(--td-brand-color-5);
--td-brand-color-active: var(--td-brand-color-6);
--td-warning-color-1: #fef3e6;
--td-warning-color-2: #f9e0c7;
--td-warning-color-3: #f7c797;
--td-warning-color-4: #f2995f;
--td-warning-color-5: #ed7b2f;
--td-warning-color-6: #d35a21;
--td-warning-color-7: #ba431b;
--td-warning-color-8: #9e3610;
--td-warning-color-9: #842b0b;
--td-warning-color-10: #5a1907;
--td-warning-color: var(--td-warning-color-5);
--td-warning-color-hover: var(--td-warning-color-4);
--td-warning-color-focus: var(--td-warning-color-2);
--td-warning-color-active: var(--td-warning-color-6);
--td-warning-color-disabled: var(--td-warning-color-3);
--td-warning-color-light: var(--td-warning-color-1);
--td-error-color-1: #fdecee;
--td-error-color-2: #f9d7d9;
--td-error-color-3: #f8b9be;
--td-error-color-4: #f78d94;
--td-error-color-5: #f36d78;
--td-error-color-6: #e34d59;
--td-error-color-7: #c9353f;
--td-error-color-8: #b11f26;
--td-error-color-9: #951114;
--td-error-color-10: #680506;
--td-error-color: var(--td-error-color-6);
--td-error-color-hover: var(--td-error-color-5);
--td-error-color-focus: var(--td-error-color-2);
--td-error-color-active: var(--td-error-color-7);
--td-error-color-disabled: var(--td-error-color-3);
--td-error-color-light: var(--td-error-color-1);
--td-success-color-1: #e8f8f2;
--td-success-color-2: #bcebdc;
--td-success-color-3: #85dbbe;
--td-success-color-4: #48c79c;
--td-success-color-5: #00a870;
--td-success-color-6: #078d5c;
--td-success-color-7: #067945;
--td-success-color-8: #056334;
--td-success-color-9: #044f2a;
--td-success-color-10: #033017;
--td-success-color: var(--td-success-color-5);
--td-success-color-hover: var(--td-success-color-4);
--td-success-color-focus: var(--td-success-color-2);
--td-success-color-active: var(--td-success-color-6);
--td-success-color-disabled: var(--td-success-color-3);
--td-success-color-light: var(--td-success-color-1);
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-container: #fff;
--td-bg-color-container-select: #fff;
--td-bg-color-page: var(--td-gray-color-2);
--td-bg-color-container-hover: var(--td-gray-color-1);
--td-bg-color-container-active: var(--td-gray-color-3);
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
--td-bg-color-component: var(--td-gray-color-3);
--td-bg-color-component-hover: var(--td-gray-color-4);
--td-bg-color-component-active: var(--td-gray-color-6);
--td-bg-color-component-disabled: var(--td-gray-color-2);
--td-component-stroke: var(--td-gray-color-3);
--td-component-border: var(--td-gray-color-4);
--td-font-white-1: #ffffff;
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-gray-1);
--td-text-color-secondary: var(--td-font-gray-2);
--td-text-color-placeholder: var(--td-font-gray-3);
--td-text-color-disabled: var(--td-font-gray-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-brand-color-light-hover: var(--td-brand-color-2);
--td-warning-color-light-hover: var(--td-warning-color-2);
--td-error-color-light-hover: var(--td-error-color-2);
--td-success-color-light-hover: var(--td-success-color-2);
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
--td-table-shadow-color: rgba(0, 0, 0, 8%);
--td-scrollbar-color: rgba(0, 0, 0, 10%);
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
--td-scroll-track-color: #fff;
--td-bg-color-specialcomponent: #fff;
--td-border-level-1-color: var(--td-gray-color-3);
--td-border-level-2-color: var(--td-gray-color-4);
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%), 0 5px 5px -3px rgba(0, 0, 0, 10%);
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%), 0 8px 10px -5px rgba(0, 0, 0, 8%);
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
--td-mask-active: rgba(0, 0, 0, 0.6);
--td-mask-disabled: rgba(255, 255, 255, 0.6);
:root[theme-mode='pink'] {
--td-brand-color-1: #fff0f1;
--td-brand-color-2: #ffd8dd;
--td-brand-color-3: #ffb7c1;
--td-brand-color-4: #ff8fa2;
--td-brand-color-5: #fc5e7e;
--td-brand-color-6: #db3d62;
--td-brand-color-7: #b2294b;
--td-brand-color-8: #8d1135;
--td-brand-color-9: #690021;
--td-brand-color-10: #480014;
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-4);
--td-brand-color: var(--td-brand-color-5);
--td-brand-color-active: var(--td-brand-color-6);
--td-warning-color-1: #fef3e6;
--td-warning-color-2: #f9e0c7;
--td-warning-color-3: #f7c797;
--td-warning-color-4: #f2995f;
--td-warning-color-5: #ed7b2f;
--td-warning-color-6: #d35a21;
--td-warning-color-7: #ba431b;
--td-warning-color-8: #9e3610;
--td-warning-color-9: #842b0b;
--td-warning-color-10: #5a1907;
--td-warning-color: var(--td-warning-color-5);
--td-warning-color-hover: var(--td-warning-color-4);
--td-warning-color-focus: var(--td-warning-color-2);
--td-warning-color-active: var(--td-warning-color-6);
--td-warning-color-disabled: var(--td-warning-color-3);
--td-warning-color-light: var(--td-warning-color-1);
--td-error-color-1: #fdecee;
--td-error-color-2: #f9d7d9;
--td-error-color-3: #f8b9be;
--td-error-color-4: #f78d94;
--td-error-color-5: #f36d78;
--td-error-color-6: #e34d59;
--td-error-color-7: #c9353f;
--td-error-color-8: #b11f26;
--td-error-color-9: #951114;
--td-error-color-10: #680506;
--td-error-color: var(--td-error-color-6);
--td-error-color-hover: var(--td-error-color-5);
--td-error-color-focus: var(--td-error-color-2);
--td-error-color-active: var(--td-error-color-7);
--td-error-color-disabled: var(--td-error-color-3);
--td-error-color-light: var(--td-error-color-1);
--td-success-color-1: #e8f8f2;
--td-success-color-2: #bcebdc;
--td-success-color-3: #85dbbe;
--td-success-color-4: #48c79c;
--td-success-color-5: #00a870;
--td-success-color-6: #078d5c;
--td-success-color-7: #067945;
--td-success-color-8: #056334;
--td-success-color-9: #044f2a;
--td-success-color-10: #033017;
--td-success-color: var(--td-success-color-5);
--td-success-color-hover: var(--td-success-color-4);
--td-success-color-focus: var(--td-success-color-2);
--td-success-color-active: var(--td-success-color-6);
--td-success-color-disabled: var(--td-success-color-3);
--td-success-color-light: var(--td-success-color-1);
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-container: #fff;
--td-bg-color-container-select: #fff;
--td-bg-color-page: var(--td-gray-color-2);
--td-bg-color-container-hover: var(--td-gray-color-1);
--td-bg-color-container-active: var(--td-gray-color-3);
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
--td-bg-color-component: var(--td-gray-color-3);
--td-bg-color-component-hover: var(--td-gray-color-4);
--td-bg-color-component-active: var(--td-gray-color-6);
--td-bg-color-component-disabled: var(--td-gray-color-2);
--td-component-stroke: var(--td-gray-color-3);
--td-component-border: var(--td-gray-color-4);
--td-font-white-1: #ffffff;
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-gray-1);
--td-text-color-secondary: var(--td-font-gray-2);
--td-text-color-placeholder: var(--td-font-gray-3);
--td-text-color-disabled: var(--td-font-gray-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-brand-color-light-hover: var(--td-brand-color-2);
--td-warning-color-light-hover: var(--td-warning-color-2);
--td-error-color-light-hover: var(--td-error-color-2);
--td-success-color-light-hover: var(--td-success-color-2);
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
--td-table-shadow-color: rgba(0, 0, 0, 8%);
--td-scrollbar-color: rgba(0, 0, 0, 10%);
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
--td-scroll-track-color: #fff;
--td-bg-color-specialcomponent: #fff;
--td-border-level-1-color: var(--td-gray-color-3);
--td-border-level-2-color: var(--td-gray-color-4);
--td-shadow-1:
0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
--td-shadow-2:
0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%),
0 5px 5px -3px rgba(0, 0, 0, 10%);
--td-shadow-3:
0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%),
0 8px 10px -5px rgba(0, 0, 0, 8%);
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
--td-mask-active: rgba(0, 0, 0, 0.6);
--td-mask-disabled: rgba(255, 255, 255, 0.6);
}
:root[theme-mode="dark"] {
--td-brand-color-1: #ff547920;
--td-brand-color-2: #690021;
--td-brand-color-3: #8d1135;
--td-brand-color-4: #b2294b;
--td-brand-color-5: #db3d62;
--td-brand-color-6: #ff5479;
--td-brand-color-7: #ff8fa2;
--td-brand-color-8: #ffb7c1;
--td-brand-color-9: #ffd8dd;
--td-brand-color-10: #fff0f1;
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-5);
--td-brand-color: var(--td-brand-color-6);
--td-brand-color-active: var(--td-brand-color-7);
--td-warning-color-1: #4f2a1d;
--td-warning-color-2: #582f21;
--td-warning-color-3: #733c23;
--td-warning-color-4: #a75d2b;
--td-warning-color-5: #cf6e2d;
--td-warning-color-6: #dc7633;
--td-warning-color-7: #e8935c;
--td-warning-color-8: #ecbf91;
--td-warning-color-9: #eed7bf;
--td-warning-color-10: #f3e9dc;
--td-error-color-1: #472324;
--td-error-color-2: #5e2a2d;
--td-error-color-3: #703439;
--td-error-color-4: #83383e;
--td-error-color-5: #a03f46;
--td-error-color-6: #c64751;
--td-error-color-7: #de6670;
--td-error-color-8: #ec888e;
--td-error-color-9: #edb1b6;
--td-error-color-10: #eeced0;
--td-success-color-1: #193a2a;
--td-success-color-2: #1a4230;
--td-success-color-3: #17533d;
--td-success-color-4: #0d7a55;
--td-success-color-5: #059465;
--td-success-color-6: #43af8a;
--td-success-color-7: #46bf96;
--td-success-color-8: #80d2b6;
--td-success-color-9: #b4e1d3;
--td-success-color-10: #deede8;
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-page: var(--td-gray-color-14);
--td-bg-color-container: var(--td-gray-color-13);
--td-bg-color-container-hover: var(--td-gray-color-12);
--td-bg-color-container-active: var(--td-gray-color-10);
--td-bg-color-container-select: var(--td-gray-color-9);
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
--td-bg-color-component: var(--td-gray-color-11);
--td-bg-color-component-hover: var(--td-gray-color-10);
--td-bg-color-component-active: var(--td-gray-color-9);
--td-bg-color-component-disabled: var(--td-gray-color-12);
--td-component-stroke: var(--td-gray-color-11);
--td-component-border: var(--td-gray-color-9);
--td-font-white-1: rgba(255, 255, 255, 0.9);
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-white-1);
--td-text-color-secondary: var(--td-font-white-2);
--td-text-color-placeholder: var(--td-font-white-3);
--td-text-color-disabled: var(--td-font-white-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-shadow-1: 0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
--td-shadow-2: 0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.10), 0 5px 5px rgba(0, 0, 0, 0.16);
--td-shadow-3: 0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.20);
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
--td-table-shadow-color: rgba(0, 0, 0, 55%);
--td-scrollbar-color: rgba(255, 255, 255, 10%);
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
--td-scroll-track-color: #333;
--td-bg-color-specialcomponent: transparent;
--td-border-level-1-color: var(--td-gray-color-11);
--td-border-level-2-color: var(--td-gray-color-9);
--td-mask-active: rgba(0, 0, 0, 0.4);
--td-mask-disabled: rgba(0, 0, 0, 0.6);
:root[theme-mode='dark'] {
--td-brand-color-1: #ff547920;
--td-brand-color-2: #690021;
--td-brand-color-3: #8d1135;
--td-brand-color-4: #b2294b;
--td-brand-color-5: #db3d62;
--td-brand-color-6: #ff5479;
--td-brand-color-7: #ff8fa2;
--td-brand-color-8: #ffb7c1;
--td-brand-color-9: #ffd8dd;
--td-brand-color-10: #fff0f1;
--td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3);
--td-brand-color-hover: var(--td-brand-color-5);
--td-brand-color: var(--td-brand-color-6);
--td-brand-color-active: var(--td-brand-color-7);
--td-warning-color-1: #4f2a1d;
--td-warning-color-2: #582f21;
--td-warning-color-3: #733c23;
--td-warning-color-4: #a75d2b;
--td-warning-color-5: #cf6e2d;
--td-warning-color-6: #dc7633;
--td-warning-color-7: #e8935c;
--td-warning-color-8: #ecbf91;
--td-warning-color-9: #eed7bf;
--td-warning-color-10: #f3e9dc;
--td-error-color-1: #472324;
--td-error-color-2: #5e2a2d;
--td-error-color-3: #703439;
--td-error-color-4: #83383e;
--td-error-color-5: #a03f46;
--td-error-color-6: #c64751;
--td-error-color-7: #de6670;
--td-error-color-8: #ec888e;
--td-error-color-9: #edb1b6;
--td-error-color-10: #eeced0;
--td-success-color-1: #193a2a;
--td-success-color-2: #1a4230;
--td-success-color-3: #17533d;
--td-success-color-4: #0d7a55;
--td-success-color-5: #059465;
--td-success-color-6: #43af8a;
--td-success-color-7: #46bf96;
--td-success-color-8: #80d2b6;
--td-success-color-9: #b4e1d3;
--td-success-color-10: #deede8;
--td-gray-color-1: #f3f3f3;
--td-gray-color-2: #eee;
--td-gray-color-3: #e8e8e8;
--td-gray-color-4: #ddd;
--td-gray-color-5: #c5c5c5;
--td-gray-color-6: #a6a6a6;
--td-gray-color-7: #8b8b8b;
--td-gray-color-8: #777;
--td-gray-color-9: #5e5e5e;
--td-gray-color-10: #4b4b4b;
--td-gray-color-11: #383838;
--td-gray-color-12: #2c2c2c;
--td-gray-color-13: #242424;
--td-gray-color-14: #181818;
--td-bg-color-page: var(--td-gray-color-14);
--td-bg-color-container: var(--td-gray-color-13);
--td-bg-color-container-hover: var(--td-gray-color-12);
--td-bg-color-container-active: var(--td-gray-color-10);
--td-bg-color-container-select: var(--td-gray-color-9);
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
--td-bg-color-component: var(--td-gray-color-11);
--td-bg-color-component-hover: var(--td-gray-color-10);
--td-bg-color-component-active: var(--td-gray-color-9);
--td-bg-color-component-disabled: var(--td-gray-color-12);
--td-component-stroke: var(--td-gray-color-11);
--td-component-border: var(--td-gray-color-9);
--td-font-white-1: rgba(255, 255, 255, 0.9);
--td-font-white-2: rgba(255, 255, 255, 0.55);
--td-font-white-3: rgba(255, 255, 255, 0.35);
--td-font-white-4: rgba(255, 255, 255, 0.22);
--td-font-gray-1: rgba(0, 0, 0, 0.9);
--td-font-gray-2: rgba(0, 0, 0, 0.6);
--td-font-gray-3: rgba(0, 0, 0, 0.4);
--td-font-gray-4: rgba(0, 0, 0, 0.26);
--td-text-color-primary: var(--td-font-white-1);
--td-text-color-secondary: var(--td-font-white-2);
--td-text-color-placeholder: var(--td-font-white-3);
--td-text-color-disabled: var(--td-font-white-4);
--td-text-color-anti: #fff;
--td-text-color-brand: var(--td-brand-color);
--td-text-color-link: var(--td-brand-color);
--td-shadow-1:
0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
--td-shadow-2:
0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.1), 0 5px 5px rgba(0, 0, 0, 0.16);
--td-shadow-3:
0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.2);
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
--td-table-shadow-color: rgba(0, 0, 0, 55%);
--td-scrollbar-color: rgba(255, 255, 255, 10%);
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
--td-scroll-track-color: #333;
--td-bg-color-specialcomponent: transparent;
--td-border-level-1-color: var(--td-gray-color-11);
--td-border-level-2-color: var(--td-gray-color-9);
--td-mask-active: rgba(0, 0, 0, 0.4);
--td-mask-disabled: rgba(0, 0, 0, 0.6);
}
:root {
--td-font-family: pingfang sc, microsoft yahei, arial regular;
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
--td-font-size-link-small: 12px;
--td-font-size-link-medium: 14px;
--td-font-size-link-large: 16px;
--td-font-size-mark-small: 12px;
--td-font-size-mark-medium: 14px;
--td-font-size-body-small: 12px;
--td-font-size-body-medium: 14px;
--td-font-size-body-large: 16px;
--td-font-size-title-small: 14px;
--td-font-size-title-medium: 16px;
--td-font-size-title-large: 20px;
--td-font-size-headline-small: 24px;
--td-font-size-headline-medium: 28px;
--td-font-size-headline-large: 36px;
--td-font-size-display-medium: 48px;
--td-font-size-display-large: 64px;
--td-line-height-link-small: 20px;
--td-line-height-link-medium: 22px;
--td-line-height-link-large: 24px;
--td-line-height-mark-small: 20px;
--td-line-height-mark-medium: 22px;
--td-line-height-body-small: 20px;
--td-line-height-body-medium: 22px;
--td-line-height-body-large: 24px;
--td-line-height-title-small: 22px;
--td-line-height-title-medium: 24px;
--td-line-height-title-large: 28px;
--td-line-height-headline-small: 32px;
--td-line-height-headline-medium: 36px;
--td-line-height-headline-large: 44px;
--td-line-height-display-medium: 56px;
--td-line-height-display-large: 72px;
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small) var(--td-font-family);
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium) var(--td-font-family);
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large) var(--td-font-family);
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small) var(--td-font-family);
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium) var(--td-font-family);
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small) var(--td-font-family);
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium) var(--td-font-family);
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large) var(--td-font-family);
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small) var(--td-font-family);
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium) var(--td-font-family);
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large) var(--td-font-family);
--td-font-headline-small: 600 var(--td-font-size-headline-small) / var(--td-line-height-headline-small) var(--td-font-family);
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) / var(--td-line-height-headline-medium) var(--td-font-family);
--td-font-headline-large: 600 var(--td-font-size-headline-large) / var(--td-line-height-headline-large) var(--td-font-family);
--td-font-display-medium: 600 var(--td-font-size-display-medium) / var(--td-line-height-display-medium) var(--td-font-family);
--td-font-display-large: 600 var(--td-font-size-display-large) / var(--td-line-height-display-large) var(--td-font-family);
--td-radius-small: 2px;
--td-radius-default: 3px;
--td-radius-medium: 6px;
--td-radius-large: 9px;
--td-radius-extraLarge: 12px;
--td-radius-round: 999px;
--td-radius-circle: 50%;
--td-size-1: 2px;
--td-size-2: 4px;
--td-size-3: 6px;
--td-size-4: 8px;
--td-size-5: 12px;
--td-size-6: 16px;
--td-size-7: 20px;
--td-size-8: 24px;
--td-size-9: 28px;
--td-size-10: 32px;
--td-size-11: 36px;
--td-size-12: 40px;
--td-size-13: 48px;
--td-size-14: 56px;
--td-size-15: 64px;
--td-size-16: 72px;
--td-comp-size-xxxs: var(--td-size-6);
--td-comp-size-xxs: var(--td-size-7);
--td-comp-size-xs: var(--td-size-8);
--td-comp-size-s: var(--td-size-9);
--td-comp-size-m: var(--td-size-10);
--td-comp-size-l: var(--td-size-11);
--td-comp-size-xl: var(--td-size-12);
--td-comp-size-xxl: var(--td-size-13);
--td-comp-size-xxxl: var(--td-size-14);
--td-comp-size-xxxxl: var(--td-size-15);
--td-comp-size-xxxxxl: var(--td-size-16);
--td-pop-padding-s: var(--td-size-2);
--td-pop-padding-m: var(--td-size-3);
--td-pop-padding-l: var(--td-size-4);
--td-pop-padding-xl: var(--td-size-5);
--td-pop-padding-xxl: var(--td-size-6);
--td-comp-paddingLR-xxs: var(--td-size-1);
--td-comp-paddingLR-xs: var(--td-size-2);
--td-comp-paddingLR-s: var(--td-size-4);
--td-comp-paddingLR-m: var(--td-size-5);
--td-comp-paddingLR-l: var(--td-size-6);
--td-comp-paddingLR-xl: var(--td-size-8);
--td-comp-paddingLR-xxl: var(--td-size-10);
--td-comp-paddingTB-xxs: var(--td-size-1);
--td-comp-paddingTB-xs: var(--td-size-2);
--td-comp-paddingTB-s: var(--td-size-4);
--td-comp-paddingTB-m: var(--td-size-5);
--td-comp-paddingTB-l: var(--td-size-6);
--td-comp-paddingTB-xl: var(--td-size-8);
--td-comp-paddingTB-xxl: var(--td-size-10);
--td-comp-margin-xxs: var(--td-size-1);
--td-comp-margin-xs: var(--td-size-2);
--td-comp-margin-s: var(--td-size-4);
--td-comp-margin-m: var(--td-size-5);
--td-comp-margin-l: var(--td-size-6);
--td-comp-margin-xl: var(--td-size-7);
--td-comp-margin-xxl: var(--td-size-8);
--td-comp-margin-xxxl: var(--td-size-10);
--td-comp-margin-xxxxl: var(--td-size-12);
}
--td-font-family: pingfang sc, microsoft yahei, arial regular;
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
--td-font-size-link-small: 12px;
--td-font-size-link-medium: 14px;
--td-font-size-link-large: 16px;
--td-font-size-mark-small: 12px;
--td-font-size-mark-medium: 14px;
--td-font-size-body-small: 12px;
--td-font-size-body-medium: 14px;
--td-font-size-body-large: 16px;
--td-font-size-title-small: 14px;
--td-font-size-title-medium: 16px;
--td-font-size-title-large: 20px;
--td-font-size-headline-small: 24px;
--td-font-size-headline-medium: 28px;
--td-font-size-headline-large: 36px;
--td-font-size-display-medium: 48px;
--td-font-size-display-large: 64px;
--td-line-height-link-small: 20px;
--td-line-height-link-medium: 22px;
--td-line-height-link-large: 24px;
--td-line-height-mark-small: 20px;
--td-line-height-mark-medium: 22px;
--td-line-height-body-small: 20px;
--td-line-height-body-medium: 22px;
--td-line-height-body-large: 24px;
--td-line-height-title-small: 22px;
--td-line-height-title-medium: 24px;
--td-line-height-title-large: 28px;
--td-line-height-headline-small: 32px;
--td-line-height-headline-medium: 36px;
--td-line-height-headline-large: 44px;
--td-line-height-display-medium: 56px;
--td-line-height-display-large: 72px;
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small)
var(--td-font-family);
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium)
var(--td-font-family);
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large)
var(--td-font-family);
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small)
var(--td-font-family);
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium)
var(--td-font-family);
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small)
var(--td-font-family);
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium)
var(--td-font-family);
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large)
var(--td-font-family);
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small)
var(--td-font-family);
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium)
var(--td-font-family);
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large)
var(--td-font-family);
--td-font-headline-small: 600 var(--td-font-size-headline-small) /
var(--td-line-height-headline-small) var(--td-font-family);
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) /
var(--td-line-height-headline-medium) var(--td-font-family);
--td-font-headline-large: 600 var(--td-font-size-headline-large) /
var(--td-line-height-headline-large) var(--td-font-family);
--td-font-display-medium: 600 var(--td-font-size-display-medium) /
var(--td-line-height-display-medium) var(--td-font-family);
--td-font-display-large: 600 var(--td-font-size-display-large) /
var(--td-line-height-display-large) var(--td-font-family);
--td-radius-small: 2px;
--td-radius-default: 3px;
--td-radius-medium: 6px;
--td-radius-large: 9px;
--td-radius-extraLarge: 12px;
--td-radius-round: 999px;
--td-radius-circle: 50%;
--td-size-1: 2px;
--td-size-2: 4px;
--td-size-3: 6px;
--td-size-4: 8px;
--td-size-5: 12px;
--td-size-6: 16px;
--td-size-7: 20px;
--td-size-8: 24px;
--td-size-9: 28px;
--td-size-10: 32px;
--td-size-11: 36px;
--td-size-12: 40px;
--td-size-13: 48px;
--td-size-14: 56px;
--td-size-15: 64px;
--td-size-16: 72px;
--td-comp-size-xxxs: var(--td-size-6);
--td-comp-size-xxs: var(--td-size-7);
--td-comp-size-xs: var(--td-size-8);
--td-comp-size-s: var(--td-size-9);
--td-comp-size-m: var(--td-size-10);
--td-comp-size-l: var(--td-size-11);
--td-comp-size-xl: var(--td-size-12);
--td-comp-size-xxl: var(--td-size-13);
--td-comp-size-xxxl: var(--td-size-14);
--td-comp-size-xxxxl: var(--td-size-15);
--td-comp-size-xxxxxl: var(--td-size-16);
--td-pop-padding-s: var(--td-size-2);
--td-pop-padding-m: var(--td-size-3);
--td-pop-padding-l: var(--td-size-4);
--td-pop-padding-xl: var(--td-size-5);
--td-pop-padding-xxl: var(--td-size-6);
--td-comp-paddingLR-xxs: var(--td-size-1);
--td-comp-paddingLR-xs: var(--td-size-2);
--td-comp-paddingLR-s: var(--td-size-4);
--td-comp-paddingLR-m: var(--td-size-5);
--td-comp-paddingLR-l: var(--td-size-6);
--td-comp-paddingLR-xl: var(--td-size-8);
--td-comp-paddingLR-xxl: var(--td-size-10);
--td-comp-paddingTB-xxs: var(--td-size-1);
--td-comp-paddingTB-xs: var(--td-size-2);
--td-comp-paddingTB-s: var(--td-size-4);
--td-comp-paddingTB-m: var(--td-size-5);
--td-comp-paddingTB-l: var(--td-size-6);
--td-comp-paddingTB-xl: var(--td-size-8);
--td-comp-paddingTB-xxl: var(--td-size-10);
--td-comp-margin-xxs: var(--td-size-1);
--td-comp-margin-xs: var(--td-size-2);
--td-comp-margin-s: var(--td-size-4);
--td-comp-margin-m: var(--td-size-5);
--td-comp-margin-l: var(--td-size-6);
--td-comp-margin-xl: var(--td-size-7);
--td-comp-margin-xxl: var(--td-size-8);
--td-comp-margin-xxxl: var(--td-size-10);
--td-comp-margin-xxxxl: var(--td-size-12);
}

View File

@@ -393,13 +393,13 @@ const handleResize = () => {
const maxX = windowSize.value.width - 120
const maxY = windowSize.value.height - 176
const minY = 90 // 顶部边界限制
// 如果悬浮球在右侧,随窗口宽度变化更新位置
if (!isOnLeft.value) {
// 重新计算右侧位置
ballPosition.value.x = windowSize.value.width - 106
}
// 确保位置在有效范围内
ballPosition.value.x = Math.max(0, Math.min(ballPosition.value.x, maxX))
ballPosition.value.y = Math.max(minY, Math.min(ballPosition.value.y, maxY))

View File

@@ -128,7 +128,7 @@ const props = withDefaults(defineProps<Props>(), {
showDuration: true
})
const emit = defineEmits(['play', 'pause', 'addToPlaylist', 'download','scroll'])
const emit = defineEmits(['play', 'pause', 'addToPlaylist', 'download', 'scroll'])
// 虚拟滚动相关状态
const scrollContainer = ref<HTMLElement>()

View File

@@ -0,0 +1,328 @@
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import { ControlAudioStore } from '@renderer/store/ControlAudio'
import { storeToRefs } from 'pinia'
import audioManager from '@renderer/utils/audioManager'
interface Props {
show?: boolean
height?: number
barCount?: number
color?: string
backgroundColor?: string
}
// 定义事件
const emit = defineEmits<{
lowFreqUpdate: [volume: number]
}>()
const props = withDefaults(defineProps<Props>(), {
show: true,
height: 80,
barCount: 64,
color: 'rgba(255, 255, 255, 0.8)',
backgroundColor: 'transparent'
})
const canvasRef = ref<HTMLCanvasElement>()
const animationId = ref<number>()
const analyser = ref<AnalyserNode>()
const dataArray = ref<Uint8Array>()
const resizeObserver = ref<ResizeObserver>()
const componentId = ref<string>(`visualizer-${Date.now()}-${Math.random()}`)
const controlAudio = ControlAudioStore()
const { Audio } = storeToRefs(controlAudio)
// 初始化音频分析器
const initAudioAnalyser = () => {
if (!Audio.value.audio) return
try {
// 计算所需的 fftSize - 必须是 2 的幂次方
const minSize = props.barCount * 2
let fftSize = 32
while (fftSize < minSize) {
fftSize *= 2
}
fftSize = Math.min(fftSize, 2048) // 限制最大值
// 使用音频管理器创建分析器
const createdAnalyser = audioManager.createAnalyser(
Audio.value.audio,
componentId.value,
fftSize
)
analyser.value = createdAnalyser || undefined
if (analyser.value) {
// 创建数据数组,明确指定 ArrayBuffer 类型
const bufferLength = analyser.value.frequencyBinCount
dataArray.value = new Uint8Array(new ArrayBuffer(bufferLength))
console.log('音频分析器初始化成功')
} else {
console.warn('无法创建音频分析器,使用模拟数据')
// 创建一个默认大小的数据数组用于模拟数据
const bufferLength = fftSize / 2
dataArray.value = new Uint8Array(new ArrayBuffer(bufferLength))
}
} catch (error) {
console.error('音频分析器初始化失败:', error)
// 创建一个默认大小的数据数组用于模拟数据
dataArray.value = new Uint8Array(new ArrayBuffer(256))
}
}
// 绘制可视化
const draw = () => {
if (!canvasRef.value || !analyser.value || !dataArray.value) return
const canvas = canvasRef.value
const ctx = canvas.getContext('2d')
if (!ctx) return
// 获取频域数据或生成模拟数据
if (analyser.value && dataArray.value) {
// 有真实音频分析器,获取真实数据
analyser.value.getByteFrequencyData(dataArray.value as Uint8Array<ArrayBuffer>)
} else {
// 没有音频分析器,生成模拟数据
const time = Date.now() * 0.001
for (let i = 0; i < dataArray.value.length; i++) {
// 生成基于时间的模拟频谱数据
const frequency = i / dataArray.value.length
const amplitude = Math.sin(time * 2 + frequency * 10) * 0.5 + 0.5
const bass = Math.sin(time * 4) * 0.3 + 0.7 // 低频变化
dataArray.value[i] = Math.floor(amplitude * bass * 255 * (1 - frequency * 0.7))
}
}
// 计算低频音量 (80hz-120hz 范围)
// 假设采样率为 44100HzfftSize 为 256则每个频率 bin 约为 172Hz
// 80-120Hz 大约对应前 1-2 个 bin
const lowFreqStart = 0
const lowFreqEnd = Math.min(3, dataArray.value.length) // 取前几个低频 bin
let lowFreqSum = 0
for (let i = lowFreqStart; i < lowFreqEnd; i++) {
lowFreqSum += dataArray.value[i]
}
const lowFreqVolume = lowFreqSum / (lowFreqEnd - lowFreqStart) / 255
// 发送低频音量给父组件
emit('lowFreqUpdate', lowFreqVolume)
// 完全清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 如果有背景色,再填充背景
if (props.backgroundColor !== 'transparent') {
ctx.fillStyle = props.backgroundColor
ctx.fillRect(0, 0, canvas.width, canvas.height)
}
// 使用容器的实际尺寸进行计算,因为 ctx 已经缩放过了
const container = canvas.parentElement
if (!container) return
const containerRect = container.getBoundingClientRect()
const canvasWidth = containerRect.width
const canvasHeight = props.height
// 计算对称柱状图参数
const halfBarCount = Math.floor(props.barCount / 2)
const barWidth = canvasWidth / 2 / halfBarCount
const maxBarHeight = canvasHeight * 0.9
const centerX = canvasWidth / 2
// 绘制左右对称的频谱柱状图
for (let i = 0; i < halfBarCount; i++) {
// 增强低频响应,让可视化更敏感
let barHeight = (dataArray.value[i] / 255) * maxBarHeight
// 对数据进行增强处理,让变化更明显
barHeight = Math.pow(barHeight / maxBarHeight, 0.6) * maxBarHeight
const y = canvasHeight - barHeight
// 创建渐变色
const gradient = ctx.createLinearGradient(0, canvasHeight, 0, y)
gradient.addColorStop(0, props.color)
gradient.addColorStop(1, props.color.replace(/[\d\.]+\)$/g, '0.3)'))
ctx.fillStyle = gradient
// 绘制左侧柱状图(从中心向左)
const leftX = centerX - (i + 1) * barWidth
ctx.fillRect(leftX, y, barWidth, barHeight)
// 绘制右侧柱状图(从中心向右)
const rightX = centerX + i * barWidth
ctx.fillRect(rightX, y, barWidth, barHeight)
}
// 继续动画
if (props.show && Audio.value.isPlay) {
animationId.value = requestAnimationFrame(draw)
}
}
// 开始可视化
const startVisualization = () => {
if (!props.show || !Audio.value.isPlay) return
if (!analyser.value) {
initAudioAnalyser()
}
draw()
}
// 停止可视化
const stopVisualization = () => {
try {
if (animationId.value) {
cancelAnimationFrame(animationId.value)
animationId.value = undefined
}
} catch (error) {
console.warn('停止动画帧时出错:', error)
}
}
// 监听播放状态变化
watch(
() => Audio.value.isPlay,
(isPlaying) => {
if (isPlaying && props.show) {
startVisualization()
} else {
stopVisualization()
}
}
)
// 监听显示状态变化
watch(
() => props.show,
(show) => {
if (show && Audio.value.isPlay) {
startVisualization()
} else {
stopVisualization()
}
}
)
// 设置画布尺寸的函数
const resizeCanvas = () => {
if (!canvasRef.value) return
const canvas = canvasRef.value
const container = canvas.parentElement
if (!container) return
// 获取容器的实际尺寸
const containerRect = container.getBoundingClientRect()
const dpr = window.devicePixelRatio || 1
// 设置 canvas 的实际尺寸
canvas.width = containerRect.width * dpr
canvas.height = props.height * dpr
// 设置 CSS 尺寸
canvas.style.width = containerRect.width + 'px'
canvas.style.height = props.height + 'px'
const ctx = canvas.getContext('2d')
if (ctx) {
// 重置变换矩阵并重新缩放
ctx.setTransform(1, 0, 0, 1, 0, 0)
ctx.scale(dpr, dpr)
}
console.log('Canvas resized:', containerRect.width, 'x', props.height)
}
// 组件挂载
onMounted(() => {
if (canvasRef.value) {
resizeCanvas()
// 使用 ResizeObserver 监听容器尺寸变化
resizeObserver.value = new ResizeObserver((entries) => {
for (const _entry of entries) {
// 使用 nextTick 确保 DOM 更新完成
nextTick(() => {
resizeCanvas()
})
}
})
// 观察 canvas 元素的父容器
const container = canvasRef.value.parentElement
if (container) {
resizeObserver.value.observe(container)
}
}
if (Audio.value.audio && props.show && Audio.value.isPlay) {
initAudioAnalyser()
startVisualization()
}
})
// 组件卸载
onBeforeUnmount(() => {
console.log('AudioVisualizer 组件开始卸载')
// 停止可视化动画
stopVisualization()
// 清理音频上下文和相关资源
try {
// 只断开分析器连接,不断开共享的音频源
if (analyser.value) {
analyser.value.disconnect()
analyser.value = undefined
}
} catch (error) {
console.warn('清理音频资源时出错:', error)
}
// 断开 ResizeObserver
try {
if (resizeObserver.value) {
resizeObserver.value.disconnect()
resizeObserver.value = undefined
}
} catch (error) {
console.warn('断开 ResizeObserver 时出错:', error)
}
// 清理数据数组
dataArray.value = undefined
console.log('AudioVisualizer 组件卸载完成')
})
</script>
<template>
<div class="audio-visualizer" :style="{ height: `${height}px` }">
<canvas ref="canvasRef" class="visualizer-canvas" :style="{ height: `${height}px` }" />
</div>
</template>
<style lang="scss" scoped>
.audio-visualizer {
width: 100%;
position: relative;
overflow: hidden;
.visualizer-canvas {
width: 100%;
display: block;
border-radius: 4px;
}
}
</style>

View File

@@ -1,6 +1,7 @@
<!-- eslint-disable vue/require-toggle-inside-transition -->
<script lang="ts" setup>
import TitleBarControls from '../TitleBarControls.vue'
import AudioVisualizer from './AudioVisualizer.vue'
// import ShaderBackground from './ShaderBackground.vue'
import {
BackgroundRender,
@@ -23,13 +24,15 @@ interface Props {
show?: boolean
coverImage?: string
songId?: string | null
songInfo: SongList | { songmid: number | null }
songInfo: SongList | { songmid: number | null | string }
mainColor: string
}
const props = withDefaults(defineProps<Props>(), {
show: false,
coverImage: '@assets/images/Default.jpg',
songId: ''
songId: '',
mainColor: '#fff'
})
// 定义事件
const emit = defineEmits(['toggle-fullscreen'])
@@ -76,7 +79,8 @@ const state = reactive({
albumUrl: props.coverImage,
albumIsVideo: false,
currentTime: 0,
lyricLines: [] as LyricLine[]
lyricLines: [] as LyricLine[],
lowFreqVolume: 1.0
})
// 监听歌曲ID变化获取歌词
@@ -242,6 +246,11 @@ watch(
state.currentTime = Math.round(newTime * 1000)
}
)
// 处理低频音量更新
const handleLowFreqUpdate = (volume: number) => {
state.lowFreqVolume = volume
}
</script>
<template>
@@ -253,7 +262,6 @@ watch(
:album-is-video="false"
:fps="30"
:flow-speed="4"
:low-freq-volume="1"
:has-lyric="state.lyricLines.length > 10"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: -1"
/>
@@ -282,14 +290,10 @@ watch(
/>
</Transition>
<div class="playbox">
<!-- 播放控件内容
<div v-if="props.show" class="song-info">
<h1>当前播放</h1>
<p>这里将显示歌曲信息</p>
</div> -->
<div class="left" :style="state.lyricLines.length <= 0 && 'width:100vw'">
<div
class="box"
class="cd-container"
:class="{ playing: Audio.isPlay }"
:style="
!Audio.isPlay
? 'animation-play-state: paused;'
@@ -299,16 +303,15 @@ watch(
: '')
"
>
<t-image
:src="coverImage"
:style="
state.lyricLines.length > 0
? 'width: min(20vw, 380px); height: min(20vw, 380px)'
: 'width: 45vh; height: 45vh;transition: width 0.3s ease, height 0.3s ease; transition-delay: 1s;'
"
shape="circle"
class="cover"
/>
<!-- 黑胶唱片 -->
<div class="vinyl-record"></div>
<!-- 唱片标签 -->
<div class="vinyl-label">
<t-image :src="coverImage" shape="circle" class="cover" />
<div class="label-shine"></div>
</div>
<!-- 中心孔 -->
<div class="center-hole"></div>
</div>
</div>
<div v-show="state.lyricLines.length > 0" class="right">
@@ -316,8 +319,6 @@ watch(
ref="lyricPlayerRef"
:lyric-lines="props.show ? state.lyricLines : []"
:current-time="state.currentTime"
:align-position="0.38"
style="mix-blend-mode: plus-lighter"
class="lyric-player"
@line-click="
(e) => {
@@ -329,6 +330,16 @@ watch(
</LyricPlayer>
</div>
</div>
<!-- 音频可视化组件 -->
<div class="audio-visualizer-container" v-if="props.show && coverImage">
<AudioVisualizer
:show="props.show && Audio.isPlay"
:height="70"
:bar-count="80"
:color="mainColor"
@low-freq-update="handleLowFreqUpdate"
/>
</div>
</div>
</template>
@@ -429,17 +440,14 @@ watch(
.playbox {
width: 100%;
height: 100%;
// background-color: rgba(0, 0, 0, 0.256);
background-color: rgba(0, 0, 0, 0.256);
drop-filter: blur(10px);
padding: 0 10vw;
-webkit-drop-filter: blur(10px);
overflow: hidden;
display: flex;
position: relative;
:deep(.lyric-player) {
--amll-lyric-player-font-size: max(2vw, 29px);
box-sizing: border-box;
width: 100%;
height: 100%;
}
.left {
width: 40%;
transition: all 0.3s ease;
@@ -454,33 +462,246 @@ watch(
justify-content: center;
align-items: center;
margin: 0 0 var(--play-bottom-height) 0;
perspective: 1000px;
.box {
.cd-container {
width: min(30vw, 700px);
height: min(30vw, 700px);
background-color: #000000;
position: relative;
display: flex;
align-items: center;
justify-content: center;
border-radius: 1000%;
animation: rotateRecord 10s linear infinite;
animation: rotateRecord 33s linear infinite;
transition: filter 0.3s ease;
filter: drop-shadow(0 15px 35px rgba(0, 0, 0, 0.6));
:deep(.cover) img {
user-select: none;
-webkit-user-drag: none;
&:hover {
filter: drop-shadow(0 20px 45px rgba(0, 0, 0, 0.7));
}
/* 黑胶唱片主体 */
.vinyl-record {
width: 100%;
height: 100%;
position: relative;
border-radius: 50%;
background: radial-gradient(circle at 50% 50%, #1a1a1a 0%, #0d0d0d 100%);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
/* 唱片纹理轨道 */
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background:
repeating-conic-gradient(
from 0deg,
transparent 0deg,
rgba(255, 255, 255, 0.02) 0.5deg,
transparent 1deg,
rgba(255, 255, 255, 0.01) 1.5deg,
transparent 2deg
),
repeating-radial-gradient(
circle at 50% 50%,
transparent 0px,
rgba(255, 255, 255, 0.03) 1px,
transparent 2px,
transparent 8px
);
z-index: 1;
}
/* 唱片光泽效果 */
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(
ellipse at 30% 30%,
rgba(255, 255, 255, 0.08) 0%,
rgba(255, 255, 255, 0.04) 25%,
rgba(255, 255, 255, 0.02) 50%,
rgba(255, 255, 255, 0.01) 75%,
transparent 100%
);
border-radius: 50%;
z-index: 2;
animation: vinylShine 6s ease-in-out infinite;
}
}
/* 唱片标签区域 */
.vinyl-label {
position: absolute;
width: 60%;
height: 60%;
background: radial-gradient(
circle at 50% 50%,
rgba(40, 40, 40, 0.95) 0%,
rgba(25, 25, 25, 0.98) 70%,
rgba(15, 15, 15, 1) 100%
);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 3;
box-shadow:
inset 0 0 20px rgba(0, 0, 0, 0.8),
inset 0 0 0 1px rgba(255, 255, 255, 0.05),
0 0 10px rgba(0, 0, 0, 0.5);
:deep(.cover) {
position: relative;
z-index: 4;
border-radius: 50%;
overflow: hidden;
box-shadow:
0 0 20px rgba(0, 0, 0, 0.4),
inset 0 0 0 2px rgba(255, 255, 255, 0.1);
width: 95% !important;
height: 95% !important;
aspect-ratio: 1 / 1;
img {
user-select: none;
-webkit-user-drag: none;
border-radius: 50%;
filter: brightness(0.85) contrast(1.15) saturate(1.1);
width: 100% !important;
height: 100% !important;
object-fit: cover;
}
}
/* 标签光泽 */
.label-shine {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(
ellipse at 25% 25%,
rgba(255, 255, 255, 0.1) 0%,
transparent 50%
);
border-radius: 50%;
z-index: 5;
pointer-events: none;
animation: labelShine 8s ease-in-out infinite;
}
}
/* 中心孔 */
.center-hole {
position: absolute;
width: 8%;
height: 8%;
background: radial-gradient(circle, #000 0%, #111 30%, #222 70%, #333 100%);
border-radius: 50%;
z-index: 10;
box-shadow:
inset 0 0 8px rgba(0, 0, 0, 0.9),
0 0 3px rgba(0, 0, 0, 0.8);
}
}
}
.right {
:deep(.lyric-player) {
font-family: lyricfont;
--amll-lyric-player-font-size: min(2.6vw, 32px);
// bottom: max(2vw, 29px);
height: 200%;
transform: translateY(-25%);
& > div {
padding-bottom: 0;
overflow: hidden;
transform: translateY(-20px);
}
}
padding: 0 20px;
padding-top: 90px;
margin: 80px 0 calc(var(--play-bottom-height)) 0;
overflow: hidden;
}
}
.audio-visualizer-container {
position: absolute;
bottom: calc(var(--play-bottom-height) - 10px);
z-index: 9999;
left: 0;
right: 0;
height: 60px;
// background: linear-gradient(to top, rgba(0, 0, 0, 0.3), transparent);
filter: blur(6px);
// max-width: 1000px;
// -webkit-backdrop-filter: blur(10px);
// border-top: 1px solid rgba(255, 255, 255, 0.1);
z-index: 5;
// padding: 0 20px;
display: flex;
align-items: center;
}
}
@keyframes rotateRecord {
to {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes vinylShine {
0% {
opacity: 0.1;
transform: rotate(0deg) scale(1);
}
50% {
opacity: 0.2;
transform: rotate(180deg) scale(1.1);
}
100% {
opacity: 0.1;
transform: rotate(360deg) scale(1);
}
}
@keyframes labelShine {
0% {
opacity: 0.05;
transform: rotate(0deg);
}
25% {
opacity: 0.15;
}
50% {
opacity: 0.1;
transform: rotate(180deg);
}
75% {
opacity: 0.15;
}
100% {
opacity: 0.05;
transform: rotate(360deg);
}
}

View File

@@ -8,7 +8,8 @@ import {
nextTick,
onActivated,
onDeactivated,
toRaw
toRaw,
provide
} from 'vue'
import { ControlAudioStore } from '@renderer/store/ControlAudio'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
@@ -25,6 +26,7 @@ import {
destroyPlaylistEventListeners,
getSongRealUrl
} from '@renderer/utils/playlistManager'
import defaultCoverImg from '/default-cover.png'
const controlAudio = ControlAudioStore()
const localUserStore = LocalUserDetailStore()
@@ -34,7 +36,7 @@ const { setCurrentTime, start, stop, setVolume, setUrl } = controlAudio
const showFullPlay = ref(false)
document.addEventListener('keydown', KeyEvent)
// 处理最小化右键的事件
window.api.onMusicCtrl(() => {
const removeMusicCtrlListener = window.api.onMusicCtrl(() => {
togglePlayPause()
})
let timer: any = null
@@ -118,7 +120,7 @@ const waitForAudioReady = (): Promise<void> => {
// 存储待恢复的播放位置
let pendingRestorePosition = 0
let pendingRestoreSongId: number | null = null
let pendingRestoreSongId: number | string | null = null
// 记录组件被停用前的播放状态
let wasPlaying = false
@@ -226,7 +228,7 @@ const playSong = async (song: SongList) => {
MessagePlugin.error('播放失败,原因:' + error.message)
}
}
provide('PlaySong', playSong)
// 歌曲信息
// const playMode = ref(userInfo.value.playMode || PlayMode.SEQUENCE)
const playMode = ref(PlayMode.SEQUENCE)
@@ -377,13 +379,10 @@ const playNext = async () => {
if (Audio.value.audio) {
Audio.value.audio.currentTime = 0
}
// 如果当前正在播放,继续播放;如果暂停,保持暂停
if (Audio.value.isPlay) {
const startResult = start()
if (startResult && typeof startResult.then === 'function') {
await startResult
}
const startResult = start()
if (startResult && typeof startResult.then === 'function') {
await startResult
}
return
}
@@ -422,6 +421,7 @@ onMounted(async () => {
// 监听音频结束事件,根据播放模式播放下一首
unEnded = controlAudio.subscribe('ended', () => {
window.requestAnimationFrame(() => {
console.log('播放结束')
playNext()
})
})
@@ -473,6 +473,9 @@ onUnmounted(() => {
if (savePositionInterval !== null) {
clearInterval(savePositionInterval)
}
if (removeMusicCtrlListener) {
removeMusicCtrlListener()
}
unEnded()
})
@@ -668,13 +671,13 @@ const handleProgressDragStart = (event: MouseEvent) => {
}
// 歌曲信息
const songInfo = ref<Omit<SongList, 'songmid'> & { songmid: null | number }>({
const songInfo = ref<Omit<SongList, 'songmid'> & { songmid: null | number | string }>({
songmid: null,
hash: '',
name: '欢迎使用CeruMusic 🎉',
singer: '可以配置音源插件来播放你的歌曲',
albumName: '',
albumId: 0,
albumId: '0',
source: '',
interval: '00:00',
img: '',
@@ -700,20 +703,41 @@ async function setColor() {
playbg.value = 'rgba(255,255,255,0.2)'
playbghover.value = 'rgba(255,255,255,0.33)'
}
const bg = ref('#ffffff46')
watch(
songInfo,
async (newVal) => {
bg.value = bg.value === '#ffffff' ? '#ffffff46' : toRaw(bg.value)
if (newVal.img) {
await setColor()
} else if (songInfo.value.songmid) {
songInfo.value.img = defaultCoverImg
await setColor()
} else {
bg.value = '#ffffff'
}
},
{ deep: true, immediate: true }
)
watch(showFullPlay, (val) => {
if (val) {
console.log('背景hei')
bg.value = '#00000020'
} else {
bg.value = '#ffffff46'
}
})
// onMounted(setColor)
</script>
<template>
<div class="player-container" @click.stop="toggleFullPlay">
<div
class="player-container"
:style="!showFullPlay && 'box-shadow: none'"
@click.stop="toggleFullPlay"
>
<!-- 进度条 -->
<div class="progress-bar-container">
<div
@@ -731,8 +755,9 @@ watch(
<div class="player-content">
<!-- 左侧封面和歌曲信息 -->
<div class="left-section">
<div class="album-cover" v-show="songInfo.img">
<img :src="songInfo.img" alt="专辑封面" />
<div class="album-cover" v-if="songInfo.albumId">
<img :src="songInfo.img" alt="专辑封面" v-if="songInfo.img" />
<img :src="defaultCoverImg" alt="默认封面" />
</div>
<div class="song-info">
@@ -828,6 +853,7 @@ watch(
:cover-image="songInfo.img"
@toggle-fullscreen="toggleFullPlay"
:song-info="songInfo"
:main-color="maincolor"
/>
</div>
@@ -901,11 +927,13 @@ watch(
}
.player-container {
box-shadow: 0px -2px 20px 0px #00000039;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #ffffff31;
transition: background 0.3s;
background: v-bind(bg);
// border-top: 1px solid #e5e7eb;
backdrop-filter: blur(1000px);
z-index: 1000;
@@ -939,7 +967,7 @@ watch(
left: 0;
right: 0;
height: 100%;
background: #ffffff71;
background: transparent;
}
.progress-filled {
@@ -991,6 +1019,7 @@ watch(
align-items: center;
min-width: 0;
flex: 1;
padding-top: 2px;
.album-cover {
width: 50px;

View File

@@ -62,6 +62,7 @@ import ThemeSelector from '@/components/ThemeSelector.vue'
组件通过以下方式实现主题切换:
1. **默认主题**: 移除 `theme-mode` 属性
```javascript
document.documentElement.removeAttribute('theme-mode')
```
@@ -73,13 +74,13 @@ import ThemeSelector from '@/components/ThemeSelector.vue'
## 支持的主题
| 主题名称 | 属性值 | 主色调 |
|---------|--------|--------|
| 默认 | `default` | #57b4ff |
| 粉色 | `pink` | #fc5e7e |
| 蓝色 | `blue` | #57b4ff |
| 青色 | `cyan` | #3ac2b8 |
| 橙色 | `orange` | #fb9458 |
| 主题名称 | 属性值 | 主色调 |
| -------- | --------- | ------- |
| 默认 | `default` | #57b4ff |
| 粉色 | `pink` | #fc5e7e |
| 蓝色 | `blue` | #57b4ff |
| 青色 | `cyan` | #3ac2b8 |
| 橙色 | `orange` | #fb9458 |
## 自定义配置
@@ -90,7 +91,7 @@ import ThemeSelector from '@/components/ThemeSelector.vue'
在 `src/renderer/src/assets/theme/` 目录下创建新的主题文件,例如 `green.css`
```css
:root[theme-mode="green"] {
:root[theme-mode='green'] {
--td-brand-color: #10b981;
--td-brand-color-hover: #059669;
/* 其他主题变量... */
@@ -158,4 +159,4 @@ import ThemeDemo from '@/components/ThemeDemo.vue'
</script>
```
这个演示组件展示了不同UI元素在各种主题下的表现。
这个演示组件展示了不同UI元素在各种主题下的表现。

View File

@@ -1,17 +1,15 @@
<template>
<div class="float-ball-settings">
<t-card hover-shadow title="AI悬浮球设置">
<div class="card-body">
<div class="setting-item">
<span class="setting-label">显示AI悬浮球</span>
<t-switch v-model="showFloatBall" @change="handleFloatBallToggle" />
</div>
<div class="setting-description">
<p>开启后AI悬浮球将显示在应用界面上您可以随时与AI助手交流</p>
<p>关闭后AI悬浮球将被隐藏您可以随时在此处重新开启</p>
</div>
<div class="card-body">
<div class="setting-item">
<span class="setting-label">显示AI悬浮球</span>
<t-switch v-model="showFloatBall" @change="handleFloatBallToggle" />
</div>
</t-card>
<div class="setting-description">
<p>开启后AI悬浮球将显示在应用界面上您可以随时与AI助手交流</p>
<p>关闭后AI悬浮球将被隐藏您可以随时在此处重新开启</p>
</div>
</div>
</div>
</template>
@@ -78,4 +76,4 @@ onMounted(() => {
font-size: 14px;
color: var(--td-text-color-secondary);
}
</style>
</style>

View File

@@ -1,25 +1,21 @@
<template>
<div class="music-cache">
<t-card hover-shadow :loading="cacheInfo ? false : true" title="本地歌曲缓存配置">
<template #actions>
已有歌曲缓存大小{{ cacheInfo.sizeFormatted }}
</template>
<template #actions> 已有歌曲缓存大小{{ cacheInfo.sizeFormatted }} </template>
<div class="card-body">
<t-button size="large" @click="clearCache">
清除本地缓存
</t-button>
<t-button size="large" @click="clearCache"> 清除本地缓存 </t-button>
</div>
</t-card>
</div>
</template>
<script lang="ts" setup>
import { DialogPlugin } from 'tdesign-vue-next';
import { onMounted, ref } from 'vue';
import { DialogPlugin } from 'tdesign-vue-next'
import { onMounted, ref } from 'vue'
const cacheInfo: any = ref({})
onMounted(() => {
window.api.musicCache.getInfo().then(res => cacheInfo.value = res)
window.api.musicCache.getInfo().then((res) => (cacheInfo.value = res))
})
const clearCache = () => {
const confirm = DialogPlugin.confirm({
@@ -27,7 +23,7 @@ const clearCache = () => {
body: '这可能会导致歌曲加载缓慢,你确定要清除所有缓存吗?',
confirmBtn: '确定清除',
cancelBtn: '我再想想',
placement:'center',
placement: 'center',
onClose: () => {
confirm.hide()
},
@@ -35,8 +31,7 @@ const clearCache = () => {
confirm.hide()
cacheInfo.value = {}
await window.api.musicCache.clear()
window.api.musicCache.getInfo().then(res => cacheInfo.value = res)
window.api.musicCache.getInfo().then((res) => (cacheInfo.value = res))
}
})
}
@@ -51,4 +46,4 @@ const clearCache = () => {
text-align: center;
}
}
</style>
</style>

View File

@@ -4,11 +4,7 @@
<h3>自动更新</h3>
<div class="update-info">
<p>当前版本: {{ currentVersion }}</p>
<t-button
theme="primary"
:loading="isChecking"
@click="handleCheckUpdate"
>
<t-button theme="primary" :loading="isChecking" @click="handleCheckUpdate">
{{ isChecking ? '检查中...' : '检查更新' }}
</t-button>
</div>
@@ -70,4 +66,4 @@ onMounted(() => {
margin: 0;
color: var(--td-text-color-secondary);
}
</style>
</style>

View File

@@ -5,24 +5,36 @@
<div class="theme-color-preview" :style="{ backgroundColor: currentThemeColor }"></div>
<span class="theme-name">{{ currentThemeName }}</span>
</div>
<svg class="dropdown-icon" :class="{ 'rotated': isDropdownOpen }" viewBox="0 0 24 24" width="16" height="16">
<path d="M7 10l5 5 5-5z" fill="currentColor"/>
<svg
class="dropdown-icon"
:class="{ rotated: isDropdownOpen }"
viewBox="0 0 24 24"
width="16"
height="16"
>
<path d="M7 10l5 5 5-5z" fill="currentColor" />
</svg>
</div>
<transition name="dropdown">
<div v-if="isDropdownOpen" class="theme-dropdown">
<div
v-for="theme in themes"
<div
v-for="theme in themes"
:key="theme.name"
class="theme-option"
:class="{ 'active': currentTheme === theme.name }"
:class="{ active: currentTheme === theme.name }"
@click="selectTheme(theme.name)"
>
<div class="theme-color-dot" :style="{ backgroundColor: theme.color }"></div>
<span class="theme-label">{{ theme.label }}</span>
<svg v-if="currentTheme === theme.name" class="check-icon" viewBox="0 0 24 24" width="16" height="16">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" fill="currentColor"/>
<svg
v-if="currentTheme === theme.name"
class="check-icon"
viewBox="0 0 24 24"
width="16"
height="16"
>
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" fill="currentColor" />
</svg>
</div>
</div>
@@ -47,7 +59,7 @@ const themes = [
const loadSavedTheme = () => {
const savedTheme = localStorage.getItem('selected-theme')
if (savedTheme && themes.some(t => t.name === savedTheme)) {
if (savedTheme && themes.some((t) => t.name === savedTheme)) {
currentTheme.value = savedTheme
applyTheme(savedTheme)
}
@@ -55,26 +67,26 @@ const loadSavedTheme = () => {
const applyTheme = (themeName) => {
const documentElement = document.documentElement
// 移除之前的主题
documentElement.removeAttribute('theme-mode')
// 应用新主题(如果不是默认主题)
if (themeName !== 'default') {
documentElement.setAttribute('theme-mode', themeName)
}
// 保存到本地存储
localStorage.setItem('selected-theme', themeName)
}
const currentThemeColor = computed(() => {
const theme = themes.find(t => t.name === currentTheme.value)
const theme = themes.find((t) => t.name === currentTheme.value)
return theme ? theme.color : '#2ba55b'
})
const currentThemeName = computed(() => {
const theme = themes.find(t => t.name === currentTheme.value)
const theme = themes.find((t) => t.name === currentTheme.value)
return theme ? theme.label : '默认'
})
@@ -93,10 +105,6 @@ const selectTheme = (themeName) => {
isDropdownOpen.value = false
}
const handleClickOutside = (event) => {
const themeSelector = event.target.closest('.theme-selector')
if (!themeSelector) {
@@ -178,7 +186,11 @@ onUnmounted(() => {
background: var(--td-bg-color-container, #ffffff);
border: 1px solid var(--td-component-border, #e2e8f0);
border-radius: var(--td-radius-medium, 6px);
box-shadow: var(--td-shadow-2, 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06));
box-shadow: var(
--td-shadow-2,
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06)
);
z-index: 1000;
overflow: hidden;
}
@@ -241,11 +253,11 @@ onUnmounted(() => {
min-width: 100px;
padding: 6px 10px;
}
.theme-name {
font-size: 13px;
}
.theme-option {
padding: 10px 14px;
}

View File

@@ -9,9 +9,7 @@
</t-button>
<!-- 测试按钮 -->
<t-button theme="default" @click="testProgress">
测试进度显示
</t-button>
<t-button theme="default" @click="testProgress"> 测试进度显示 </t-button>
</div>
<!-- 显示当前下载状态 -->
@@ -142,4 +140,4 @@ const formatBytes = (bytes: number): string => {
margin: 4px 0;
color: #666;
}
</style>
</style>

View File

@@ -1,25 +1,25 @@
import { autoUpdateService, downloadState } from '../services/autoUpdateService';
import { autoUpdateService, downloadState } from '../services/autoUpdateService'
export function useAutoUpdate() {
// 检查更新
const checkForUpdates = async () => {
await autoUpdateService.checkForUpdates();
};
await autoUpdateService.checkForUpdates()
}
// 下载更新
const downloadUpdate = async () => {
await autoUpdateService.downloadUpdate();
};
await autoUpdateService.downloadUpdate()
}
// 安装更新
const quitAndInstall = async () => {
await autoUpdateService.quitAndInstall();
};
await autoUpdateService.quitAndInstall()
}
return {
checkForUpdates,
downloadUpdate,
quitAndInstall,
downloadState // 导出下载状态供组件使用
};
}
}
}

View File

@@ -1,5 +1,5 @@
import './assets/base.css'
import 'animate.css';
import 'animate.css'
// 引入组件库的少量全局样式变量
// import 'tdesign-vue-next/es/style/index.css' //tdesign 组件样式

View File

@@ -42,8 +42,8 @@ let routes: RouteRecordRaw[] = [
path: '/settings',
name: 'settings',
meta: {
transitionIn: "animate__fadeIn",
transitionOut: "animate__fadeOut",
transitionIn: 'animate__fadeIn',
transitionOut: 'animate__fadeOut'
},
component: () => import('@renderer/views/settings/index.vue')
},
@@ -55,14 +55,14 @@ let routes: RouteRecordRaw[] = [
]
function setAnimate(routerObj: RouteRecordRaw[]) {
for (let i = 0; i < routerObj.length; i++) {
let item = routerObj[i];
let item = routerObj[i]
if (item.children && item.children.length > 0) {
setAnimate(item.children);
setAnimate(item.children)
} else {
if (item.meta) continue
item.meta = item.meta || {};
item.meta.transitionIn = 'animate__fadeInRight';
item.meta.transitionOut = 'animate__fadeOutLeft';
item.meta = item.meta || {}
item.meta.transitionIn = 'animate__fadeInRight'
item.meta.transitionOut = 'animate__fadeOutLeft'
}
}
}
@@ -72,11 +72,11 @@ const option: RouterOptions = {
routes,
scrollBehavior(_to_, _from_, savedPosition) {
if (savedPosition) {
return savedPosition;
return savedPosition
} else {
return { top: 0 };
return { top: 0 }
}
},
}
}
const router = createRouter(option)

View File

@@ -1,18 +1,18 @@
import { NotifyPlugin, DialogPlugin } from 'tdesign-vue-next';
import { NotifyPlugin, DialogPlugin } from 'tdesign-vue-next'
import { reactive } from 'vue';
import { reactive } from 'vue'
export interface DownloadProgress {
percent: number;
transferred: number;
total: number;
percent: number
transferred: number
total: number
}
export interface UpdateInfo {
url: string;
name: string;
notes: string;
pub_date: string;
url: string
name: string
notes: string
pub_date: string
}
// 响应式的下载状态
@@ -24,109 +24,109 @@ export const downloadState = reactive({
total: 0
} as DownloadProgress,
updateInfo: null as UpdateInfo | null
});
})
export class AutoUpdateService {
private static instance: AutoUpdateService;
private isListening = false;
private static instance: AutoUpdateService
private isListening = false
constructor() {
// 构造函数中自动开始监听
this.startListening();
this.startListening()
}
static getInstance(): AutoUpdateService {
if (!AutoUpdateService.instance) {
AutoUpdateService.instance = new AutoUpdateService();
AutoUpdateService.instance = new AutoUpdateService()
}
return AutoUpdateService.instance;
return AutoUpdateService.instance
}
// 开始监听更新消息
startListening() {
if (this.isListening) return;
this.isListening = true;
if (this.isListening) return
this.isListening = true
// 监听各种更新事件
window.api.autoUpdater.onCheckingForUpdate(() => {
this.showCheckingNotification();
});
window.api.autoUpdater.onUpdateAvailable((_,updateInfo: UpdateInfo) => {
this.showUpdateAvailableDialog(updateInfo);
});
this.showCheckingNotification()
})
window.api.autoUpdater.onUpdateAvailable((_, updateInfo: UpdateInfo) => {
this.showUpdateAvailableDialog(updateInfo)
})
window.api.autoUpdater.onUpdateNotAvailable(() => {
this.showNoUpdateNotification();
});
this.showNoUpdateNotification()
})
window.api.autoUpdater.onDownloadStarted((updateInfo: UpdateInfo) => {
this.handleDownloadStarted(updateInfo);
});
this.handleDownloadStarted(updateInfo)
})
window.api.autoUpdater.onDownloadProgress((progress: DownloadProgress) => {
console.log(progress)
this.showDownloadProgressNotification(progress);
});
this.showDownloadProgressNotification(progress)
})
window.api.autoUpdater.onUpdateDownloaded(() => {
this.showUpdateDownloadedDialog();
});
window.api.autoUpdater.onError((_,error: string) => {
this.showUpdateErrorNotification(error);
});
this.showUpdateDownloadedDialog()
})
window.api.autoUpdater.onError((_, error: string) => {
this.showUpdateErrorNotification(error)
})
}
// 停止监听更新消息
stopListening() {
if (!this.isListening) return;
this.isListening = false;
window.api.autoUpdater.removeAllListeners();
if (!this.isListening) return
this.isListening = false
window.api.autoUpdater.removeAllListeners()
}
// 检查更新
async checkForUpdates() {
try {
await window.api.autoUpdater.checkForUpdates();
await window.api.autoUpdater.checkForUpdates()
} catch (error) {
console.error('检查更新失败:', error);
console.error('检查更新失败:', error)
NotifyPlugin.error({
title: '更新检查失败',
content: '无法检查更新,请稍后重试',
duration: 3000
});
})
}
}
// 下载更新
async downloadUpdate() {
try {
await window.api.autoUpdater.downloadUpdate();
await window.api.autoUpdater.downloadUpdate()
} catch (error) {
console.error('下载更新失败:', error);
console.error('下载更新失败:', error)
NotifyPlugin.error({
title: '下载更新失败',
content: '无法下载更新,请稍后重试',
duration: 3000
});
})
}
}
// 安装更新
async quitAndInstall() {
try {
await window.api.autoUpdater.quitAndInstall();
await window.api.autoUpdater.quitAndInstall()
} catch (error) {
console.error('安装更新失败:', error);
console.error('安装更新失败:', error)
NotifyPlugin.error({
title: '安装更新失败',
content: '无法安装更新,请稍后重试',
duration: 3000
});
})
}
}
@@ -136,29 +136,29 @@ export class AutoUpdateService {
title: '检查更新',
content: '正在检查是否有新版本...',
duration: 2000
});
})
}
// 显示有更新可用对话框
private showUpdateAvailableDialog(updateInfo: UpdateInfo) {
// 保存更新信息到状态中
downloadState.updateInfo = updateInfo;
downloadState.updateInfo = updateInfo
console.log(updateInfo)
const releaseDate = new Date(updateInfo.pub_date).toLocaleDateString('zh-CN');
const dialog =DialogPlugin.confirm({
const releaseDate = new Date(updateInfo.pub_date).toLocaleDateString('zh-CN')
const dialog = DialogPlugin.confirm({
header: `发现新版本 ${updateInfo.name}`,
body: `发布时间: ${releaseDate}\n\n更新说明:\n${updateInfo.notes || '暂无更新说明'}\n\n是否立即下载此更新`,
confirmBtn: '立即下载',
cancelBtn: '稍后提醒',
onConfirm: () => {
this.downloadUpdate();
this.downloadUpdate()
dialog.hide()
},
onCancel: () => {
console.log('用户选择稍后下载更新');
console.log('用户选择稍后下载更新')
}
});
})
}
// 显示无更新通知
@@ -167,49 +167,51 @@ export class AutoUpdateService {
title: '已是最新版本',
content: '当前已是最新版本,无需更新',
duration: 3000
});
})
}
// 处理下载开始事件
private handleDownloadStarted(updateInfo: UpdateInfo) {
downloadState.isDownloading = true;
downloadState.updateInfo = updateInfo;
downloadState.isDownloading = true
downloadState.updateInfo = updateInfo
downloadState.progress = {
percent: 0,
transferred: 0,
total: 0
};
console.log('开始下载更新:', updateInfo.name);
}
console.log('开始下载更新:', updateInfo.name)
}
// 更新下载进度状态
private showDownloadProgressNotification(progress: DownloadProgress) {
// 更新响应式状态
downloadState.isDownloading = true;
downloadState.progress = progress;
console.log(`下载进度: ${Math.round(progress.percent)}% (${this.formatBytes(progress.transferred)} / ${this.formatBytes(progress.total)})`);
downloadState.isDownloading = true
downloadState.progress = progress
console.log(
`下载进度: ${Math.round(progress.percent)}% (${this.formatBytes(progress.transferred)} / ${this.formatBytes(progress.total)})`
)
}
// 显示更新下载完成对话框
private showUpdateDownloadedDialog() {
// 更新下载状态
downloadState.isDownloading = false;
downloadState.progress.percent = 100;
downloadState.isDownloading = false
downloadState.progress.percent = 100
DialogPlugin.confirm({
header: '更新下载完成',
body: '新版本已下载完成,是否立即重启应用以完成更新?',
confirmBtn: '立即重启',
cancelBtn: '稍后重启',
onConfirm: () => {
this.quitAndInstall();
this.quitAndInstall()
},
onCancel: () => {
console.log('用户选择稍后重启');
console.log('用户选择稍后重启')
}
});
})
}
// 显示更新错误通知
@@ -218,20 +220,20 @@ export class AutoUpdateService {
title: '更新失败',
content: `更新过程中出现错误: ${error}`,
duration: 5000
});
})
}
// 格式化字节大小
private formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
}
// 导出单例实例
export const autoUpdateService = AutoUpdateService.getInstance();
export const autoUpdateService = AutoUpdateService.getInstance()

View File

@@ -79,12 +79,16 @@ export const LocalUserDetailStore = defineStore('Local', () => {
return list.value
}
function removeSong(songId: number) {
function removeSong(songId: number | string) {
const index = list.value.findIndex((item) => item.songmid === songId)
if (index !== -1) {
list.value.splice(index, 1)
}
}
function clearList() {
list.value = []
}
const userSource = computed(() => {
return {
pluginId: userInfo.value.pluginId,
@@ -100,6 +104,7 @@ export const LocalUserDetailStore = defineStore('Local', () => {
addSong,
addSongToFirst,
removeSong,
clearList,
userSource
}
})

View File

@@ -16,7 +16,7 @@ export const useSettingsStore = defineStore('settings', () => {
} catch (error) {
console.error('加载设置失败:', error)
}
// 默认设置
return {
showFloatBall: true
@@ -47,4 +47,4 @@ export const useSettingsStore = defineStore('settings', () => {
updateSettings,
toggleFloatBall
}
})
})

View File

@@ -1,3 +1,5 @@
import playList from '@common/types/playList'
// 音频事件相关类型定义
// 事件回调函数类型定义
@@ -44,18 +46,4 @@ export type ControlAudioState = {
url: string
}
export type SongList = {
songmid: number
hash?: string
singer: string
name: string
albumName: string
albumId: number
source: string
interval: string
img: string
lrc: null | string
types: string[]
_types: Record<string, any>
typeUrl: Record<string, any>
}
export type SongList = playList

View File

@@ -2,7 +2,7 @@ import { PlayMode } from './audio'
import { Sources } from './Sources'
export interface UserInfo {
lastPlaySongId?: number | null
lastPlaySongId?: number | string | null
currentTime?: number
volume?: number
topBarStyle?: boolean

View File

@@ -0,0 +1,122 @@
// 全局音频管理器,用于管理音频源和分析器
class AudioManager {
private static instance: AudioManager
private audioSources = new WeakMap<HTMLAudioElement, MediaElementAudioSourceNode>()
private audioContexts = new WeakMap<HTMLAudioElement, AudioContext>()
private analysers = new Map<string, AnalyserNode>()
static getInstance(): AudioManager {
if (!AudioManager.instance) {
AudioManager.instance = new AudioManager()
}
return AudioManager.instance
}
// 获取或创建音频源
getOrCreateAudioSource(
audioElement: HTMLAudioElement
): { source: MediaElementAudioSourceNode; context: AudioContext } | null {
try {
// 检查是否已经有音频源
let source = this.audioSources.get(audioElement)
let context = this.audioContexts.get(audioElement)
if (!source || !context || context.state === 'closed') {
// 创建新的音频上下文和源
context = new (window.AudioContext || (window as any).webkitAudioContext)()
source = context.createMediaElementSource(audioElement)
// 连接到输出,确保音频能正常播放
source.connect(context.destination)
// 存储引用
this.audioSources.set(audioElement, source)
this.audioContexts.set(audioElement, context)
console.log('AudioManager: 创建新的音频源和上下文')
}
return { source, context }
} catch (error) {
console.error('AudioManager: 创建音频源失败:', error)
return null
}
}
// 创建分析器
createAnalyser(
audioElement: HTMLAudioElement,
id: string,
fftSize: number = 256
): AnalyserNode | null {
const audioData = this.getOrCreateAudioSource(audioElement)
if (!audioData) return null
const { source, context } = audioData
try {
// 创建分析器
const analyser = context.createAnalyser()
analyser.fftSize = fftSize
analyser.smoothingTimeConstant = 0.6
// 创建增益节点作为中介,避免直接断开主音频链
const gainNode = context.createGain()
gainNode.gain.value = 1.0
// 连接source -> gainNode -> analyser
// -> destination (保持音频播放)
source.disconnect() // 先断开所有连接
source.connect(gainNode)
gainNode.connect(context.destination) // 确保音频继续播放
gainNode.connect(analyser) // 连接到分析器
// 存储分析器引用
this.analysers.set(id, analyser)
console.log('AudioManager: 创建分析器成功')
return analyser
} catch (error) {
console.error('AudioManager: 创建分析器失败:', error)
return null
}
}
// 移除分析器
removeAnalyser(id: string): void {
const analyser = this.analysers.get(id)
if (analyser) {
try {
analyser.disconnect()
this.analysers.delete(id)
console.log('AudioManager: 移除分析器成功')
} catch (error) {
console.warn('AudioManager: 移除分析器时出错:', error)
}
}
}
// 清理音频元素的所有资源
cleanupAudioElement(audioElement: HTMLAudioElement): void {
try {
const context = this.audioContexts.get(audioElement)
if (context && context.state !== 'closed') {
context.close()
}
this.audioSources.delete(audioElement)
this.audioContexts.delete(audioElement)
console.log('AudioManager: 清理音频元素资源')
} catch (error) {
console.warn('AudioManager: 清理资源时出错:', error)
}
}
// 获取分析器
getAnalyser(id: string): AnalyserNode | undefined {
return this.analysers.get(id)
}
}
export default AudioManager.getInstance()

View File

@@ -1,4 +1,92 @@
import { extractDominantColor } from './colorExtractor'
import DefaultCover from '@renderer/assets/images/Default.jpg'
import CoverImage from '@renderer/assets/images/cover.png'
/**
* 直接从图片分析平均亮度,不依赖颜色提取器
* @param imageSrc 图片路径
* @returns 返回平均亮度值 (0-1)
*/
async function getImageAverageLuminance(imageSrc: string): Promise<number> {
return new Promise((resolve, reject) => {
// 处理相对路径
let actualSrc = imageSrc
if (
imageSrc.includes('@assets/images/Default.jpg') ||
imageSrc.includes('@renderer/assets/images/Default.jpg')
) {
actualSrc = DefaultCover
} else if (
imageSrc.includes('@assets/images/cover.png') ||
imageSrc.includes('@renderer/assets/images/cover.png')
) {
actualSrc = CoverImage
}
// 如果仍然是相对路径,使用默认亮度
if (actualSrc.startsWith('@')) {
console.warn('无法解析相对路径,使用默认亮度')
resolve(0.5) // 中等亮度
return
}
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = () => {
try {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) {
reject(new Error('无法创建canvas上下文'))
return
}
// 使用较小的采样尺寸提高性能
const size = 50
canvas.width = size
canvas.height = size
ctx.drawImage(img, 0, 0, size, size)
const imageData = ctx.getImageData(0, 0, size, size).data
let totalLuminance = 0
let pixelCount = 0
// 计算所有非透明像素的平均亮度
for (let i = 0; i < imageData.length; i += 4) {
// 忽略透明像素
if (imageData[i + 3] < 128) continue
const r = imageData[i] / 255
const g = imageData[i + 1] / 255
const b = imageData[i + 2] / 255
// 使用WCAG 2.0相对亮度公式
const R = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4)
const G = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4)
const B = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4)
const luminance = 0.2126 * R + 0.7152 * G + 0.0722 * B
totalLuminance += luminance
pixelCount++
}
const averageLuminance = pixelCount > 0 ? totalLuminance / pixelCount : 0.5
resolve(averageLuminance)
} catch (error) {
console.error('分析图片亮度失败:', error)
resolve(0.5) // 默认中等亮度
}
}
img.onerror = () => {
console.error('图片加载失败:', actualSrc)
resolve(0.5) // 默认中等亮度
}
img.src = actualSrc
})
}
/**
* 判断图片应该使用黑色还是白色作为对比色
@@ -7,46 +95,24 @@ import { extractDominantColor } from './colorExtractor'
*/
export async function shouldUseBlackText(imageSrc: string): Promise<boolean> {
try {
// 提取主要颜色
const dominantColor = await extractDominantColor(imageSrc)
// 使用更准确的相对亮度计算公式 (sRGB相对亮度)
// 先将RGB值标准化到0-1范围
const r = dominantColor.r / 255
const g = dominantColor.g / 255
const b = dominantColor.b / 255
// 应用sRGB转换
const R = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4)
const G = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4)
const B = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4)
// 计算相对亮度 (WCAG 2.0公式)
const luminance = 0.2126 * R + 0.7152 * G + 0.0722 * B
// 计算与黑色和白色的对比度
// 对比度计算公式: (L1 + 0.05) / (L2 + 0.05)其中L1是较亮的颜色L2是较暗的颜色
const contrastWithBlack = (luminance + 0.05) / 0.05
const contrastWithWhite = 1.05 / (luminance + 0.05)
// 直接分析图片的平均亮度
const averageLuminance = await getImageAverageLuminance(imageSrc)
console.log(
`颜色: RGB(${dominantColor.r},${dominantColor.g},${dominantColor.b}), ` +
`亮度: ${luminance.toFixed(3)}, ` +
`与黑色对比度: ${contrastWithBlack.toFixed(2)}, ` +
`与白色对比度: ${contrastWithWhite.toFixed(2)}`
`图片: ${imageSrc}, ` +
`平均亮度: ${averageLuminance.toFixed(3)} ` +
`(考虑半透明黑色背景覆盖效果)`
)
// 不仅考虑亮度,还要考虑对比度
// 如果与黑色的对比度更高,说明背景较亮,应该使用黑色文字
// 如果与白色的对比度更高,说明背景较暗,应该使用色文字
// 但对于中等亮度的颜色,我们需要更精细的判断
// 考虑到图片会受到rgba(0, 0, 0, 0.256)背景覆盖,实际显示会更暗
// 大幅提高阈值,让白色文字在更多情况下被选择
// 只有非常明亮的图片才使用色文字
// 对于中等亮度的颜色(0.3-0.6),我们更倾向于使用黑色文本,因为黑色文本通常更易读
if (luminance > 0.3) {
return true // 使用黑色文本
} else {
return false // 使用白色文本
}
const shouldUseBlack = averageLuminance >= 0.6
console.log(`决定使用${shouldUseBlack ? '黑色' : '白色'}文字`)
return shouldUseBlack
} catch (error) {
console.error('计算对比色失败:', error)
// 默认返回白色作为安全选择
@@ -75,24 +141,11 @@ export async function getBestContrastTextColorWithOpacity(
opacity: number = 1
): Promise<string> {
try {
// 提取主要颜色
const dominantColor = await extractDominantColor(imageSrc)
// 使用相同的亮度分析逻辑
const averageLuminance = await getImageAverageLuminance(imageSrc)
// 使用更准确的相对亮度计算公式 (sRGB相对亮度)
const r = dominantColor.r / 255
const g = dominantColor.g / 255
const b = dominantColor.b / 255
// 应用sRGB转换
const R = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4)
const G = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4)
const B = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4)
// 计算相对亮度
const luminance = 0.2126 * R + 0.7152 * G + 0.0722 * B
// 根据亮度决定文本颜色,使用更低的阈值
if (luminance > 0.3) {
// 使用与shouldUseBlackText相同的逻辑
if (averageLuminance >= 0.6) {
// 背景较亮,使用黑色文本
return `rgba(0, 0, 0, ${opacity})`
} else {

View File

@@ -1,7 +1,7 @@
import { NotifyPlugin } from 'tdesign-vue-next'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
import { toRaw } from 'vue'
import { MessagePlugin } from 'tdesign-vue-next';
import { MessagePlugin } from 'tdesign-vue-next'
interface MusicItem {
singer: string
@@ -31,22 +31,23 @@ const qualityMap: Record<string, string> = {
const qualityKey = Object.keys(qualityMap)
async function downloadSingleSong(songInfo: MusicItem): Promise<void> {
try {
const LocalUserDetail = LocalUserDetailStore()
let quality = LocalUserDetail.userSource.quality as string
let quality = LocalUserDetail.userSource.quality as string
if (
qualityKey.indexOf(quality) >
qualityKey.indexOf((songInfo.types[songInfo.types.length - 1] as unknown as { type: any }).type)
qualityKey.indexOf(
(songInfo.types[songInfo.types.length - 1] as unknown as { type: any }).type
)
) {
quality = (songInfo.types[songInfo.types.length - 1] as unknown as { type: any }).type
}
const tip = MessagePlugin.success('开始下载歌曲:'+ songInfo.name)
const result = await window.api.music.requestSdk('downloadSingleSong',{
pluginId:LocalUserDetail.userSource.pluginId?.toString() || '',
source:songInfo.source,
const tip = MessagePlugin.success('开始下载歌曲:' + songInfo.name)
const result = await window.api.music.requestSdk('downloadSingleSong', {
pluginId: LocalUserDetail.userSource.pluginId?.toString() || '',
source: songInfo.source,
quality,
songInfo:toRaw(songInfo)
songInfo: toRaw(songInfo)
})
;(await tip).close()
if (!Object.hasOwn(result, 'path')) {
@@ -61,7 +62,7 @@ async function downloadSingleSong(songInfo: MusicItem): Promise<void> {
console.error('下载失败:', error)
await NotifyPlugin.error({
title: '下载失败',
content: `${error.message.includes('歌曲正在')? '歌曲正在下载中':'未知错误'}`
content: `${error.message.includes('歌曲正在') ? '歌曲正在下载中' : '未知错误'}`
})
}
}

View File

@@ -7,13 +7,14 @@ import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
type PlaylistEvents = {
addToPlaylistAndPlay: SongList
addToPlaylistEnd: SongList
replacePlaylist: SongList[]
}
// 创建全局事件总线
const emitter = mitt<PlaylistEvents>()
// 将事件总线挂载到全局
; (window as any).musicEmitter = emitter
// 将事件总线挂载到全局
;(window as any).musicEmitter = emitter
const qualityMap: Record<string, string> = {
'128k': '标准音质',
'192k': '高品音质',
@@ -46,7 +47,7 @@ export async function getSongRealUrl(song: SongList): Promise<string> {
const urlData = await window.api.music.requestSdk('getMusicUrl', {
pluginId: LocalUserDetail.userSource.pluginId as unknown as string,
source: song.source,
songInfo: song,
songInfo: song as any,
quality
})
console.log(urlData)
@@ -124,6 +125,52 @@ export async function addToPlaylistEnd(song: SongList, localUserStore: any) {
}
}
/**
* 替换整个播放列表
* @param songs 要替换的歌曲列表
* @param localUserStore LocalUserDetail store实例
* @param playSongCallback 播放歌曲的回调函数
*/
export async function replacePlaylist(
songs: SongList[],
localUserStore: any,
playSongCallback: (song: SongList) => Promise<void>
) {
try {
if (songs.length === 0) {
await MessagePlugin.warning('歌曲列表为空')
return
}
// 清空当前播放列表
localUserStore.list.length = 0
// 添加所有歌曲到播放列表
songs.forEach((song) => {
localUserStore.addSong(song)
})
// 播放第一首歌曲
if (songs[0]) {
await getSongRealUrl(songs[0])
const playResult = playSongCallback(songs[0])
if (playResult && typeof playResult.then === 'function') {
await playResult
}
}
await MessagePlugin.success(`已用 ${songs.length} 首歌曲替换播放列表`)
} catch (error: any) {
console.error('替换播放列表失败:', error)
if (error.message) {
await MessagePlugin.error('替换失败: ' + error.message)
return
}
await MessagePlugin.error('替换播放列表失败,请重试')
}
}
/**
* 初始化播放列表事件监听器
* @param localUserStore LocalUserDetail store实例
@@ -142,6 +189,11 @@ export function initPlaylistEventListeners(
emitter.on('addToPlaylistEnd', async (song: SongList) => {
await addToPlaylistEnd(song, localUserStore)
})
// 监听替换播放列表的事件
emitter.on('replacePlaylist', async (songs: SongList[]) => {
await replacePlaylist(songs, localUserStore, playSongCallback)
})
}
/**
@@ -150,6 +202,7 @@ export function initPlaylistEventListeners(
export function destroyPlaylistEventListeners() {
emitter.off('addToPlaylistAndPlay')
emitter.off('addToPlaylistEnd')
emitter.off('replacePlaylist')
}
/**

View File

@@ -2,16 +2,15 @@
<div class="settings-page">
<div class="settings-container">
<h2>应用设置</h2>
<!-- 其他设置项 -->
<div class="settings-section">
<h3>常规设置</h3>
<!-- 这里可以添加其他设置项 -->
</div>
<!-- 自动更新设置 -->
<UpdateSettings />
</div>
</div>
</template>
@@ -48,4 +47,4 @@ import UpdateSettings from '../components/Settings/UpdateSettings.vue'
font-weight: 600;
color: var(--td-text-color-primary);
}
</style>
</style>

View File

@@ -104,8 +104,14 @@ const handleKeyDown = () => {
</div>
<nav class="nav-section">
<t-button v-for="(item, index) in menuList" :key="index" :variant="menuActive == index ? 'base' : 'text'"
:class="menuActive == index ? 'nav-button active' : 'nav-button'" block @click="handleClick(index)">
<t-button
v-for="(item, index) in menuList"
:key="index"
:variant="menuActive == index ? 'base' : 'text'"
:class="menuActive == index ? 'nav-button active' : 'nav-button'"
block
@click="handleClick(index)"
>
<i :class="`iconfont ${item.icon} nav-icon`"></i>
{{ item.name }}
</t-button>
@@ -130,10 +136,20 @@ const handleKeyDown = () => {
<svg class="icon" aria-hidden="true">
<use :xlink:href="`#icon-${source}`"></use>
</svg>
<t-input v-model="keyword" placeholder="搜索音乐、歌手" style="width: 100%" @enter="handleKeyDown">
<t-input
v-model="keyword"
placeholder="搜索音乐、歌手"
style="width: 100%"
@enter="handleKeyDown"
>
<template #suffix>
<t-button theme="primary" variant="text" shape="circle"
style="display: flex; align-items: center; justify-content: center" @click="handleSearch">
<t-button
theme="primary"
variant="text"
shape="circle"
style="display: flex; align-items: center; justify-content: center"
@click="handleSearch"
>
<SearchIcon style="font-size: 16px; color: #000" />
</t-button>
</template>
@@ -146,9 +162,11 @@ const handleKeyDown = () => {
<div class="mainContent">
<router-view v-slot="{ Component, route }">
<Transition name="page"
<Transition
name="page"
:enter-active-class="`animate__animated ${route.meta.transitionIn} animate__fast`"
:leave-active-class="`animate__animated ${route.meta.transitionOut} animate__fast`">
:leave-active-class="`animate__animated ${route.meta.transitionOut} animate__fast`"
>
<KeepAlive exclude="list">
<component :is="Component" />
</KeepAlive>
@@ -182,7 +200,7 @@ const handleKeyDown = () => {
.sidebar {
width: 15rem;
background-color: #fff;
background-image: linear-gradient(to bottom, var(--td-brand-color-4) -140vh, #ffffff 30vh);
border-right: 0.0625rem solid #e5e7eb;
flex-shrink: 0;
@@ -270,7 +288,8 @@ const handleKeyDown = () => {
.content {
padding: 0;
background: #f6f6f6;
background-image: linear-gradient(to bottom, var(--td-brand-color-4) -110vh, #ffffff 15vh);
display: flex;
flex: 1;
flex-direction: column;
@@ -309,12 +328,16 @@ const handleKeyDown = () => {
-webkit-app-region: no-drag;
display: flex;
align-items: center;
transition: width 0.3s;
padding: 0 0.5rem;
width: 18.75rem;
width: min(18.75rem, 400px);
margin-right: 0.5rem;
border-radius: 1.25rem !important;
background-color: #fff;
overflow: hidden;
&:has(input:focus) {
width: max(18.75rem, 400px);
}
}
:deep(.t-input) {

View File

@@ -1,7 +1,9 @@
<script setup lang="ts">
import { ref, onMounted, watch,WatchHandle, onUnmounted } from 'vue'
import { ref, onMounted, watch, WatchHandle, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
import { extractDominantColor } from '../../utils/colorExtractor'
// 路由实例
const router = useRouter()
@@ -9,20 +11,25 @@ const router = useRouter()
const recommendPlaylists: any = ref([])
const loading = ref(true)
const error = ref('')
const mainColors = ref<any[]>([])
const textColors = ref<string[]>([])
// 热门歌曲数据
const hotSongs: any = ref([])
let watchSource:WatchHandle |null = null
let watchSource: WatchHandle | null = null
// 获取热门歌单数据
const fetchHotSonglist = async () => {
const LocalUserDetail = LocalUserDetailStore()
watchSource =watch(LocalUserDetail.userSource,()=>{
if(LocalUserDetail.userSource.source){
fetchHotSonglist()
}
},{deep:true})
watchSource = watch(
LocalUserDetail.userSource,
() => {
if (LocalUserDetail.userSource.source) {
fetchHotSonglist()
}
},
{ deep: true }
)
try {
loading.value = true
error.value = ''
@@ -31,13 +38,12 @@ const fetchHotSonglist = async () => {
const result = await window.api.music.requestSdk('getHotSonglist', {
source: LocalUserDetail.userSource.source
})
if (result && result.list) {
recommendPlaylists.value = result.list.map((item: any) => ({
id: item.id,
title: item.name,
description: item.desc || '精选歌单',
cover: item.img || 'https://via.placeholder.com/200x200/f97316/ffffff?text=歌单',
cover: item.img,
playCount: item.play_count, // 直接使用返回的格式化字符串
author: item.author,
total: item.total,
@@ -45,6 +51,38 @@ const fetchHotSonglist = async () => {
source: item.source
}))
}
// 初始化主题色和文字颜色数组
mainColors.value = Array.from({ length: recommendPlaylists.value.length }).map(() => '#55C277')
textColors.value = Array.from({ length: recommendPlaylists.value.length }).map(() => '#fff')
// 异步获取每个封面的主题色和对应的文字颜色
const colorPromises = recommendPlaylists.value.map(async (item: any, index: number) => {
try {
const color = await extractDominantColor(item.cover)
// const textColor = await getBestContrastTextColor(item.cover)
return { index, color }
} catch (error) {
console.warn(`获取封面主题色失败 (索引 ${index}):`, error)
textColors.value[index] = '#000'
return { index, color: '#fff' }
}
})
// 等待所有颜色提取完成
const results = await Promise.all(colorPromises)
// 更新主题色和文字颜色数组
results.forEach(({ index, color }) => {
if (index < mainColors.value.length) {
// 深化颜色值,让颜色更深邃
const deepR = Math.floor(color.r * 0.7)
const deepG = Math.floor(color.g * 0.7)
const deepB = Math.floor(color.b * 0.7)
mainColors.value[index] = `rgba(${deepR}, ${deepG}, ${deepB}, 0.85)`
// textColors.value[index] = textColor
}
})
} catch (err) {
console.error('获取热门歌单失败:', err)
error.value = '获取数据失败,请稍后重试'
@@ -78,8 +116,8 @@ const playSong = (song: any): void => {
onMounted(() => {
fetchHotSonglist()
})
onUnmounted(()=>{
if(watchSource){
onUnmounted(() => {
if (watchSource) {
watchSource()
}
})
@@ -92,7 +130,6 @@ onUnmounted(()=>{
<h2>发现音乐</h2>
<p>探索最新最热的音乐内容</p>
</div>
<!-- 推荐歌单 -->
<div class="section">
<h3 class="section-title">热门歌单Top{{ recommendPlaylists.length }}</h3>
@@ -113,7 +150,7 @@ onUnmounted(()=>{
<!-- 歌单列表 -->
<div v-else class="playlist-grid">
<div
v-for="playlist in recommendPlaylists"
v-for="(playlist, index) in recommendPlaylists"
:key="playlist.id"
class="playlist-card"
@click="playPlaylist(playlist)"
@@ -121,15 +158,27 @@ onUnmounted(()=>{
<div class="playlist-cover">
<img :src="playlist.cover" :alt="playlist.title" />
</div>
<div class="playlist-info">
<h4 class="playlist-title">{{ playlist.title }}</h4>
<p class="playlist-desc">{{ playlist.description }}</p>
<div
class="playlist-info"
:style="{
'background-color': mainColors[index],
color: textColors[index]
}"
>
<h4 class="playlist-title" :style="{ color: textColors[index] }">
{{ playlist.title }}
</h4>
<p class="playlist-desc" :style="{ color: textColors[index] }">
{{ playlist.description }}
</p>
<div class="playlist-meta">
<span class="play-count">
<span class="play-count" :style="{ color: textColors[index] }">
<i class="iconfont icon-bofang"></i>
{{ playlist.playCount }}
</span>
<span class="song-count" v-if="playlist.total">{{ playlist.total }}</span>
<span class="song-count" v-if="playlist.total" :style="{ color: textColors[index] }"
>{{ playlist.total }}</span
>
</div>
<!-- <div class="playlist-author">by {{ playlist.author }}</div> -->
</div>
@@ -212,68 +261,139 @@ onUnmounted(()=>{
.playlist-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1.5rem;
gap: 1.25rem;
// 响应式grid列数
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
// 响应式断点优化
@media (max-width: 480px) {
gap: 0.75rem;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
@media (min-width: 481px) and (max-width: 768px) {
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
@media (min-width: 769px) and (max-width: 1024px) {
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
}
@media (min-width: 1200px) {
gap: 1.5rem;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
}
}
.playlist-card {
// 卡片样式
background: #fff;
border-radius: 0.75rem;
border-radius: 1rem;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.06),
0 1px 4px rgba(0, 0, 0, 0.04);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
position: relative;
// 现代化悬浮效果
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-4px) scale(1.02);
box-shadow:
0 8px 25px rgba(0, 0, 0, 0.12),
0 4px 10px rgba(0, 0, 0, 0.08);
.play-overlay {
.playlist-cover::after {
opacity: 1;
}
.playlist-info {
backdrop-filter: blur(8px);
}
}
// 活跃状态
&:active {
transform: translateY(-2px) scale(1.01);
}
.playlist-cover {
position: relative;
aspect-ratio: 1;
overflow: hidden;
// 悬浮遮罩层
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.3) 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
user-select: none;
-webkit-user-drag: none;
transition: transform 0.3s ease;
}
// 图片悬浮缩放效果
&:hover img {
transform: scale(1.05);
}
}
.playlist-info {
padding: 1rem;
padding: 1.25rem 1rem;
position: relative;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(4px);
transition: backdrop-filter 0.3s ease;
.playlist-title {
font-size: 1rem;
font-weight: 600;
color: #111827;
margin-bottom: 0.25rem;
white-space: nowrap;
color: #1f2937;
margin-bottom: 0.5rem;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
min-height: 2.8rem; // 确保标题区域高度一致
}
.playlist-desc {
font-size: 0.875rem;
color: #6b7280;
margin-bottom: 0.5rem;
margin-bottom: 0.75rem;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
min-height: 2.625rem; // 确保描述区域高度一致
}
.playlist-meta {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
justify-content: space-between;
gap: 0.5rem;
margin-top: auto; // 推到底部
padding-top: 0.5rem;
border-top: 1px solid rgba(229, 231, 235, 0.5);
}
.play-count {
@@ -282,21 +402,29 @@ onUnmounted(()=>{
display: flex;
align-items: center;
gap: 0.25rem;
font-weight: 500;
.iconfont {
font-size: 0.75rem;
font-size: 0.875rem;
opacity: 0.8;
}
}
.song-count {
font-size: 0.75rem;
color: #9ca3af;
font-weight: 500;
background: rgba(156, 163, 175, 0.1);
padding: 0.125rem 0.5rem;
border-radius: 0.375rem;
}
.playlist-author {
font-size: 0.75rem;
color: #6b7280;
font-style: italic;
margin-top: 0.25rem;
opacity: 0.8;
}
}
}

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, onMounted, toRaw } from 'vue'
import { useRoute } from 'vue-router'
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
import { downloadSingleSong } from '@renderer/utils/download'
import SongVirtualList from '@renderer/components/Music/SongVirtualList.vue'
@@ -53,12 +54,79 @@ const fetchPlaylistSongs = async () => {
source: (route.query.source as string) || (LocalUserDetail.userSource.source as any)
}
// 检查是否是本地歌单
const isLocalPlaylist = route.query.type === 'local' || route.query.source === 'local'
if (isLocalPlaylist) {
// 处理本地歌单
await fetchLocalPlaylistSongs()
} else {
// 处理网络歌单
await fetchNetworkPlaylistSongs()
}
} catch (error) {
console.error('获取歌单歌曲失败:', error)
} finally {
loading.value = false
}
}
// 获取本地歌单歌曲
const fetchLocalPlaylistSongs = async () => {
try {
// 调用本地歌单API获取歌曲列表
const result = await window.api.songList.getSongs(playlistInfo.value.id)
if (result.success && result.data) {
songs.value = result.data.map((song: any) => ({
singer: song.singer || '未知歌手',
name: song.name || '未知歌曲',
albumName: song.albumName || '未知专辑',
albumId: song.albumId || 0,
source: song.source || 'local',
interval: song.interval || '0:00',
songmid: song.songmid,
img: song.img || '',
lrc: song.lrc || null,
types: song.types || [],
_types: song._types || {},
typeUrl: song.typeUrl || {}
}))
// 更新歌单信息中的歌曲总数
playlistInfo.value.total = songs.value.length
// 获取歌单详细信息
const playlistResult = await window.api.songList.getById(playlistInfo.value.id)
if (playlistResult.success && playlistResult.data) {
const playlist = playlistResult.data
playlistInfo.value = {
...playlistInfo.value,
title: playlist.name,
cover: playlist.coverImgUrl || playlistInfo.value.cover,
total: songs.value.length
}
}
} else {
console.error('获取本地歌单失败:', result.error)
songs.value = []
}
} catch (error) {
console.error('获取本地歌单歌曲失败:', error)
songs.value = []
}
}
// 获取网络歌单歌曲
const fetchNetworkPlaylistSongs = async () => {
try {
// 调用API获取歌单详情和歌曲列表
const result = await window.api.music.requestSdk('getPlaylistDetail', {
const result = (await window.api.music.requestSdk('getPlaylistDetail', {
source: playlistInfo.value.source,
id: playlistInfo.value.id,
page: 1
}) as any
})) as any
console.log(result)
if (result && result.list) {
songs.value = result.list
@@ -78,9 +146,8 @@ const fetchPlaylistSongs = async () => {
}
}
} catch (error) {
console.error('获取歌单歌曲失败:', error)
} finally {
loading.value = false
console.error('获取网络歌单失败:', error)
songs.value = []
}
}
@@ -134,6 +201,92 @@ const handleAddToPlaylist = (song: MusicItem) => {
;(window as any).musicEmitter.emit('addToPlaylistEnd', toRaw(song))
}
}
// 替换播放列表的通用函数
const replacePlaylist = (songsToReplace: MusicItem[], shouldShuffle = false) => {
if (!(window as any).musicEmitter) {
MessagePlugin.error('播放器未初始化')
return
}
let finalSongs = [...songsToReplace]
if (shouldShuffle) {
// 创建歌曲索引数组并打乱
const shuffledIndexes = Array.from({ length: songsToReplace.length }, (_, i) => i)
for (let i = shuffledIndexes.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[shuffledIndexes[i], shuffledIndexes[j]] = [shuffledIndexes[j], shuffledIndexes[i]]
}
// 按打乱的顺序重新排列歌曲
finalSongs = shuffledIndexes.map((index) => songsToReplace[index])
}
// 使用自定义事件替换整个播放列表
if ((window as any).musicEmitter) {
;(window as any).musicEmitter.emit(
'replacePlaylist',
finalSongs.map((song) => toRaw(song))
)
}
// // 更新当前播放状态
// if (finalSongs[0]) {
// currentSong.value = finalSongs[0]
// isPlaying.value = true
// }
// const playerSong = inject('PlaySong',(...args:any)=>args)
// nextTick(()=>{
// playerSong(finalSongs[0])
// })
MessagePlugin.success(`请稍等歌曲加载完成播放`)
}
// 播放整个歌单
const handlePlayPlaylist = () => {
if (songs.value.length === 0) {
MessagePlugin.warning('歌单为空,无法播放')
return
}
const dialog = DialogPlugin.confirm({
header: '播放歌单',
body: `确定要用歌单"${playlistInfo.value.title}"中的 ${songs.value.length} 首歌曲替换当前播放列表吗?`,
confirmBtn: '确定替换',
cancelBtn: '取消',
onConfirm: () => {
console.log('播放歌单:', playlistInfo.value.title)
replacePlaylist(songs.value, false)
dialog.destroy()
},
onCancel: () => {
dialog.destroy()
}
})
}
// 随机播放歌单
const handleShufflePlaylist = () => {
if (songs.value.length === 0) {
MessagePlugin.warning('歌单为空,无法播放')
return
}
const dialog = DialogPlugin.confirm({
header: '随机播放歌单',
body: `确定要用歌单"${playlistInfo.value.title}"中的 ${songs.value.length} 首歌曲随机替换当前播放列表吗?`,
confirmBtn: '确定替换',
cancelBtn: '取消',
onConfirm: () => {
console.log('随机播放歌单:', playlistInfo.value.title)
replacePlaylist(songs.value, true)
dialog.destroy()
},
onCancel: () => {
dialog.destroy()
}
})
}
// 组件挂载时获取数据
onMounted(() => {
fetchPlaylistSongs()
@@ -153,6 +306,41 @@ onMounted(() => {
<h1 class="playlist-title">{{ playlistInfo.title }}</h1>
<p class="playlist-author">by {{ playlistInfo.author }}</p>
<p class="playlist-stats">{{ playlistInfo.total }} 首歌曲</p>
<!-- 播放控制按钮 -->
<div class="playlist-actions">
<t-button
theme="primary"
size="medium"
@click="handlePlayPlaylist"
:disabled="songs.length === 0 || loading"
class="play-btn"
>
<template #icon>
<svg class="play-icon" viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z" />
</svg>
</template>
播放全部
</t-button>
<t-button
variant="outline"
size="medium"
@click="handleShufflePlaylist"
:disabled="songs.length === 0 || loading"
class="shuffle-btn"
>
<template #icon>
<svg class="shuffle-icon" viewBox="0 0 24 24" fill="currentColor">
<path
d="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z"
/>
</svg>
</template>
随机播放
</t-button>
</div>
</div>
</div>
</div>
@@ -290,7 +478,24 @@ onMounted(() => {
.playlist-stats {
font-size: 0.875rem;
color: #9ca3af;
margin: 0;
margin: 0 0 1rem 0;
}
.playlist-actions {
display: flex;
gap: 0.75rem;
margin-top: 1rem;
.play-btn,
.shuffle-btn {
min-width: 120px;
.play-icon,
.shuffle-icon {
width: 16px;
height: 16px;
}
}
}
}
}
@@ -316,6 +521,36 @@ onMounted(() => {
width: 100px;
height: 100px;
}
.playlist-details {
.playlist-actions {
flex-direction: column;
gap: 0.5rem;
.play-btn,
.shuffle-btn {
width: 100%;
min-width: auto;
}
}
}
}
}
@media (max-width: 480px) {
.playlist-header {
.playlist-details {
.playlist-actions {
.play-btn,
.shuffle-btn {
.play-icon,
.shuffle-icon {
width: 14px;
height: 14px;
}
}
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -105,10 +105,10 @@ async function setPic(offset: number, source: string) {
if (typeof url !== 'object') {
searchResults.value[i].img = url
} else {
searchResults.value[i].img = 'resources/logo.png'
searchResults.value[i].img = ''
}
} catch (e) {
searchResults.value[i].img = 'logo.svg'
searchResults.value[i].img = ''
console.log('获取失败 index' + i, e)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
<div class="logo-section">
<div class="image-container">
<div class="image-bg"></div>
<img class="logo-image" src="/logo.svg" alt="Ceru Music Logo">
<img class="logo-image" src="/logo.svg" alt="Ceru Music Logo" />
</div>
</div>
@@ -181,7 +181,7 @@ onMounted(async () => {
.tag {
padding: 0.4rem 0.8rem;
background: #B8F1CE;
background: #b8f1ce;
border: 1px solid #e9ecef;
border-radius: 20px;
font-size: 0.8rem;
@@ -309,4 +309,4 @@ onMounted(async () => {
color: #666666;
}
}
</style>
</style>

View File

@@ -7,4 +7,4 @@ export interface CacheInfo {
export interface CacheOperationResult {
success: boolean
message: string
}
}

216
src/types/songList.ts Normal file
View File

@@ -0,0 +1,216 @@
import type { SongList, Songs } from '@common/types/songList'
// IPC 响应基础类型
export interface IPCResponse<T = any> {
success: boolean
data?: T
error?: string
message?: string
code?: string
}
// 歌单管理相关类型定义
export interface SongListAPI {
// === 歌单管理 ===
/**
* 创建新歌单
*/
create(
name: string,
description?: string,
source?: SongList['source']
): Promise<IPCResponse<{ id: string }>>
/**
* 获取所有歌单
*/
getAll(): Promise<IPCResponse<SongList[]>>
/**
* 根据ID获取歌单信息
*/
getById(hashId: string): Promise<IPCResponse<SongList | null>>
/**
* 删除歌单
*/
delete(hashId: string): Promise<IPCResponse>
/**
* 批量删除歌单
*/
batchDelete(hashIds: string[]): Promise<IPCResponse<{ success: string[]; failed: string[] }>>
/**
* 编辑歌单信息
*/
edit(hashId: string, updates: Partial<Omit<SongList, 'id' | 'createTime'>>): Promise<IPCResponse>
/**
* 更新歌单封面
*/
updateCover(hashId: string, coverImgUrl: string): Promise<IPCResponse>
/**
* 搜索歌单
*/
search(keyword: string, source?: SongList['source']): Promise<IPCResponse<SongList[]>>
/**
* 获取歌单统计信息
*/
getStatistics(): Promise<
IPCResponse<{
total: number
bySource: Record<string, number>
lastUpdated: string
}>
>
/**
* 检查歌单是否存在
*/
exists(hashId: string): Promise<IPCResponse<boolean>>
// === 歌曲管理 ===
/**
* 添加歌曲到歌单
*/
addSongs(hashId: string, songs: Songs[]): Promise<IPCResponse>
/**
* 从歌单移除歌曲
*/
removeSong(hashId: string, songmid: string | number): Promise<IPCResponse<boolean>>
/**
* 批量移除歌曲
*/
removeSongs(
hashId: string,
songmids: (string | number)[]
): Promise<IPCResponse<{ removed: number; notFound: number }>>
/**
* 清空歌单
*/
clearSongs(hashId: string): Promise<IPCResponse>
/**
* 获取歌单中的歌曲列表
*/
getSongs(hashId: string): Promise<IPCResponse<readonly Songs[]>>
/**
* 获取歌单歌曲数量
*/
getSongCount(hashId: string): Promise<IPCResponse<number>>
/**
* 检查歌曲是否在歌单中
*/
hasSong(hashId: string, songmid: string | number): Promise<IPCResponse<boolean>>
/**
* 根据ID获取歌曲
*/
getSong(hashId: string, songmid: string | number): Promise<IPCResponse<Songs | null>>
/**
* 搜索歌单中的歌曲
*/
searchSongs(hashId: string, keyword: string): Promise<IPCResponse<Songs[]>>
/**
* 获取歌单歌曲统计信息
*/
getSongStatistics(hashId: string): Promise<
IPCResponse<{
total: number
bySinger: Record<string, number>
byAlbum: Record<string, number>
lastModified: string
}>
>
/**
* 验证歌单完整性
*/
validateIntegrity(hashId: string): Promise<IPCResponse<{ isValid: boolean; issues: string[] }>>
/**
* 修复歌单数据
*/
repairData(hashId: string): Promise<IPCResponse<{ fixed: boolean; changes: string[] }>>
/**
* 强制保存歌单
*/
forceSave(hashId: string): Promise<IPCResponse>
}
// 错误码枚举
export enum SongListErrorCode {
INVALID_HASH_ID = 'INVALID_HASH_ID',
EMPTY_NAME = 'EMPTY_NAME',
EMPTY_SOURCE = 'EMPTY_SOURCE',
CREATE_FAILED = 'CREATE_FAILED',
FILE_CREATE_FAILED = 'FILE_CREATE_FAILED',
PLAYLIST_NOT_FOUND = 'PLAYLIST_NOT_FOUND',
DELETE_FAILED = 'DELETE_FAILED',
EMPTY_UPDATES = 'EMPTY_UPDATES',
EDIT_FAILED = 'EDIT_FAILED',
READ_FAILED = 'READ_FAILED',
INIT_FAILED = 'INIT_FAILED',
WRITE_FAILED = 'WRITE_FAILED',
INVALID_ACTION = 'INVALID_ACTION',
UPDATE_COVER_FAILED = 'UPDATE_COVER_FAILED',
INVALID_DATA_FORMAT = 'INVALID_DATA_FORMAT',
FILE_NOT_FOUND = 'FILE_NOT_FOUND',
INVALID_SONG_ID = 'INVALID_SONG_ID',
SAVE_FAILED = 'SAVE_FAILED',
UNKNOWN_ERROR = 'UNKNOWN_ERROR'
}
// 歌单来源类型
export type SongListSource = 'local' | 'wy' | 'tx' | 'mg' | 'kg' | 'kw'
// 批量操作结果类型
export interface BatchOperationResult {
success: string[]
failed: string[]
}
// 歌曲移除结果类型
export interface RemoveSongsResult {
removed: number
notFound: number
}
// 统计信息类型
export interface SongListStatistics {
total: number
bySource: Record<string, number>
lastUpdated: string
}
export interface SongStatistics {
total: number
bySinger: Record<string, number>
byAlbum: Record<string, number>
lastModified: string
}
// 数据完整性检查结果
export interface IntegrityCheckResult {
isValid: boolean
issues: string[]
}
// 数据修复结果
export interface RepairResult {
fixed: boolean
changes: string[]
}

View File

@@ -3,7 +3,13 @@
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/common/**/*", "src/types/**/*"],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"types": ["electron-vite/node"],
"moduleResolution": "bundler"
"moduleResolution": "bundler",
"paths": {
"@common/*":[
"src/common/*"
],
}
}
}

View File

@@ -5,7 +5,8 @@
"src/renderer/src/**/*",
"src/renderer/src/**/*.vue",
"src/preload/*.d.ts",
"src/types/**/*"
"src/types/**/*",
"src/common/**/*"
],
"compilerOptions": {
"composite": true,

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,258 +1,267 @@
<!DOCTYPE html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ceru Music - 跨平台音乐播放器</title>
<link rel="stylesheet" href="styles.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<link rel="stylesheet" href="styles.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
</head>
<body>
<!-- Navigation -->
<nav class="navbar">
<div class="nav-container">
<div class="nav-logo">
<img src="./resources/logo.svg" alt="Ceru Music" class="logo-img">
<span class="logo-text">Ceru Music</span>
</div>
<div class="nav-links">
<a href="#features">功能特色</a>
<a href="#download">下载</a>
<a href="./CeruUse.html" target="_blank">文档</a>
</div>
<div class="nav-container">
<div class="nav-logo">
<img src="./resources/logo.svg" alt="Ceru Music" class="logo-img" />
<span class="logo-text">Ceru Music</span>
</div>
<div class="nav-links">
<a href="#features">功能特色</a>
<a href="#download">下载</a>
<a href="./CeruUse.html" target="_blank">文档</a>
</div>
</div>
</nav>
<!-- Hero Section -->
<section class="hero">
<div class="hero-container">
<div class="hero-content">
<h1 class="hero-title">
<span class="gradient-text">Ceru Music</span>
<br>跨平台音乐播放器
</h1>
<p class="hero-description">
集成多平台音乐源提供优雅的桌面音乐体验。支持网易云音乐、QQ音乐等多个平台让你的音乐世界更加丰富。
</p>
<div class="hero-buttons">
<button class="btn btn-primary" onclick="scrollToDownload()">
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7,10 12,15 17,10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
立即下载
</button>
<button class="btn btn-secondary" onclick="scrollToFeatures()">
了解更多
</button>
</div>
</div>
<div class="hero-image">
<div class="app-preview">
<div class="app-window">
<div class="window-header">
<div class="window-controls">
<span class="control close"></span>
<span class="control minimize"></span>
<span class="control maximize"></span>
</div>
</div>
<div class="window-content">
<div class="music-player-preview">
<div class="album-art"></div>
<div class="player-info">
<div class="song-title"></div>
<div class="artist-name"></div>
<div class="progress-bar">
<div class="progress"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="hero-container">
<div class="hero-content">
<h1 class="hero-title">
<span class="gradient-text">Ceru Music</span>
<br />跨平台音乐播放器
</h1>
<p class="hero-description">
集成多平台音乐源提供优雅的桌面音乐体验。支持网易云音乐、QQ音乐等多个平台让你的音乐世界更加丰富。
</p>
<div class="hero-buttons">
<button class="btn btn-primary" onclick="scrollToDownload()">
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7,10 12,15 17,10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
立即下载
</button>
<button class="btn btn-secondary" onclick="scrollToFeatures()">了解更多</button>
</div>
</div>
<div class="hero-image">
<div class="app-preview">
<div class="app-window">
<div class="window-header">
<div class="window-controls">
<span class="control close"></span>
<span class="control minimize"></span>
<span class="control maximize"></span>
</div>
</div>
<div class="window-content">
<div class="music-player-preview">
<div class="album-art"></div>
<div class="player-info">
<div class="song-title"></div>
<div class="artist-name"></div>
<div class="progress-bar">
<div class="progress"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section id="features" class="features">
<div class="container">
<h2 class="section-title">功能特色</h2>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="12" cy="12" r="3"/>
<path d="M12 1v6m0 6v6"/>
<path d="m21 12-6-3-6 3-6-3"/>
</svg>
</div>
<h3>多平台音源</h3>
<p>支持网易云音乐、QQ音乐等多个平台一站式访问海量音乐资源</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
</div>
<h3>跨平台支持</h3>
<p>原生桌面应用,支持 Windows、macOS、Linux 三大操作系统</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M9 18V5l12-2v13"/>
<circle cx="6" cy="18" r="3"/>
<circle cx="18" cy="16" r="3"/>
</svg>
</div>
<h3>歌词显示</h3>
<p>实时歌词显示,支持专辑信息获取,让音乐体验更加丰富</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
</svg>
</div>
<h3>性能优化</h3>
<p>虚拟滚动技术,轻松处理大型音乐列表,流畅的用户体验</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>
</svg>
</div>
<h3>本地播放列表</h3>
<p>创建和管理个人播放列表,本地数据存储,个性化音乐体验</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
</div>
<h3>优雅界面</h3>
<p>现代化设计语言,流畅动画效果,为你带来愉悦的视觉体验</p>
</div>
<div class="container">
<h2 class="section-title">功能特色</h2>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="12" cy="12" r="3" />
<path d="M12 1v6m0 6v6" />
<path d="m21 12-6-3-6 3-6-3" />
</svg>
</div>
<h3>多平台音源</h3>
<p>支持网易云音乐、QQ音乐等多个平台一站式访问海量音乐资源</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
<line x1="8" y1="21" x2="16" y2="21" />
<line x1="12" y1="17" x2="12" y2="21" />
</svg>
</div>
<h3>跨平台支持</h3>
<p>原生桌面应用,支持 Windows、macOS、Linux 三大操作系统</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M9 18V5l12-2v13" />
<circle cx="6" cy="18" r="3" />
<circle cx="18" cy="16" r="3" />
</svg>
</div>
<h3>歌词显示</h3>
<p>实时歌词显示,支持专辑信息获取,让音乐体验更加丰富</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
</svg>
</div>
<h3>性能优化</h3>
<p>虚拟滚动技术,轻松处理大型音乐列表,流畅的用户体验</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
</svg>
</div>
<h3>本地播放列表</h3>
<p>创建和管理个人播放列表,本地数据存储,个性化音乐体验</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"
/>
</svg>
</div>
<h3>优雅界面</h3>
<p>现代化设计语言,流畅动画效果,为你带来愉悦的视觉体验</p>
</div>
</div>
</div>
</section>
<!-- Download Section -->
<section id="download" class="download">
<div class="container">
<h2 class="section-title">立即下载</h2>
<p class="section-subtitle">选择适合你操作系统的版本</p>
<div class="download-cards">
<div class="download-card">
<div class="platform-icon">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M0 12v-2h24v2H12.5c-.2 0-.5.2-.5.5s.3.5.5.5H24v2H0v-2h11.5c.2 0 .5-.2.5-.5s-.3-.5-.5-.5H0z"/>
</svg>
</div>
<h3>Windows</h3>
<p>Windows 10/11 (64-bit)</p>
<button class="btn btn-download" onclick="downloadApp('windows')">
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7,10 12,15 17,10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
下载 .exe
</button>
</div>
<div class="download-card">
<div class="platform-icon">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12.5 2C13.3 2 14 2.7 14 3.5S13.3 5 12.5 5 11 4.3 11 3.5 11.7 2 12.5 2M21 9H15L13.5 7.5C13.1 7.1 12.6 6.9 12 6.9S10.9 7.1 10.5 7.5L9 9H3C1.9 9 1 9.9 1 11V19C1 20.1 1.9 21 3 21H21C22.1 21 23 20.1 23 19V11C23 9.9 22.1 9 21 9Z"/>
</svg>
</div>
<h3>macOS</h3>
<p>macOS 10.15+ (Intel & Apple Silicon)</p>
<button class="btn btn-download" onclick="downloadApp('macos')">
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7,10 12,15 17,10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
下载 .dmg
</button>
</div>
<div class="download-card">
<div class="platform-icon">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2M21 9V7L15 1H5C3.9 1 3 1.9 3 3V21C3 22.1 3.9 23 5 23H19C20.1 23 21 22.1 21 21V9H21Z"/>
</svg>
</div>
<h3>Linux</h3>
<p>Ubuntu 18.04+ / Debian 10+</p>
<button class="btn btn-download" onclick="downloadApp('linux')">
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7,10 12,15 17,10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
下载 .AppImage
</button>
</div>
<div class="container">
<h2 class="section-title">立即下载</h2>
<p class="section-subtitle">选择适合你操作系统的版本</p>
<div class="download-cards">
<div class="download-card">
<div class="platform-icon">
<svg viewBox="0 0 24 24" fill="currentColor">
<path
d="M0 12v-2h24v2H12.5c-.2 0-.5.2-.5.5s.3.5.5.5H24v2H0v-2h11.5c.2 0 .5-.2.5-.5s-.3-.5-.5-.5H0z"
/>
</svg>
</div>
<div class="version-info">
<p>当前版本: <span class="version">v1.0.0</span> | 更新时间: 2024年12月</p>
<h3>Windows</h3>
<p>Windows 10/11 (64-bit)</p>
<button class="btn btn-download" onclick="downloadApp('windows')">
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7,10 12,15 17,10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
下载 .exe
</button>
</div>
<div class="download-card">
<div class="platform-icon">
<svg viewBox="0 0 24 24" fill="currentColor">
<path
d="M12.5 2C13.3 2 14 2.7 14 3.5S13.3 5 12.5 5 11 4.3 11 3.5 11.7 2 12.5 2M21 9H15L13.5 7.5C13.1 7.1 12.6 6.9 12 6.9S10.9 7.1 10.5 7.5L9 9H3C1.9 9 1 9.9 1 11V19C1 20.1 1.9 21 3 21H21C22.1 21 23 20.1 23 19V11C23 9.9 22.1 9 21 9Z"
/>
</svg>
</div>
<h3>macOS</h3>
<p>macOS 10.15+ (Intel & Apple Silicon)</p>
<button class="btn btn-download" onclick="downloadApp('macos')">
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7,10 12,15 17,10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
下载 .dmg
</button>
</div>
<div class="download-card">
<div class="platform-icon">
<svg viewBox="0 0 24 24" fill="currentColor">
<path
d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2M21 9V7L15 1H5C3.9 1 3 1.9 3 3V21C3 22.1 3.9 23 5 23H19C20.1 23 21 22.1 21 21V9H21Z"
/>
</svg>
</div>
<h3>Linux</h3>
<p>Ubuntu 18.04+ / Debian 10+</p>
<button class="btn btn-download" onclick="downloadApp('linux')">
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7,10 12,15 17,10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
下载 .AppImage
</button>
</div>
</div>
<div class="version-info">
<p>当前版本: <span class="version">v1.0.0</span> | 更新时间: 2024年12月</p>
</div>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="container">
<div class="footer-content">
<div class="footer-section">
<div class="footer-logo">
<img src="./resources/logo.svg" alt="Ceru Music" class="logo-img">
<span class="logo-text">Ceru Music</span>
</div>
<p>跨平台音乐播放器,为你带来优雅的音乐体验</p>
</div>
<div class="footer-section">
<h4>产品</h4>
<ul>
<li><a href="#features">功能特色</a></li>
<li><a href="#download">下载</a></li>
<li><a href="./design.html">设计文档</a></li>
</ul>
</div>
<div class="footer-section">
<h4>开发者</h4>
<ul>
<!-- <li><a href="../docs/api.md">API 文档</a></li> -->
<li><a href="./pluginDev.html">插件开发</a></li>
<li><a href="https://github.com/timeshiftsauce">GitHub</a></li>
</ul>
</div>
<div class="footer-section">
<h4>支持</h4>
<ul>
<li><a href="./CeruUse.html">使用文档</a></li>
<li><a href="#contact">联系我们</a></li>
<li><a href="#feedback">反馈建议</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 Ceru Music. All rights reserved.</p>
<div class="container">
<div class="footer-content">
<div class="footer-section">
<div class="footer-logo">
<img src="./resources/logo.svg" alt="Ceru Music" class="logo-img" />
<span class="logo-text">Ceru Music</span>
</div>
<p>跨平台音乐播放器,为你带来优雅的音乐体验</p>
</div>
<div class="footer-section">
<h4>产品</h4>
<ul>
<li><a href="#features">功能特色</a></li>
<li><a href="#download">下载</a></li>
<li><a href="./design.html">设计文档</a></li>
</ul>
</div>
<div class="footer-section">
<h4>开发者</h4>
<ul>
<!-- <li><a href="../docs/api.md">API 文档</a></li> -->
<li><a href="./pluginDev.html">插件开发</a></li>
<li><a href="https://github.com/timeshiftsauce">GitHub</a></li>
</ul>
</div>
<div class="footer-section">
<h4>支持</h4>
<ul>
<li><a href="./CeruUse.html">使用文档</a></li>
<li><a href="#contact">联系我们</a></li>
<li><a href="#feedback">反馈建议</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 Ceru Music. All rights reserved.</p>
</div>
</div>
</footer>
<script src="script.js"></script>
</body>
</html>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

618
yarn.lock
View File

@@ -327,6 +327,16 @@
ajv "^6.12.0"
ajv-keywords "^3.4.1"
"@docsearch/css@^4.0.0-beta.7":
version "4.0.0-beta.8"
resolved "https://registry.npmmirror.com/@docsearch/css/-/css-4.0.0-beta.8.tgz#836ac7c3eeecf87cfc9c518210f4dfd27e49b05f"
integrity sha512-/ZlyvZCjIJM4aaOYoJpVNHPJckX7J5KIbt6IWjnZXvo0QAUI1aH976vKEJUC9olgUbE3LWafB8yuX4qoqahIQg==
"@docsearch/js@^4.0.0-beta.7":
version "4.0.0-beta.8"
resolved "https://registry.npmmirror.com/@docsearch/js/-/js-4.0.0-beta.8.tgz#eca030c793fad34487c0c1c9147112544edb02bc"
integrity sha512-elgqPYpykRQr5MlfqoO8U2uC3BcPgjUQhzmHt/H4lSzP7khJ9Jpv/cCB4tiZreXb6GkdRgWr5csiItNq6jjnhg==
"@electron-toolkit/eslint-config-prettier@3.0.0":
version "3.0.0"
resolved "https://registry.npmmirror.com/@electron-toolkit/eslint-config-prettier/-/eslint-config-prettier-3.0.0.tgz"
@@ -673,6 +683,18 @@
resolved "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz"
integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
"@iconify-json/simple-icons@^1.2.47":
version "1.2.50"
resolved "https://registry.npmmirror.com/@iconify-json/simple-icons/-/simple-icons-1.2.50.tgz#62475b9a6b82e1b55d78ac5c769f550c7b2bc932"
integrity sha512-Z2ggRwKYEBB9eYAEi4NqEgIzyLhu0Buh4+KGzMPD6+xG7mk52wZJwLT/glDPtfslV503VtJbqzWqBUGkCMKOFA==
dependencies:
"@iconify/types" "*"
"@iconify/types@*":
version "2.0.0"
resolved "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz#ab0e9ea681d6c8a1214f30cd741fe3a20cc57f57"
integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==
"@img/sharp-darwin-arm64@0.33.5":
version "0.33.5"
resolved "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz"
@@ -1121,7 +1143,7 @@
resolved "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0":
"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5":
version "1.5.5"
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz"
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
@@ -1568,6 +1590,68 @@
resolved "https://registry.npmmirror.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz"
integrity sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==
"@shikijs/core@3.12.2", "@shikijs/core@^3.9.2":
version "3.12.2"
resolved "https://registry.npmmirror.com/@shikijs/core/-/core-3.12.2.tgz#c1690b9d9d1f982491a59a1e01cb49bb5fe0c43c"
integrity sha512-L1Safnhra3tX/oJK5kYHaWmLEBJi1irASwewzY3taX5ibyXyMkkSDZlq01qigjryOBwrXSdFgTiZ3ryzSNeu7Q==
dependencies:
"@shikijs/types" "3.12.2"
"@shikijs/vscode-textmate" "^10.0.2"
"@types/hast" "^3.0.4"
hast-util-to-html "^9.0.5"
"@shikijs/engine-javascript@3.12.2":
version "3.12.2"
resolved "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-3.12.2.tgz#c0badb74b4d5ded6f600b39f9abd3c83ab9f28a4"
integrity sha512-Nm3/azSsaVS7hk6EwtHEnTythjQfwvrO5tKqMlaH9TwG1P+PNaR8M0EAKZ+GaH2DFwvcr4iSfTveyxMIvXEHMw==
dependencies:
"@shikijs/types" "3.12.2"
"@shikijs/vscode-textmate" "^10.0.2"
oniguruma-to-es "^4.3.3"
"@shikijs/engine-oniguruma@3.12.2":
version "3.12.2"
resolved "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.12.2.tgz#3314a43666d751f80c0d1fc584029a3074af4e9a"
integrity sha512-hozwnFHsLvujK4/CPVHNo3Bcg2EsnG8krI/ZQ2FlBlCRpPZW4XAEQmEwqegJsypsTAN9ehu2tEYe30lYKSZW/w==
dependencies:
"@shikijs/types" "3.12.2"
"@shikijs/vscode-textmate" "^10.0.2"
"@shikijs/langs@3.12.2":
version "3.12.2"
resolved "https://registry.npmmirror.com/@shikijs/langs/-/langs-3.12.2.tgz#f41f1e0eb940c5c41f1017f8765be969e74a0c9b"
integrity sha512-bVx5PfuZHDSHoBal+KzJZGheFuyH4qwwcwG/n+MsWno5cTlKmaNtTsGzJpHYQ8YPbB5BdEdKU1rga5/6JGY8ww==
dependencies:
"@shikijs/types" "3.12.2"
"@shikijs/themes@3.12.2":
version "3.12.2"
resolved "https://registry.npmmirror.com/@shikijs/themes/-/themes-3.12.2.tgz#e4b9b636669658576cdc532ebe1c405cadba6272"
integrity sha512-fTR3QAgnwYpfGczpIbzPjlRnxyONJOerguQv1iwpyQZ9QXX4qy/XFQqXlf17XTsorxnHoJGbH/LXBvwtqDsF5A==
dependencies:
"@shikijs/types" "3.12.2"
"@shikijs/transformers@^3.9.2":
version "3.12.2"
resolved "https://registry.npmmirror.com/@shikijs/transformers/-/transformers-3.12.2.tgz#0cc01cba547f9a1dff550bec20c047eb284fa39d"
integrity sha512-+z1aMq4N5RoNGY8i7qnTYmG2MBYzFmwkm/yOd6cjEI7OVzcldVvzQCfxU1YbIVgsyB0xHVc2jFe1JhgoXyUoSQ==
dependencies:
"@shikijs/core" "3.12.2"
"@shikijs/types" "3.12.2"
"@shikijs/types@3.12.2", "@shikijs/types@^3.9.2":
version "3.12.2"
resolved "https://registry.npmmirror.com/@shikijs/types/-/types-3.12.2.tgz#8f7f02a1fd67a93470aac9409d072880b3148612"
integrity sha512-K5UIBzxCyv0YoxN3LMrKB9zuhp1bV+LgewxuVwHdl4Gz5oePoUFrr9EfgJlGlDeXCU1b/yhdnXeuRvAnz8HN8Q==
dependencies:
"@shikijs/vscode-textmate" "^10.0.2"
"@types/hast" "^3.0.4"
"@shikijs/vscode-textmate@^10.0.2":
version "10.0.2"
resolved "https://registry.npmmirror.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz#a90ab31d0cc1dfb54c66a69e515bf624fa7b2224"
integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==
"@sindresorhus/is@^4.0.0":
version "4.6.0"
resolved "https://registry.npmmirror.com/@sindresorhus/is/-/is-4.6.0.tgz"
@@ -1738,6 +1822,13 @@
dependencies:
"@types/node" "*"
"@types/hast@^3.0.0", "@types/hast@^3.0.4":
version "3.0.4"
resolved "https://registry.npmmirror.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa"
integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==
dependencies:
"@types/unist" "*"
"@types/http-cache-semantics@*":
version "4.0.4"
resolved "https://registry.npmmirror.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz"
@@ -1755,6 +1846,11 @@
dependencies:
"@types/node" "*"
"@types/linkify-it@^5":
version "5.0.0"
resolved "https://registry.npmmirror.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76"
integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==
"@types/lodash-es@^4.17.12":
version "4.17.12"
resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz"
@@ -1767,6 +1863,26 @@
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.20.tgz"
integrity sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==
"@types/markdown-it@^14.1.2":
version "14.1.2"
resolved "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61"
integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
dependencies:
"@types/linkify-it" "^5"
"@types/mdurl" "^2"
"@types/mdast@^4.0.0":
version "4.0.4"
resolved "https://registry.npmmirror.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6"
integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==
dependencies:
"@types/unist" "*"
"@types/mdurl@^2":
version "2.0.0"
resolved "https://registry.npmmirror.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
"@types/ms@*":
version "2.1.0"
resolved "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz"
@@ -1839,6 +1955,11 @@
resolved "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz"
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
"@types/unist@*", "@types/unist@^3.0.0":
version "3.0.3"
resolved "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c"
integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==
"@types/uuid@^10.0.0":
version "10.0.0"
resolved "https://registry.npmmirror.com/@types/uuid/-/uuid-10.0.0.tgz"
@@ -1854,6 +1975,11 @@
resolved "https://registry.npmmirror.com/@types/verror/-/verror-1.10.11.tgz"
integrity sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==
"@types/web-bluetooth@^0.0.21":
version "0.0.21"
resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz#525433c784aed9b457aaa0ee3d92aeb71f346b63"
integrity sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==
"@types/yauzl@^2.9.1":
version "2.10.3"
resolved "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz"
@@ -1959,7 +2085,12 @@
"@typescript-eslint/types" "8.39.1"
eslint-visitor-keys "^4.2.1"
"@vitejs/plugin-vue@^6.0.0":
"@ungap/structured-clone@^1.0.0":
version "1.3.0"
resolved "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8"
integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==
"@vitejs/plugin-vue@^6.0.0", "@vitejs/plugin-vue@^6.0.1":
version "6.0.1"
resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz"
integrity sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==
@@ -2029,6 +2160,17 @@
estree-walker "^2.0.2"
source-map-js "^1.2.1"
"@vue/compiler-core@3.5.21":
version "3.5.21"
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.21.tgz#5915b19273f0492336f0beb227aba86813e2c8a8"
integrity sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==
dependencies:
"@babel/parser" "^7.28.3"
"@vue/shared" "3.5.21"
entities "^4.5.0"
estree-walker "^2.0.2"
source-map-js "^1.2.1"
"@vue/compiler-dom@3.5.18", "@vue/compiler-dom@^3.3.4", "@vue/compiler-dom@^3.5.0":
version "3.5.18"
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.18.tgz"
@@ -2037,7 +2179,30 @@
"@vue/compiler-core" "3.5.18"
"@vue/shared" "3.5.18"
"@vue/compiler-sfc@3.5.18", "@vue/compiler-sfc@^3.5.18":
"@vue/compiler-dom@3.5.21":
version "3.5.21"
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz#26126447fe1e1d16c8cbac45b26e66b3f7175f65"
integrity sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==
dependencies:
"@vue/compiler-core" "3.5.21"
"@vue/shared" "3.5.21"
"@vue/compiler-sfc@3.5.21":
version "3.5.21"
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz#e48189ef3ffe334c864c2625389ebe3bb4fa41eb"
integrity sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==
dependencies:
"@babel/parser" "^7.28.3"
"@vue/compiler-core" "3.5.21"
"@vue/compiler-dom" "3.5.21"
"@vue/compiler-ssr" "3.5.21"
"@vue/shared" "3.5.21"
estree-walker "^2.0.2"
magic-string "^0.30.18"
postcss "^8.5.6"
source-map-js "^1.2.1"
"@vue/compiler-sfc@^3.5.18":
version "3.5.18"
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.18.tgz"
integrity sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==
@@ -2060,6 +2225,14 @@
"@vue/compiler-dom" "3.5.18"
"@vue/shared" "3.5.18"
"@vue/compiler-ssr@3.5.21":
version "3.5.21"
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz#f351c27aa5c075faa609596b2269c53df0df3aa1"
integrity sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==
dependencies:
"@vue/compiler-dom" "3.5.21"
"@vue/shared" "3.5.21"
"@vue/compiler-vue2@^2.7.16":
version "2.7.16"
resolved "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz"
@@ -2080,6 +2253,13 @@
dependencies:
"@vue/devtools-kit" "^7.7.7"
"@vue/devtools-api@^8.0.0":
version "8.0.1"
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-8.0.1.tgz#142f94161ec80698f57ee8379bffad969868b251"
integrity sha512-YBvjfpM7LEp5+b7ZDm4+mFrC+TgGjUmN8ff9lZcbHQ1MKhmftT/urCTZP0y1j26YQWr25l9TPaEbNLbILRiGoQ==
dependencies:
"@vue/devtools-kit" "^8.0.1"
"@vue/devtools-core@^8.0.0":
version "8.0.0"
resolved "https://registry.npmmirror.com/@vue/devtools-core/-/devtools-core-8.0.0.tgz"
@@ -2118,6 +2298,19 @@
speakingurl "^14.0.1"
superjson "^2.2.2"
"@vue/devtools-kit@^8.0.1":
version "8.0.1"
resolved "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.0.1.tgz#84b8741bbd16fa7d6d5ce0baf3c2e8b3822237c2"
integrity sha512-7kiPhgTKNtNeXltEHnJJjIDlndlJP4P+UJvCw54uVHNDlI6JzwrSiRmW4cxKTug2wDbc/dkGaMnlZghcwV+aWA==
dependencies:
"@vue/devtools-shared" "^8.0.1"
birpc "^2.5.0"
hookable "^5.5.3"
mitt "^3.0.1"
perfect-debounce "^1.0.0"
speakingurl "^14.0.1"
superjson "^2.2.2"
"@vue/devtools-shared@^7.7.7":
version "7.7.7"
resolved "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz"
@@ -2132,6 +2325,13 @@
dependencies:
rfdc "^1.4.1"
"@vue/devtools-shared@^8.0.1":
version "8.0.1"
resolved "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.0.1.tgz#ace79d39fec42f35e02d1053d5c83c4f492423a9"
integrity sha512-PqtWqPPRpMwZ9FjTzyugb5KeV9kmg2C3hjxZHwjl0lijT4QIJDd0z6AWcnbM9w2nayjDymyTt0+sbdTv3pVeNg==
dependencies:
rfdc "^1.4.1"
"@vue/language-core@3.0.5":
version "3.0.5"
resolved "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.0.5.tgz"
@@ -2146,44 +2346,76 @@
path-browserify "^1.0.1"
picomatch "^4.0.2"
"@vue/reactivity@3.5.18":
version "3.5.18"
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.18.tgz"
integrity sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==
"@vue/reactivity@3.5.21":
version "3.5.21"
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.21.tgz#34d4532c325876cf5481206060a3d525862c8ac5"
integrity sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==
dependencies:
"@vue/shared" "3.5.18"
"@vue/shared" "3.5.21"
"@vue/runtime-core@3.5.18":
version "3.5.18"
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.18.tgz"
integrity sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==
"@vue/runtime-core@3.5.21":
version "3.5.21"
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.21.tgz#d97a4e7223a99644129f95c7d8318a7e92f255e4"
integrity sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==
dependencies:
"@vue/reactivity" "3.5.18"
"@vue/shared" "3.5.18"
"@vue/reactivity" "3.5.21"
"@vue/shared" "3.5.21"
"@vue/runtime-dom@3.5.18":
version "3.5.18"
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.18.tgz"
integrity sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==
"@vue/runtime-dom@3.5.21":
version "3.5.21"
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.21.tgz#a3d35d53320abe8462c3bf2a469f729d8c9f78ff"
integrity sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==
dependencies:
"@vue/reactivity" "3.5.18"
"@vue/runtime-core" "3.5.18"
"@vue/shared" "3.5.18"
"@vue/reactivity" "3.5.21"
"@vue/runtime-core" "3.5.21"
"@vue/shared" "3.5.21"
csstype "^3.1.3"
"@vue/server-renderer@3.5.18":
version "3.5.18"
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.18.tgz"
integrity sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==
"@vue/server-renderer@3.5.21":
version "3.5.21"
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.21.tgz#1d0be5059a0c10f2c0483eef71ebf5bfd21a8b49"
integrity sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==
dependencies:
"@vue/compiler-ssr" "3.5.18"
"@vue/shared" "3.5.18"
"@vue/compiler-ssr" "3.5.21"
"@vue/shared" "3.5.21"
"@vue/shared@3.5.18", "@vue/shared@^3.5.0", "@vue/shared@^3.5.18":
version "3.5.18"
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.18.tgz"
integrity sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==
"@vue/shared@3.5.21":
version "3.5.21"
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.21.tgz#505edb122629d1979f70a2a65ca0bd4050dc2e54"
integrity sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==
"@vueuse/core@13.9.0", "@vueuse/core@^13.6.0":
version "13.9.0"
resolved "https://registry.npmmirror.com/@vueuse/core/-/core-13.9.0.tgz#051aeff47a259e9e4d7d0cc3e54879817b0cbcad"
integrity sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==
dependencies:
"@types/web-bluetooth" "^0.0.21"
"@vueuse/metadata" "13.9.0"
"@vueuse/shared" "13.9.0"
"@vueuse/integrations@^13.6.0":
version "13.9.0"
resolved "https://registry.npmmirror.com/@vueuse/integrations/-/integrations-13.9.0.tgz#1bd1d77093a327321cca00e2bbf5da7b18aa6b43"
integrity sha512-SDobKBbPIOe0cVL7QxMzGkuUGHvWTdihi9zOrrWaWUgFKe15cwEcwfWmgrcNzjT6kHnNmWuTajPHoIzUjYNYYQ==
dependencies:
"@vueuse/core" "13.9.0"
"@vueuse/shared" "13.9.0"
"@vueuse/metadata@13.9.0":
version "13.9.0"
resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-13.9.0.tgz#57c738d99661c33347080c0bc4cd11160e0d0881"
integrity sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==
"@vueuse/shared@13.9.0":
version "13.9.0"
resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-13.9.0.tgz#7168b4ed647e625b05eb4e7e80fe8aabd00e3923"
integrity sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==
"@xmldom/xmldom@^0.8.8":
version "0.8.11"
resolved "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.11.tgz"
@@ -2805,6 +3037,11 @@ caseless@~0.12.0:
resolved "https://registry.npmmirror.com/caseless/-/caseless-0.12.0.tgz"
integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==
ccount@^2.0.0:
version "2.0.1"
resolved "https://registry.npmmirror.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
centra@^2.7.0:
version "2.7.0"
resolved "https://registry.npmmirror.com/centra/-/centra-2.7.0.tgz"
@@ -2829,6 +3066,16 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
character-entities-html4@^2.0.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b"
integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==
character-entities-legacy@^3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b"
integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==
charenc@0.0.2:
version "0.0.2"
resolved "https://registry.npmmirror.com/charenc/-/charenc-0.0.2.tgz"
@@ -3016,6 +3263,11 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
comma-separated-tokens@^2.0.0:
version "2.0.3"
resolved "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
commander@^5.0.0:
version "5.1.0"
resolved "https://registry.npmmirror.com/commander/-/commander-5.1.0.tgz"
@@ -3305,6 +3557,11 @@ depd@2.0.0:
resolved "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
dequal@^2.0.0:
version "2.0.3"
resolved "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
destroy@1.2.0:
version "1.2.0"
resolved "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz"
@@ -3325,6 +3582,13 @@ detect-node@^2.0.4:
resolved "https://registry.npmmirror.com/detect-node/-/detect-node-2.1.0.tgz"
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
devlop@^1.0.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018"
integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==
dependencies:
dequal "^2.0.0"
dijkstrajs@^1.0.1:
version "1.0.3"
resolved "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz"
@@ -3997,7 +4261,7 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"
fdir@^6.4.4, fdir@^6.4.6:
fdir@^6.4.4, fdir@^6.4.6, fdir@^6.5.0:
version "6.5.0"
resolved "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz"
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
@@ -4094,6 +4358,13 @@ flatted@^3.2.9:
resolved "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz"
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
focus-trap@^7.6.5:
version "7.6.5"
resolved "https://registry.npmmirror.com/focus-trap/-/focus-trap-7.6.5.tgz#56f0814286d43c1a2688e9bc4f31f17ae047fb76"
integrity sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==
dependencies:
tabbable "^6.2.0"
follow-redirects@^1.15.6:
version "1.15.11"
resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz"
@@ -4495,6 +4766,30 @@ hasown@^2.0.2:
dependencies:
function-bind "^1.1.2"
hast-util-to-html@^9.0.5:
version "9.0.5"
resolved "https://registry.npmmirror.com/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz#ccc673a55bb8e85775b08ac28380f72d47167005"
integrity sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==
dependencies:
"@types/hast" "^3.0.0"
"@types/unist" "^3.0.0"
ccount "^2.0.0"
comma-separated-tokens "^2.0.0"
hast-util-whitespace "^3.0.0"
html-void-elements "^3.0.0"
mdast-util-to-hast "^13.0.0"
property-information "^7.0.0"
space-separated-tokens "^2.0.0"
stringify-entities "^4.0.0"
zwitch "^2.0.4"
hast-util-whitespace@^3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621"
integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==
dependencies:
"@types/hast" "^3.0.0"
he@^1.2.0:
version "1.2.0"
resolved "https://registry.npmmirror.com/he/-/he-1.2.0.tgz"
@@ -4522,6 +4817,11 @@ hpagent@^1.2.0:
resolved "https://registry.npmmirror.com/hpagent/-/hpagent-1.2.0.tgz#0ae417895430eb3770c03443456b8d90ca464903"
integrity sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==
html-void-elements@^3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7"
integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==
http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0:
version "4.2.0"
resolved "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz"
@@ -5357,6 +5657,13 @@ magic-string@^0.30.17, magic-string@^0.30.4:
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.0"
magic-string@^0.30.18:
version "0.30.19"
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.19.tgz#cebe9f104e565602e5d2098c5f2e79a77cc86da9"
integrity sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.5"
make-fetch-happen@^10.0.3:
version "10.2.1"
resolved "https://registry.npmmirror.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz"
@@ -5379,6 +5686,11 @@ make-fetch-happen@^10.0.3:
socks-proxy-agent "^7.0.0"
ssri "^9.0.0"
mark.js@8.11.1:
version "8.11.1"
resolved "https://registry.npmmirror.com/mark.js/-/mark.js-8.11.1.tgz#180f1f9ebef8b0e638e4166ad52db879beb2ffc5"
integrity sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==
marked@^16.1.2:
version "16.2.0"
resolved "https://registry.npmmirror.com/marked/-/marked-16.2.0.tgz"
@@ -5405,6 +5717,21 @@ md5@^2.3.0:
crypt "0.0.2"
is-buffer "~1.1.6"
mdast-util-to-hast@^13.0.0:
version "13.2.0"
resolved "https://registry.npmmirror.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz#5ca58e5b921cc0a3ded1bc02eed79a4fe4fe41f4"
integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==
dependencies:
"@types/hast" "^3.0.0"
"@types/mdast" "^4.0.0"
"@ungap/structured-clone" "^1.0.0"
devlop "^1.0.0"
micromark-util-sanitize-uri "^2.0.0"
trim-lines "^3.0.0"
unist-util-position "^5.0.0"
unist-util-visit "^5.0.0"
vfile "^6.0.0"
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz"
@@ -5430,6 +5757,38 @@ methods@~1.1.2:
resolved "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz"
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
micromark-util-character@^2.0.0:
version "2.1.1"
resolved "https://registry.npmmirror.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6"
integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==
dependencies:
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-util-encode@^2.0.0:
version "2.0.1"
resolved "https://registry.npmmirror.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8"
integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==
micromark-util-sanitize-uri@^2.0.0:
version "2.0.1"
resolved "https://registry.npmmirror.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7"
integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==
dependencies:
micromark-util-character "^2.0.0"
micromark-util-encode "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-symbol@^2.0.0:
version "2.0.1"
resolved "https://registry.npmmirror.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8"
integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==
micromark-util-types@^2.0.0:
version "2.0.2"
resolved "https://registry.npmmirror.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e"
integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==
micromatch@^4.0.5, micromatch@^4.0.8:
version "4.0.8"
resolved "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz"
@@ -5571,6 +5930,11 @@ minipass@^5.0.0:
resolved "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz"
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
minisearch@^7.1.2:
version "7.1.2"
resolved "https://registry.npmmirror.com/minisearch/-/minisearch-7.1.2.tgz#296ee8d1906cc378f7e57a3a71f07e5205a75df5"
integrity sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA==
minizlib@^2.1.1, minizlib@^2.1.2:
version "2.1.2"
resolved "https://registry.npmmirror.com/minizlib/-/minizlib-2.1.2.tgz"
@@ -5878,6 +6242,20 @@ onetime@^5.1.0:
dependencies:
mimic-fn "^2.1.0"
oniguruma-parser@^0.12.1:
version "0.12.1"
resolved "https://registry.npmmirror.com/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz#82ba2208d7a2b69ee344b7efe0ae930c627dcc4a"
integrity sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==
oniguruma-to-es@^4.3.3:
version "4.3.3"
resolved "https://registry.npmmirror.com/oniguruma-to-es/-/oniguruma-to-es-4.3.3.tgz#50db2c1e28ec365e102c1863dfd3d1d1ad18613e"
integrity sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==
dependencies:
oniguruma-parser "^0.12.1"
regex "^6.0.1"
regex-recursion "^6.0.2"
open@^10.2.0:
version "10.2.0"
resolved "https://registry.npmmirror.com/open/-/open-10.2.0.tgz"
@@ -6367,6 +6745,11 @@ promise-retry@^2.0.1:
err-code "^2.0.2"
retry "^0.12.0"
property-information@^7.0.0:
version "7.1.0"
resolved "https://registry.npmmirror.com/property-information/-/property-information-7.1.0.tgz#b622e8646e02b580205415586b40804d3e8bfd5d"
integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==
proxy-addr@~2.0.7:
version "2.0.7"
resolved "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz"
@@ -6537,6 +6920,25 @@ regenerator-runtime@^0.13.3:
resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regex-recursion@^6.0.2:
version "6.0.2"
resolved "https://registry.npmmirror.com/regex-recursion/-/regex-recursion-6.0.2.tgz#a0b1977a74c87f073377b938dbedfab2ea582b33"
integrity sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==
dependencies:
regex-utilities "^2.3.0"
regex-utilities@^2.3.0:
version "2.3.0"
resolved "https://registry.npmmirror.com/regex-utilities/-/regex-utilities-2.3.0.tgz#87163512a15dce2908cf079c8960d5158ff43280"
integrity sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==
regex@^6.0.1:
version "6.0.1"
resolved "https://registry.npmmirror.com/regex/-/regex-6.0.1.tgz#282fa4435d0c700b09c0eb0982b602e05ab6a34f"
integrity sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==
dependencies:
regex-utilities "^2.3.0"
request-progress@^2.0.1:
version "2.0.1"
resolved "https://registry.npmmirror.com/request-progress/-/request-progress-2.0.1.tgz"
@@ -7002,6 +7404,20 @@ shebang-regex@^3.0.0:
resolved "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shiki@^3.9.2:
version "3.12.2"
resolved "https://registry.npmmirror.com/shiki/-/shiki-3.12.2.tgz#0ca67fb4b76519f0c0057acb758d521efa6e13a0"
integrity sha512-uIrKI+f9IPz1zDT+GMz+0RjzKJiijVr6WDWm9Pe3NNY6QigKCfifCEv9v9R2mDASKKjzjQ2QpFLcxaR3iHSnMA==
dependencies:
"@shikijs/core" "3.12.2"
"@shikijs/engine-javascript" "3.12.2"
"@shikijs/engine-oniguruma" "3.12.2"
"@shikijs/langs" "3.12.2"
"@shikijs/themes" "3.12.2"
"@shikijs/types" "3.12.2"
"@shikijs/vscode-textmate" "^10.0.2"
"@types/hast" "^3.0.4"
side-channel-list@^1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz"
@@ -7148,6 +7564,11 @@ source-map@^0.6.0, source-map@~0.6.1:
resolved "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
space-separated-tokens@^2.0.0:
version "2.0.2"
resolved "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f"
integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==
spdx-correct@^3.0.0:
version "3.2.0"
resolved "https://registry.npmmirror.com/spdx-correct/-/spdx-correct-3.2.0.tgz"
@@ -7271,6 +7692,14 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
stringify-entities@^4.0.0:
version "4.0.4"
resolved "https://registry.npmmirror.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3"
integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==
dependencies:
character-entities-html4 "^2.0.0"
character-entities-legacy "^3.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz"
@@ -7405,6 +7834,11 @@ synckit@^0.11.7:
dependencies:
"@pkgr/core" "^0.2.9"
tabbable@^6.2.0:
version "6.2.0"
resolved "https://registry.npmmirror.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.1.2:
version "6.2.1"
resolved "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz"
@@ -7484,6 +7918,14 @@ tinyglobby@^0.2.14:
fdir "^6.4.4"
picomatch "^4.0.2"
tinyglobby@^0.2.15:
version "0.2.15"
resolved "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
dependencies:
fdir "^6.5.0"
picomatch "^4.0.3"
tmp-promise@^3.0.2:
version "3.0.3"
resolved "https://registry.npmmirror.com/tmp-promise/-/tmp-promise-3.0.3.tgz"
@@ -7534,6 +7976,11 @@ tr46@~0.0.3:
resolved "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
trim-lines@^3.0.0:
version "3.0.1"
resolved "https://registry.npmmirror.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==
truncate-utf8-bytes@^1.0.0:
version "1.0.2"
resolved "https://registry.npmmirror.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz"
@@ -7662,6 +8109,44 @@ unique-slug@^3.0.0:
dependencies:
imurmurhash "^0.1.4"
unist-util-is@^6.0.0:
version "6.0.0"
resolved "https://registry.npmmirror.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424"
integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==
dependencies:
"@types/unist" "^3.0.0"
unist-util-position@^5.0.0:
version "5.0.0"
resolved "https://registry.npmmirror.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4"
integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==
dependencies:
"@types/unist" "^3.0.0"
unist-util-stringify-position@^4.0.0:
version "4.0.0"
resolved "https://registry.npmmirror.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2"
integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==
dependencies:
"@types/unist" "^3.0.0"
unist-util-visit-parents@^6.0.0:
version "6.0.1"
resolved "https://registry.npmmirror.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815"
integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==
dependencies:
"@types/unist" "^3.0.0"
unist-util-is "^6.0.0"
unist-util-visit@^5.0.0:
version "5.0.0"
resolved "https://registry.npmmirror.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6"
integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==
dependencies:
"@types/unist" "^3.0.0"
unist-util-is "^6.0.0"
unist-util-visit-parents "^6.0.0"
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.npmmirror.com/universalify/-/universalify-0.1.2.tgz"
@@ -7836,6 +8321,22 @@ verror@^1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
vfile-message@^4.0.0:
version "4.0.3"
resolved "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4"
integrity sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==
dependencies:
"@types/unist" "^3.0.0"
unist-util-stringify-position "^4.0.0"
vfile@^6.0.0:
version "6.0.3"
resolved "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab"
integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==
dependencies:
"@types/unist" "^3.0.0"
vfile-message "^4.0.0"
vite-dev-rpc@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz"
@@ -7926,6 +8427,44 @@ vite@^7.0.5:
optionalDependencies:
fsevents "~2.3.3"
vite@^7.1.2:
version "7.1.5"
resolved "https://registry.npmmirror.com/vite/-/vite-7.1.5.tgz#4dbcb48c6313116689be540466fc80faa377be38"
integrity sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==
dependencies:
esbuild "^0.25.0"
fdir "^6.5.0"
picomatch "^4.0.3"
postcss "^8.5.6"
rollup "^4.43.0"
tinyglobby "^0.2.15"
optionalDependencies:
fsevents "~2.3.3"
vitepress@^2.0.0-alpha.12:
version "2.0.0-alpha.12"
resolved "https://registry.npmmirror.com/vitepress/-/vitepress-2.0.0-alpha.12.tgz#e75648eec6c43bff1d669f9a7f81f777acc6e4fd"
integrity sha512-yZwCwRRepcpN5QeAhwSnEJxS3I6zJcVixqL1dnm6km4cnriLpQyy2sXQDsE5Ti3pxGPbhU51nTMwI+XC1KNnJg==
dependencies:
"@docsearch/css" "^4.0.0-beta.7"
"@docsearch/js" "^4.0.0-beta.7"
"@iconify-json/simple-icons" "^1.2.47"
"@shikijs/core" "^3.9.2"
"@shikijs/transformers" "^3.9.2"
"@shikijs/types" "^3.9.2"
"@types/markdown-it" "^14.1.2"
"@vitejs/plugin-vue" "^6.0.1"
"@vue/devtools-api" "^8.0.0"
"@vue/shared" "^3.5.18"
"@vueuse/core" "^13.6.0"
"@vueuse/integrations" "^13.6.0"
focus-trap "^7.6.5"
mark.js "8.11.1"
minisearch "^7.1.2"
shiki "^3.9.2"
vite "^7.1.2"
vue "^3.5.18"
vscode-uri@^3.0.8:
version "3.1.0"
resolved "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz"
@@ -7958,16 +8497,16 @@ vue-tsc@^3.0.3:
"@volar/typescript" "2.4.22"
"@vue/language-core" "3.0.5"
vue@^3.5.17:
version "3.5.18"
resolved "https://registry.npmmirror.com/vue/-/vue-3.5.18.tgz"
integrity sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==
vue@^3.5.18, vue@^3.5.21:
version "3.5.21"
resolved "https://registry.npmmirror.com/vue/-/vue-3.5.21.tgz#30af9553fd9642870321b7dc547b46c395cb7b91"
integrity sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==
dependencies:
"@vue/compiler-dom" "3.5.18"
"@vue/compiler-sfc" "3.5.18"
"@vue/runtime-dom" "3.5.18"
"@vue/server-renderer" "3.5.18"
"@vue/shared" "3.5.18"
"@vue/compiler-dom" "3.5.21"
"@vue/compiler-sfc" "3.5.21"
"@vue/runtime-dom" "3.5.21"
"@vue/server-renderer" "3.5.21"
"@vue/shared" "3.5.21"
wcwidth@^1.0.1:
version "1.0.1"
@@ -8263,3 +8802,8 @@ zod@^3.25.32:
version "3.25.76"
resolved "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz"
integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
zwitch@^2.0.4:
version "2.0.4"
resolved "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"
integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==