PKC音色管理后台v1.0.0

This commit is contained in:
curtinlv 2024-11-02 02:00:46 +08:00
parent 154b1a45cc
commit 05bd4aa961
10 changed files with 30207 additions and 2 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
PKC_PASSWORD=Abc!@123456

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM python:3.10-slim
COPY PKCYsManage ./app
# 设置工作目录
WORKDIR /app
VOLUME ["/app"]
# 设置环境变量
ENV PKC_VERSION=v1.0.0
# 安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 39900
# 设置容器启动时执行的命令
CMD ["python", "main.py"]

10
PKCYsManage/config.json Normal file
View File

@ -0,0 +1,10 @@
{
"标题": "PKC音色管理后台",
"端口": "39900",
"users": [
{
"username": "pkc",
"password": "pkc"
}
]
}

330
PKCYsManage/main.py Normal file
View File

@ -0,0 +1,330 @@
# -*- coding:utf-8 -*-
from flask import Flask, render_template, request, jsonify, send_from_directory, redirect, url_for, abort, Response, session, flash
import json
import os
import shutil
from werkzeug.utils import secure_filename
import datetime
app = Flask(__name__)
app.secret_key = 'pkc' # 用于会话管理
# 设置文件路径
JSON_FILE = 'ys.json'
# BG_SOUND_DIR = 'bgSound' # 背景音文件夹
# 读取 JSON 文件
def read_json_file(file):
if os.path.exists(file):
with open(file, 'r', encoding='utf-8') as f:
return json.load(f)
return {}
# 加载用户数据
def load_users():
with open('config.json', 'r', encoding='utf-8') as f:
data = json.load(f)
return data['users']
def get_config():
with open('config.json', 'r', encoding='utf-8') as f:
data = json.load(f)
return data
userConfig = get_config()
PKC_USER = os.environ.get('PKC_USER')
PKC_PASSWORD = os.environ.get('PKC_PASSWORD')
PKC_VERSION = os.environ.get('PKC_VERSION')
PKC_TITLE = os.environ.get('PKC_TITLE')
if PKC_USER is None:
PKC_USER = userConfig['users'][0]['username']
if PKC_PASSWORD is None:
PKC_PASSWORD = userConfig['users'][0]['password']
if PKC_VERSION is None:
PKC_VERSION = 'v1.0.0'
if PKC_TITLE is None:
PKC_TITLE = userConfig['标题']
# 导出 JSON 文件
@app.route('/ysList')
def printYsList():
# return jsonify(read_json_file(JSON_FILE))
# 将数据转换为格式化的 JSON 字符串
json_data = json.dumps(read_json_file(JSON_FILE), ensure_ascii=False, indent=4)
# 创建响应对象,设置内容类型为 JSON
return Response(json_data, mimetype='application/json; charset=utf-8')
# 保存 JSON 文件
def save_json_file(file, data):
with open(file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
# 添加分类
def add_category(audio_colors, name, token, sort, desc, url, alias, type):
if name not in audio_colors:
audio_colors[name] = {
'token': token,
'sort': sort,
'desc': desc,
'url': url,
'alias': alias,
'type': type,
'list': []
}
save_json_file(JSON_FILE, audio_colors)
return f"分类【{name}】 已添加。"
else:
return f"分类【{name}】 已存在。"
# 修改分类
def edit_category(audio_colors, old_name, new_name, token, sort, desc, url, alias, type):
if old_name in audio_colors:
audio_colors[new_name] = {
'token': token,
'sort': sort,
'desc': desc,
'url': url,
'alias': alias,
'type': type,
'list': audio_colors[old_name]['list']
}
if new_name != old_name:
del audio_colors[old_name]
save_json_file(JSON_FILE, audio_colors)
return f"分类【{old_name}】 已修改为【{new_name}】。"
else:
return f"分类【{old_name}】 不存在。"
# 删除分类
def delete_category(audio_colors, name):
if name in audio_colors:
del audio_colors[name]
save_json_file(JSON_FILE, audio_colors)
return f"分类【{name}】 已删除。"
else:
return f"分类【{name}】 不存在。"
# 管理分类中的 list
def add_to_list(audio_colors, category_name, name, desc, vid, img):
if category_name in audio_colors:
if vid_exists(audio_colors, vid):
return f"音色 ID【{vid}】 已存在。"
else:
audio_colors[category_name]['list'].append({
'name': name,
'desc': desc,
'img': img,
'vid': vid
})
save_json_file(JSON_FILE, audio_colors)
return f"音色【{name}】 已添加到分类【{category_name}】。"
else:
return f"分类【{category_name}】 不存在。"
def edit_list_item(audio_colors, category_name, old_name, new_name, desc, vid, img):
if category_name in audio_colors:
for i, item in enumerate(audio_colors[category_name]['list']):
if item['name'] == old_name:
if item['vid'] != vid and vid_exists(audio_colors, vid):
return f"音色 ID【{vid}】 已存在。"
else:
audio_colors[category_name]['list'][i] = {
'name': new_name,
'desc': desc,
'img': img,
'vid': vid
}
save_json_file(JSON_FILE, audio_colors)
return f"音色【{old_name}】 已修改为【{new_name}】。"
return f"音色【{old_name}】 不存在于分类【{category_name}】。"
else:
return f"分类【{category_name}】不存在。"
# 删除 list 中的音色
def delete_from_list(audio_colors, category_name, name):
if category_name in audio_colors:
audio_colors[category_name]['list'] = [
item for item in audio_colors[category_name]['list'] if item['name'] != name
]
save_json_file(JSON_FILE, audio_colors)
return f"音色【{name}】已从分类【{category_name}】删除。"
else:
return f"分类【{category_name}】 不存在。"
# 检查音色 ID 是否已存在
def vid_exists(audio_colors, vid):
for category_data in audio_colors.values():
for item in category_data['list']:
if item['vid'] == vid:
return True
return False
def backup_json_file(file):
# 获取当前时间
backup_time = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
# 创建备份目录
backup_dir = 'backup'
if not os.path.exists(backup_dir):
os.makedirs(backup_dir)
# 生成备份文件名
backup_file = os.path.join(backup_dir, f"{os.path.basename(file)}_{backup_time}.json")
# 复制文件到备份目录
shutil.copyfile(file, backup_file)
return f"备份成功,备份文件名为:{backup_dir}/{file}_{backup_time}.json"
# 导出 JSON 文件
@app.route('/export')
def export_json():
if 'username' not in session:
flash('请先登录!', 'warning')
return redirect(url_for('index'))
return send_from_directory(os.path.dirname(JSON_FILE), os.path.basename(JSON_FILE), as_attachment=True)
@app.route('/logout')
def logout():
session.pop('username', None)
flash('已成功注销!', 'info')
return redirect(url_for('index'))
@app.route('/')
def index():
return render_template('login.html', titleName=PKC_TITLE, PKC_VERSION=PKC_VERSION)
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
# users = load_users()
users = [{"username": PKC_USER, "password": PKC_PASSWORD}]
# 验证用户
for user in users:
if user['username'] == username and user['password'] == password:
session['username'] = username
flash('登录成功!', 'success')
return redirect(url_for('dashboard'))
flash('用户名或密码错误!', 'danger')
return render_template('login.html', titleName=PKC_TITLE, PKC_VERSION=PKC_VERSION)
# 处理表单提交
@app.route('/dashboard', methods=['POST', 'GET'])
def dashboard():
audio_colors = read_json_file(JSON_FILE)
response = None
curtabName = None
if 'username' not in session:
flash('请先登录!', 'warning')
return redirect(url_for('index'))
# if request.method == 'GET':
# # 获取 URL 参数 y 的值
# y = request.args.get('y', type=int)
if request.method == 'POST':
action = request.form.get('action')
# 添加分类
if action == 'add_category':
name = request.form.get('name')
token = request.form.get('token')
sort = request.form.get('sort')
desc = request.form.get('desc')
url = request.form.get('url')
alias = request.form.get('alias')
type = request.form.get('type')
curtabName='addCategory'
response = add_category(audio_colors, name, token, sort, desc, url, alias, type)
# 修改分类
elif action == 'edit_category':
old_name = request.form.get('old_name')
new_name = request.form.get('new_name')
token = request.form.get('token')
sort = request.form.get('sort')
desc = request.form.get('desc')
url = request.form.get('url')
alias = request.form.get('alias')
type = request.form.get('type')
response = edit_category(audio_colors, old_name, new_name, token, sort, desc, url, alias, type)
curtabName='editCategory'
# 删除分类
elif action == 'delete_category':
name = request.form.get('name')
response = delete_category(audio_colors, name)
curtabName='deleteCategory'
# 添加到 list
elif action == 'add_to_list':
category_name = request.form.get('category_name')
name = request.form.get('name')
desc = request.form.get('desc')
vid = request.form.get('vid')
img = request.form.get('img')
response = add_to_list(audio_colors, category_name, name, desc, vid, img)
curtabName='addToList'
# 修改 list 中的音色
elif action == 'edit_list_item':
category_name = request.form.get('category_name')
old_name = request.form.get('old_name')
new_name = request.form.get('new_name')
desc = request.form.get('desc')
vid = request.form.get('vid')
img = request.form.get('img')
response = edit_list_item(audio_colors, category_name, old_name, new_name, desc, vid, img)
curtabName='editListItem'
# 删除 list 中的音色
elif action == 'delete_from_list':
category_name = request.form.get('category_name')
name = request.form.get('name')
response = delete_from_list(audio_colors, category_name, name)
curtabName='deleteFromList'
# 备份
elif action == 'backup':
response = backup_json_file(JSON_FILE)
curtabName='backup'
# 导出
elif action == 'export':
# 重定向给 当前路由/export
return redirect(url_for('export_json')) # 重定向到 export_json 路由
# 导入
elif action == 'import':
if 'import_file' in request.files:
file = request.files['import_file']
if file.filename.endswith('.json'):
backup_time = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
backup_dir = 'backup'
if not os.path.exists(backup_dir):
os.makedirs(backup_dir)
# 生成备份文件名
backup_file = os.path.join(backup_dir, f"{os.path.basename(JSON_FILE)}_{backup_time}.json")
os.rename(JSON_FILE, backup_file)
file.save(JSON_FILE)
response = f"导入成功,原文件已备份为:{backup_file}"
else:
response = "导入失败,请上传正确的 JSON 文件。"
else:
response = "导入失败,请上传文件。"
curtabName='import'
# 读取所有音色
audio_colors = read_json_file(JSON_FILE)
# 获取第一个分类的名称和音色列表
first_category_name = list(audio_colors.keys())[0] if audio_colors else None
first_category_audio_colors = audio_colors.get(first_category_name, {}).get('list', [])
return render_template('index.html',
audio_colors=audio_colors,
ysCount=len(audio_colors),
first_category_name=first_category_name,
first_category_audio_colors=first_category_audio_colors,
curtabName=curtabName,
titleName=PKC_TITLE,
PKC_VERSION=PKC_VERSION,
response=response) # 将 response 传递到模板
if __name__ == '__main__':
protValue = userConfig['端口']
port = protValue if len(protValue) > 0 else "39900"
app.run(host='0.0.0.0', port=port, debug=False)

View File

@ -0,0 +1,707 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ titleName }}{{ PKC_VERSION }}</title>
<style>
/* CSS 代码 */
body {
font-family: 'Roboto', sans-serif; /* 使用现代字体 Roboto */
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #e0eafc, #cfdef3); /* 更柔和的渐变背景 */
/*background: linear-gradient(120deg, #00ccff, #ff00ff);*/
color: #333;
overflow: hidden;
display: flex; /* 使用 flexbox 布局 */
flex-direction: column;
align-items: center; /* 居中内容 */
}
h2 {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
font-size: 2.5rem; /* 更大的标题 */
font-weight: 600; /* 更粗的标题 */
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 添加阴影 */
}
.form-group {
margin-bottom: 20px;
width: 100%;
max-width: 500px; /* 限制表单宽度 */
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
input, select {
width: 100%;
padding: 12px 15px;
border: 2px solid #ccc;
border-radius: 6px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
transition: border-color 0.3s, box-shadow 0.3s;
font-size: 16px;
background-color: #f9f9f9; /* 添加更浅的背景色 */
}
input:focus, select:focus {
border-color: #3498db;
box-shadow: 0 0 8px rgba(52, 152, 219, 0.5);
outline: none;
}
button {
padding: 12px 20px;
background-color: #2ecc71;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.3s, transform 0.3s;
font-size: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* 添加阴影 */
}
button:hover {
background-color: #27ae60;
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.2); /* 悬停时阴影更明显 */
}
button:focus {
outline: none;
}
.tab {
overflow: hidden;
border: 1px solid #ccc;
background-color: #f1f1f1;
border-radius: 8px;
width: 100%;
max-width: 100%; /* 限制 Tab 宽度 */
margin-bottom: 20px; /* 添加 Tab 与内容之间的间隔 */
}
.tab button {
background-color: inherit;
float: left;
border: none;
outline: none;
cursor: pointer;
padding: 14px 16px;
transition: 0.3s;
font-size: 17px;
color: #2c3e50;
border-right: 1px solid #ccc;
font-weight: bold; /* 加粗 Tab 标题 */
}
.tab button:last-child {
border-right: none;
}
.tab button:hover {
background-color: #e0e0e0;
}
.tab button.active {
background-color: #2c3e50; /* 使用深蓝色作为激活状态 */
color: #fff;
}
.tabcontent {
display: block; /* 确保内容可见 */
padding: 20px;
border: 1px solid #ccc;
border-top: none;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
/*background-color: #fff;*/
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
max-height: 500px; /* 设置最大高度 */
overflow-y: auto; /* 允许垂直滚动 */
width: 100%; /* 使宽度适应父容器 */
box-sizing: border-box; /* 包括内边距和边框在宽度计算内 */
}
.search-input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 5px;
}
.custom-dropdown {
position: relative;
}
#search_input_edit, #search_input_delete {
width: 100%;
padding: 12px 15px;
border: 2px solid #ccc;
border-radius: 6px;
box-sizing: border-box;
font-size: 16px;
background-color: #f9f9f9; /* 添加更浅的背景色 */
}
.dropdown-options {
position: absolute;
width: 100%;
max-height: 200px;
overflow-y: auto;
border: 1px solid #ccc;
border-top: none;
background-color: white;
display: none;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 999;
}
.dropdown-item {
padding: 10px;
cursor: pointer;
transition: background-color 0.3s;
}
.dropdown-item:hover {
background-color: #f0f0f0;
}
/* 显示下拉框 */
#search_input_edit:focus + .dropdown-options,
#search_input_delete:focus + .dropdown-options {
display: block;
}
#urlModal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
text-align: center;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
</style>
</head>
<body>
<h2>{{ titleName }} {{ PKC_VERSION }}</h2>
<div class="tab">
<button class="tablinks active" onclick="openTab(event, 'addCategory')">添加分类</button>
<button class="tablinks" onclick="openTab(event, 'editCategory')">修改分类</button>
<button class="tablinks" onclick="openTab(event, 'deleteCategory')">删除分类</button>
<button class="tablinks" onclick="openTab(event, 'addToList')">添加音色</button>
<button class="tablinks" onclick="openTab(event, 'editListItem')">修改音色</button>
<button class="tablinks" onclick="openTab(event, 'deleteFromList')">删除音色</button>
<button class="tablinks" onclick="openTab(event, 'backup')">备份</button>
<button class="tablinks" onclick="openTab(event, 'export')">导出</button>
<button class="tablinks" onclick="openTab(event, 'import')">导入</button>
<button class="tablinks" onclick="getYsList()">音色接口</button>
<button class="tablinks" onclick="loginout()">退出</button>
</div>
<div id="addCategory" class="tabcontent" style="display: block;">
<h3>添加分类</h3>
<form method="POST" onsubmit="return validateForm('add_category','addCategory')">
<input type="hidden" name="action" value="add_category">
<div class="form-group">
<label for="type">音色接口:</label>
<select id="type" name="type">
<option value="fish.audio" selected>fish.audio</option>
<option value="FineVoice">FineVoice</option>
<option value="琅琅">琅琅</option>
<option value="讯飞">讯飞</option>
<option value="acgn.ttson.cn">二次元(原神)</option>
</select>
</div>
<div class="form-group">
<label for="name">*分类名称:</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="token">是否需要Token:</label>
<select id="token" name="token">
<option value="yes" selected>Yes</option>
<option value="no">No</option>
</select>
</div>
<div class="form-group">
<label for="sort">*排序:</label>
<input type="number" id="sort" name="sort" required>
</div>
<div class="form-group">
<label for="url">URL:</label>
<input type="text" id="url" name="url">
</div>
<div class="form-group">
<label for="alias">别名:</label>
<input type="text" id="alias" name="alias">
</div>
<div class="form-group">
<label for="desc">描述:</label>
<input type="text" id="desc" name="desc">
</div>
<button type="submit">添加分类</button>
</form>
</div>
<div id="editCategory" class="tabcontent">
<h3>修改分类</h3>
<form method="POST" onsubmit="return validateForm('edit_category','editCategory')">
<input type="hidden" name="action" value="edit_category">
<div class="form-group">
<label for="old_name">选择分类:</label>
<select id="old_name" name="old_name" required onchange="fillCategoryDetails()" onkeyup="filterSelectOptions('old_name')">
{% for category_name in audio_colors.keys() %}
<option value="{{ category_name }}">{{ category_name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="type">音色接口:</label>
<select id="type" name="type">
<option value="fish.audio">fish.audio</option>
<option value="FineVoice">FineVoice</option>
<option value="琅琅" selected>琅琅</option>
<option value="讯飞">讯飞</option>
<option value="acgn.ttson.cn">二次元(原神)</option>
</select>
</div>
<div class="form-group">
<label for="new_name">*新分类名称:</label>
<input type="text" id="new_name" name="new_name" required>
</div>
<div class="form-group">
<label for="token">是否需要Token:</label>
<select id="token" name="token">
<option value="yes" selected>Yes</option>
<option value="no">No</option>
</select>
</div>
<div class="form-group">
<label for="sort">*排序:</label>
<input type="number" id="sort" name="sort" required>
</div>
<div class="form-group">
<label for="url">URL:</label>
<input type="text" id="url" name="url">
</div>
<div class="form-group">
<label for="alias">别名:</label>
<input type="text" id="alias" name="alias">
</div>
<div class="form-group">
<label for="desc">描述:</label>
<input type="text" id="desc" name="desc">
</div>
<button type="submit">修改分类</button>
</form>
</div>
<div id="deleteCategory" class="tabcontent">
<h3>删除分类</h3>
<form method="POST" onsubmit="return validateForm('delete_category','deleteCategory')">
<input type="hidden" name="action" value="delete_category">
<div class="form-group">
<label for="name">选择分类:</label>
<select id="name" name="name" required>
{% for category_name in audio_colors.keys() %}
<option value="{{ category_name }}">{{ category_name }}</option>
{% endfor %}
</select>
</div>
<button type="submit">删除分类</button>
</form>
</div>
<div id="addToList" class="tabcontent">
<h3>添加音色到分类</h3>
<form method="POST" onsubmit="return validateForm('add_to_list','addToList')">
<input type="hidden" name="action" value="add_to_list">
<div class="form-group">
<label for="category_name_add">选择分类:</label>
<select id="category_name_add" name="category_name" required>
{% for category_name in audio_colors.keys() %}
<option value="{{ category_name }}">{{ category_name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="name">*音色名称:</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="desc">*描述:</label>
<input type="text" id="desc" name="desc" required>
</div>
<div class="form-group">
<label for="vid">*音色 ID:</label>
<input type="text" id="vid" name="vid" required>
</div>
<div class="form-group">
<label for="img">图片链接:</label>
<input type="text" id="img" name="img">
</div>
<button type="submit">添加音色</button>
</form>
</div>
<div id="editListItem" class="tabcontent">
<h3>修改音色</h3>
<form method="POST" onsubmit="return validateForm('edit_list_item','editListItem')">
<input type="hidden" name="action" value="edit_list_item">
<div class="form-group">
<label for="category_name_edit">选择分类:</label>
<select id="category_name_edit" name="category_name" required onchange="updateAudioColorsList('edit')">
{% for category_name in audio_colors.keys() %}
<option value="{{ category_name }}" {% if category_name == first_category_name %}selected{% endif %}>{{ category_name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="old_name_edit">选择音色:</label>
<div class="custom-dropdown">
<input type="text" id="search_input_edit" placeholder="搜索音色(输入要搜索的内容然后点下面的下拉菜单)..." onkeyup="filterSelectOptions('edit')" autocomplete="off" />
<select id="old_name_edit" name="old_name" onchange="fillAudioColorDetails('edit')">
<!-- 音色选项将在这里动态生成 -->
</select>
<div id="dropdown_options_edit" class="dropdown-options">
<!-- 音色选项将在这里动态生成 -->
</div>
</div>
</div>
<div class="form-group">
<label for="new_name">新音色名称:</label>
<input type="text" id="new_name" name="new_name" required>
</div>
<div class="form-group">
<label for="desc">*描述:</label>
<input type="text" id="desc" name="desc" required>
</div>
<div class="form-group">
<label for="vid">*音色 ID:</label>
<input type="text" id="vid" name="vid" required>
</div>
<div class="form-group">
<label for="img">图片链接:</label>
<input type="text" id="img" name="img">
</div>
<button type="submit">修改音色</button>
</form>
</div>
<div id="deleteFromList" class="tabcontent">
<h3>删除音色</h3>
<form method="POST" onsubmit="return validateForm('old_name_delete','deleteFromList')">
<input type="hidden" name="action" value="delete_from_list">
<div class="form-group">
<label for="category_name_delete">选择分类:</label>
<select id="category_name_delete" name="category_name" required onchange="updateAudioColorsList('delete')">
{% for category_name in audio_colors.keys() %}
<option value="{{ category_name }}" {% if category_name == first_category_name %}selected{% endif %}>{{ category_name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="old_name_delete">选择音色:</label>
<div class="custom-dropdown">
<input type="text" id="search_input_delete" placeholder="搜索音色(输入要搜索的内容然后点下面的下拉菜单)..." onkeyup="filterSelectOptions('delete')" autocomplete="off" />
<select id="old_name_delete" name="name" onchange="fillAudioColorDetailsForDelete('delete')">
<!-- 音色选项将在这里动态生成 -->
</select>
<div id="dropdown_options_delete" class="dropdown-options">
<!-- 音色选项将在这里动态生成 -->
</div>
</div>
</div>
<button type="submit">删除音色</button>
</form>
</div>
<div id="backup" class="tabcontent">
<h3>备份</h3>
<form method="POST" onsubmit="return validateForm('backup','backup')">
<input type="hidden" name="action" value="backup">
<button type="submit">备份</button>
</form>
</div>
<div id="export" class="tabcontent">
<h3>导出</h3>
<form method="POST" onsubmit="return validateForm('export','export')">
<input type="hidden" name="action" value="export">
<button type="submit">导出</button>
</form>
</div>
<div id="import" class="tabcontent">
<h3>导入</h3>
<form method="POST" enctype="multipart/form-data" onsubmit="return validateForm('import','import')">
<input type="hidden" name="action" value="import">
<div class="form-group">
<label for="import_file">选择 PKC音色JSON文件:</label>
<input type="file" id="import_file" name="import_file" accept=".json" required>
</div>
<button type="submit">导入</button>
</form>
</div>
<div id="urlModal" style="display:none;">
<div class="modal-content">
<p id="urlText"></p>
<button id="copyButton">复制</button>
<button id="openButton">访问</button>
<button id="cancelButton">取消</button>
</div>
</div>
<script>
function showResponseMessage(message) {
// 导航到新URL
alert(message); // 使用 alert() 函数显示弹窗提示
var currentUrl = window.location.href;
window.location.href = currentUrl;
}
function openTab(evt, tabName) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tabName).style.display = "block";
evt.currentTarget.className += " active";
}
function updateAudioColorsList(action) {
var categorySelect = document.getElementById("category_name_" + action);
var selectedCategory = categorySelect.value;
// 获取对应分类的音色列表
var audioColors = {{ audio_colors | tojson | safe }}; // 使用 JSON.stringify 转换
var categoryData = audioColors[selectedCategory];
var dropdownOptions = document.getElementById('dropdown_options_' + action);
dropdownOptions.innerHTML = ''; // 清空下拉选项
var oldNameSelect = document.getElementById('old_name_' + action);
oldNameSelect.innerHTML = ''; // 清空下拉选项
if (categoryData && categoryData['list']) {
categoryData['list'].forEach(function(item) {
var dropdownItem = document.createElement('div');
dropdownItem.classList.add('dropdown-item');
dropdownItem.textContent = item['name'] + ' | ' + item['desc'];
dropdownItem.dataset.desc = item['desc'] + '|' + item['img'] + '|' + item['vid'];
dropdownItem.addEventListener('click', function() {
document.getElementById('search_input_' + action).value = this.textContent;
if (action === 'edit') {
fillAudioColorDetails(action);
} else if (action === 'delete') {
fillAudioColorDetailsForDelete('delete');
}
});
dropdownOptions.appendChild(dropdownItem);
var option = document.createElement('option');
option.value = item['name'];
option.textContent = item['name'] + ' | ' + item['desc'];
option.dataset.desc = item['desc'] + '|' + item['img'] + '|' + item['vid'];
oldNameSelect.appendChild(option);
});
}
}
function fillCategoryDetails() {
var selectedCategory = document.getElementById('old_name').value;
var categoryData = {{ audio_colors | tojson | safe }};
document.getElementById('editCategory').querySelector('#new_name').value = selectedCategory;
document.getElementById('editCategory').querySelector('#desc').value = categoryData[selectedCategory]['desc'];
document.getElementById('editCategory').querySelector('#token').value = categoryData[selectedCategory]['token'];
document.getElementById('editCategory').querySelector('#sort').value = categoryData[selectedCategory]['sort']; // 填充 sort 值
document.getElementById('editCategory').querySelector('#url').value = categoryData[selectedCategory]['url']; // 填充 sort 值
document.getElementById('editCategory').querySelector('#type').value = categoryData[selectedCategory]['type']; // 填充 sort 值
document.getElementById('editCategory').querySelector('#alias').value = categoryData[selectedCategory]['alias']; // 填充 sort 值
}
function fillCategoryAdd() {
document.getElementById('addCategory').querySelector('#sort').value = {{ ysCount }};
}
function fillAudioColorDetails(action) {
var selectedItemText = document.getElementById('old_name_edit').value;
var descAndImgAndVid = document.getElementById('old_name_edit').querySelector('option[value="' + selectedItemText + '"]').dataset.desc.split('|');
document.getElementById('editListItem').querySelector('#new_name').value = selectedItemText;
document.getElementById('editListItem').querySelector('#desc').value = descAndImgAndVid[0];
document.getElementById('editListItem').querySelector('#img').value = descAndImgAndVid[1];
document.getElementById('editListItem').querySelector('#vid').value = descAndImgAndVid[2];
}
function fillAudioColorDetailsForDelete(action) {
var selectedItemText = document.getElementById('old_name_delete').value;
var descAndImgAndVid = document.getElementById('old_name_delete').querySelector('option[value="' + selectedItemText + '"]').dataset.desc.split('|');
document.getElementById('deleteFromList').querySelector('#old_name_delete').value = selectedItemText;
// document.getElementById('deleteFromList').querySelector('#desc').value = descAndImgAndVid[0]; // 填充描述
// document.getElementById('deleteFromList').querySelector('#img').value = descAndImgAndVid[1]; // 填充图片链接
// document.getElementById('deleteFromList').querySelector('#vid').value = descAndImgAndVid[2]; // 填充音色 ID
}
// 页面加载时,初始化修改和删除音色的音色列表
document.addEventListener('DOMContentLoaded', function() {
updateAudioColorsList('edit');
updateAudioColorsList('delete');
fillCategoryAdd();
var response = '{{ response }}'; // 获取 response 变量的值
if (response && response !== 'None') {
showResponseMessage(response);
}
let cacheValue = localStorage.getItem('curtabName');
if (cacheValue && cacheValue.length >1){
openTab(event, cacheValue)
}
});
function filterSelectOptions(action) {
var input = document.getElementById('search_input_' + action);
const select = document.getElementById('old_name_' + action);
var filter = input.value.toLocaleUpperCase('en-US');
var options = document.getElementById('old_name_' + action).getElementsByTagName('option');
var isINList = 0;
for (var i = 0; i < options.length; i++) {
var optionText = options[i].textContent;
if (optionText.toLocaleUpperCase('en-US').indexOf(filter) > -1) {
options[i].style.display = "";
isINList++;
} else {
options[i].style.display = "none";
}
}
// 设置下拉框大小
select.size = isINList > 0 ? Math.min(optionText.length, 5) : 1;
// 如果没有匹配项,清空选择
if (isINList === 0 && filter.length > 0) {
select.selectedIndex = -1; // 清空选择
}
}
// 为搜索框添加 onkeyup 事件
document.getElementById('search_input_edit').addEventListener('keyup', function() {
filterSelectOptions('edit');
});
document.getElementById('search_input_delete').addEventListener('keyup', function() {
filterSelectOptions('delete');
});
// 选择项点击事件
document.getElementById('dropdown_options_edit').addEventListener('click', function(event) {
var selectedItem = event.target;
if (selectedItem.classList.contains('dropdown-item')) {
document.getElementById('search_input_edit').value = selectedItem.textContent; // 设置输入框的值为选中的项
fillAudioColorDetails('edit');
}
});
document.getElementById('dropdown_options_delete').addEventListener('click', function(event) {
var selectedItem = event.target;
if (selectedItem.classList.contains('dropdown-item')) {
document.getElementById('search_input_delete').value = selectedItem.textContent; // 设置输入框的值为选中的项
fillAudioColorDetailsForDelete('delete');
}
});
function loginout() {
var currentUrl = window.location.origin + '/logout';
// 提示用户确认登出
var confirmation = confirm("您确定要登出吗?");
// 如果用户确认登出,则跳转到登出页面
if (confirmation) {
window.location.href = currentUrl;
}
// else {
// // 用户选择取消,可以选择在此处添加其他逻辑
// console.log("用户取消了登出操作");
// }
}
function getYsList() {
{#var domainName = '{{ domainName }}';#}
{#var mainUrl = domainName.length > 0 ? window.location.protocol+'//'+domainName+':'+window.location.port:window.location.origin;#}
var currentUrl = window.location.origin + '/ysList';
document.getElementById('urlText').textContent = currentUrl;
document.getElementById('urlModal').style.display = 'flex';
document.getElementById('copyButton').onclick = function() {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(currentUrl).then(() => {
alert('链接已复制到剪贴板!');
}).catch(err => {
alert('复制链接失败:' + err);
});
} else {
// 备选方案
const textArea = document.createElement('textarea');
textArea.value = currentUrl;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
alert('链接已复制到剪贴板!');
} catch (err) {
alert('复制链接失败:' + err);
}
document.body.removeChild(textArea);
}
};
document.getElementById('openButton').onclick = function() {
window.open(currentUrl, '_blank');
};
document.getElementById('cancelButton').onclick = function() {
document.getElementById('urlModal').style.display = 'none';
};
}
function validateForm(a,b) {
localStorage.setItem('curtabName', b);
if (a === 'old_name_delete'){
var select = document.getElementById(a);
if (select.selectedIndex === -1 || select.value === "") {
alert("未选中");
return false; // 阻止表单提交
}
return true; // 允许表单提交
}
}
</script>
</body>
</html>

View File

@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ titleName }}{{ PKC_VERSION }}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<style>
body {
margin: 0;
font-family: 'Arial', sans-serif;
background-color: #0c0c0c;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
}
.login-container {
background: rgba(20, 20, 20, 0.9);
border-radius: 10px;
padding: 2rem;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
width: 400px;
position: relative;
overflow: hidden;
}
.login-container h1 {
text-align: center;
margin-bottom: 1rem;
font-size: 2rem;
color: #00ccff;
}
.login-container input {
width: 100%;
padding: 1rem;
margin: 0.5rem 0;
border: none;
border-radius: 5px;
background: #222;
color: #ffffff;
font-size: 1rem;
transition: background 0.3s;
}
.login-container input:focus {
background: #333;
outline: none;
}
.login-container button {
width: 100%;
padding: 1rem;
background: #00ccff;
border: none;
border-radius: 5px;
font-size: 1.2rem;
color: #ffffff;
cursor: pointer;
transition: background 0.3s;
}
.login-container button:hover {
background: #0099cc;
}
.message {
color: #ff0000;
text-align: center;
margin-top: 1rem;
}
.background-animation {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background: linear-gradient(120deg, #00ccff, #ff00ff);
animation: gradient 6s ease infinite;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
</style>
</head>
<body>
<div class="background-animation"></div>
<div class="login-container">
<h1>{{ titleName }}</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="message">
{% for category, message in messages %}
{% if category != 'success' %}
<p class="{{ category }}">{{ message }}</p>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endwith %}
<form method="POST" action="/login">
<input type="text" id="username" name="username" placeholder="用户名" required>
<input type="password" id="password" name="password" placeholder="密码" required>
<button type="submit">登录</button>
</form>
</div>
</body>
</html>

28904
PKCYsManage/ys.json Normal file

File diff suppressed because it is too large Load Diff

103
README.md
View File

@ -1,2 +1,101 @@
# pkc-ys
PKC音色管理后台
# PKC音色管理后台
```
仅用于PKC音色管理可自定义音色接口地址
```
## 一、启动项目
## 1. 使用 Python 启动
首先,确保你已安装 Python 和相关依赖项。可以使用 `pip` 安装所需的库:
```bash
pip install -r requirements.txt
```
然后,使用以下命令启动应用:
```bash
# 进入主目录
cd PKCYsManage
# 启动项目
nohup python3 main.py &
# 查看日志
tail -f nohup.out
```
## 2. 使用 Docker 启动
确保你已安装 Docker。使用以下命令构建镜像并运行容器
```bash
docker run -d -p 39900:39900 -e PKC_USER=pkc -e PKC_PASSWORD=pkc --name pkc-ys curtinlv/pkc-ys
```
## 3. 使用 Docker Compose 启动
确保你已安装 Docker Compose。创建一个 `docker-compose.yml` 文件并填入以下内容:
```yaml
version: '3.3'
services:
pkc-ys:
image: curtinlv/pkc-ys
container_name: pkc-ys
ports:
- "39900:39900"
environment:
- PKC_TITLE=PKC音色管理系统 # 系统名称
- PKC_USER=pkc # 用户名
- PKC_PASSWORD=pkc # 密码,如需带特殊字符用.env引入
volumes:
- ./backup:/app/backup # 音色备份目录
- /etc/localtime:/etc/localtime:ro
restart: unless-stopped
```
然后,在包含 docker-compose.yml 的目录下运行以下命令启动服务:
```bash
docker-compose up -d
```
## 二、访问地址
音色管理后台访问地址:
```http request
http://ip:39900
```
音色列表接口地址:
```http request
http://ip:39900/ysList
```
## 三、常用 Docker 命令
以下是一些常用的 Docker 命令,用于管理容器和查看日志等:
- 查看运行中的容器:
```bash
docker ps
```
- 更新容器,或修改`docker-compose.yml`配置需执行生效:
```bash
docker up -d
```
- 查看所有容器(包括停止的):
```bash
docker ps -a
```
- 查看容器日志:
```bash
docker logs -f pkc-ys
```
- 重启容器:
```bash
docker restart pkc-ys
```
- 停止容器:
```bash
docker stop pkc-ys
```
- 启动已停止的容器:
```bash
docker start pkc-ys
```
- 删除容器:
```bash
docker rm pkc-ys
```

16
docker-compose.yml Normal file
View File

@ -0,0 +1,16 @@
version: '3.3'
services:
pkc-ys:
image: curtinlv/pkc-ys
container_name: pkc-ys
ports:
- "39900:39900"
environment:
- PKC_TITLE=PKC音色管理系统 # 系统名称
- PKC_USER=Curtin # 用户名
- PKC_PASSWORD=${PKC_PASSWORD} # 密码
volumes:
# - ./ys.json:/app/ys.json # 音色文件
- ./backup:/app/backup # 音色备份目录
- /etc/localtime:/etc/localtime:ro
restart: unless-stopped

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
Flask==2.3.2
Werkzeug==2.3.6