增加了markdown文件的上传和相关参数的修改

This commit is contained in:
chenyixian
2025-11-26 07:46:55 +08:00
parent a8a26316cb
commit b2e24af7b9
30 changed files with 36766 additions and 953 deletions

673
add_1114_to_minio_file.py Normal file
View File

@@ -0,0 +1,673 @@
# -*- coding: utf-8 -*-
import os
import re
import io
from minio import Minio
from minio.error import S3Error
def process_minio_and_local_objects(minio_client, bucket_name, base_path, season_dirs, local_base_path, date_suffix):
"""
处理MinIO和本地文件系统中的对象为图片文件添加日期后缀并更新markdown文件中的引用
:param date_suffix: 日期后缀,例如 '_1114'
"""
# 记录所有重命名映射关系 {旧对象键: 新对象键}
rename_mappings = {}
# 记录所有需要更新的markdown文件及其更新内容
minio_markdown_updates = {}
local_markdown_updates = {}
# 先收集所有markdown文件的内容构建索引
minio_markdown_index = build_minio_markdown_index(minio_client, bucket_name, base_path)
local_markdown_index = build_local_markdown_index(local_base_path)
# 构建本地图片文件索引
local_image_index = build_local_image_index(local_base_path)
for season_dir in season_dirs:
print(f"\n处理季节目录: {season_dir} {base_path}")
# 构造完整路径
full_path = f"{base_path}/{season_dir}/"
# 递归处理季节目录下的所有子目录和图片
season_rename_mappings = process_directory_recursive(
minio_client, bucket_name, full_path, season_dir, base_path,
minio_markdown_index, local_markdown_index, local_image_index,
minio_markdown_updates, local_markdown_updates, local_base_path, date_suffix
)
rename_mappings.update(season_rename_mappings)
# 显示所有更改并请求确认
if show_changes_and_confirm(rename_mappings, minio_markdown_updates, local_markdown_updates):
# 用户确认后执行所有更改
execute_changes(minio_client, bucket_name, rename_mappings, minio_markdown_updates, local_markdown_updates)
return rename_mappings
else:
print("\n操作已取消,未执行任何更改。")
return {}
def build_minio_markdown_index(minio_client, bucket_name, base_path):
"""
构建MinIO中markdown文件索引用于快速查找引用关系
"""
print("构建MinIO中markdown文件索引...")
markdown_index = {}
# 查找所有markdown文件
markdown_files = find_minio_markdown_files(minio_client, bucket_name, base_path)
for md_file in markdown_files:
try:
# 下载markdown文件内容
response = minio_client.get_object(bucket_name, md_file)
content = response.read().decode('utf-8')
response.close()
response.release_conn()
# 提取所有图片引用
image_refs = extract_image_references(content)
# 为每个引用的图片记录markdown文件
for image_ref in image_refs:
if image_ref not in markdown_index:
markdown_index[image_ref] = []
markdown_index[image_ref].append(md_file)
except S3Error as exc:
print(f"处理MinIO中markdown文件 {md_file} 时发生错误: {exc}")
print(f"MinIO索引构建完成共找到 {len(markdown_index)} 个图片引用")
return markdown_index
def build_local_markdown_index(local_base_path):
"""
构建本地markdown文件索引用于快速查找引用关系
"""
print("构建本地markdown文件索引...")
markdown_index = {}
# 查找所有markdown文件
markdown_files = find_local_markdown_files(local_base_path)
for md_file in markdown_files:
try:
# 读取markdown文件内容
with open(md_file, 'r', encoding='utf-8') as f:
content = f.read()
# 提取所有图片引用
image_refs = extract_image_references(content)
# 为每个引用的图片记录markdown文件
for image_ref in image_refs:
if image_ref not in markdown_index:
markdown_index[image_ref] = []
markdown_index[image_ref].append(md_file)
except Exception as exc:
print(f"处理本地markdown文件 {md_file} 时发生错误: {exc}")
print(f"本地索引构建完成,共找到 {len(markdown_index)} 个图片引用")
return markdown_index
def build_local_image_index(local_base_path):
"""
构建本地图片文件索引
"""
print("构建本地图片文件索引...")
image_index = {}
# 递归查找所有图片文件
for root, dirs, files in os.walk(local_base_path):
for file in files:
if is_image_file(file):
filename = os.path.basename(file)
if filename not in image_index:
image_index[filename] = []
image_index[filename].append(os.path.join(root, file))
print(f"本地图片索引构建完成,共找到 {len(image_index)} 个图片文件")
return image_index
def extract_image_references(content):
"""
从markdown内容中提取所有图片引用
"""
# 匹配markdown图片语法 ![](url)
pattern = r'!\[\]\(([^)]+)\)'
matches = re.findall(pattern, content)
# 提取图片文件名去掉URL前缀
image_refs = []
for match in matches:
# 从URL中提取文件名
filename = os.path.basename(match)
if filename:
image_refs.append(filename)
return image_refs
def process_directory_recursive(
minio_client, bucket_name, current_path, season_dir, base_path,
minio_markdown_index, local_markdown_index, local_image_index,
minio_markdown_updates, local_markdown_updates, local_base_path, date_suffix
):
"""
递归处理目录及其所有子目录
:param date_suffix: 日期后缀,例如 '_1114'
"""
rename_mappings = {}
try:
# 列出当前目录下的所有对象
objects = minio_client.list_objects(bucket_name, prefix=current_path, recursive=False)
for obj in objects:
object_name = obj.object_name
# 如果是目录,递归处理
if object_name.endswith('/'):
subdir_rename_mappings = process_directory_recursive(
minio_client, bucket_name, object_name, season_dir, base_path,
minio_markdown_index, local_markdown_index, local_image_index,
minio_markdown_updates, local_markdown_updates, local_base_path, date_suffix
)
rename_mappings.update(subdir_rename_mappings)
else:
# 处理文件
if is_image_file(object_name):
# 分离文件名和扩展名
filename = os.path.basename(object_name)
name_part, ext_part = os.path.splitext(filename)
# 检查是否已经包含类似的日期后缀格式_数字
date_pattern = r'_\d{4}$'
need_rename = False
new_filename = None # 初始化变量
if re.search(date_pattern, name_part):
# 已有日期后缀,替换为新的后缀
new_name_part = re.sub(date_pattern, date_suffix, name_part)
new_filename = f"{new_name_part}{ext_part}"
need_rename = True
elif not name_part.endswith(date_suffix):
# 没有日期后缀,添加新后缀
new_filename = f"{name_part}{date_suffix}{ext_part}"
need_rename = True
if need_rename and new_filename:
# 生成新的文件名(添加或替换日期后缀)
# 构造新的完整对象路径
dir_path = os.path.dirname(object_name)
new_object_name = f"{dir_path}/{new_filename}" if dir_path else new_filename
# 查找引用此图片的markdown文件
minio_affected_markdowns = minio_markdown_index.get(filename, [])
local_affected_markdowns = local_markdown_index.get(filename, [])
# 查找本地对应的图片文件
local_affected_images = local_image_index.get(filename, [])
# 显示修改提示并请求确认
rel_path = object_name.replace(f"{base_path}/{season_dir}/", "")
new_rel_path = new_object_name.replace(f"{base_path}/{season_dir}/", "")
# 移除缩进,这段代码已经在 if need_rename 块内
print(f"\n{'=' * 80}")
print(f"发现需要重命名: {rel_path} -> {new_rel_path}")
print(f"{'=' * 80}")
# 显示MinIO中受影响的markdown文件及其具体修改内容
if minio_affected_markdowns:
print(f"MinIO中此图片被以下 {len(minio_affected_markdowns)} 个markdown文件引用:")
for md_file in minio_affected_markdowns:
md_filename = os.path.basename(md_file)
print(f" - {md_filename}:")
# 获取并显示具体修改内容
show_markdown_changes(minio_client, bucket_name, md_file, filename, new_filename,
"MinIO")
# 显示本地受影响的markdown文件及其具体修改内容
if local_affected_markdowns:
print(f"本地此图片被以下 {len(local_affected_markdowns)} 个markdown文件引用:")
for md_file in local_affected_markdowns:
md_filename = os.path.basename(md_file)
print(f" - {md_filename}:")
# 获取并显示具体修改内容
show_markdown_changes_local(md_file, filename, new_filename, "本地")
# 显示本地受影响的图片文件
if local_affected_images:
print(f"本地有以下 {len(local_affected_images)} 个同名图片文件需要重命名:")
for img_file in local_affected_images:
rel_img_path = os.path.relpath(img_file, local_base_path)
new_img_file = os.path.join(os.path.dirname(img_file), new_filename)
rel_new_img_path = os.path.relpath(new_img_file, local_base_path)
print(f" - {rel_img_path} -> {rel_new_img_path}")
# 确认修改
if confirm_single_change("MinIO和本地文件修改"):
# 记录重命名映射
rename_mappings[object_name] = new_object_name
# 记录MinIO markdown更新
for md_file in minio_affected_markdowns:
if md_file not in minio_markdown_updates:
minio_markdown_updates[md_file] = {
'old_filename': filename,
'new_filename': new_filename
}
# 记录本地markdown更新
for md_file in local_affected_markdowns:
if md_file not in local_markdown_updates:
local_markdown_updates[md_file] = {
'type': 'markdown',
'old_filename': filename,
'new_filename': new_filename
}
# 记录本地图片重命名(使用单独的字典或标记类型)
for img_file in local_affected_images:
new_img_file = os.path.join(os.path.dirname(img_file), new_filename)
if img_file not in local_markdown_updates:
local_markdown_updates[img_file] = {
'type': 'image',
'old_filename': filename,
'new_filename': new_filename,
'new_path': new_img_file
}
print(f" ✓ 已确认修改: {rel_path} -> {new_rel_path}")
# 立即执行此单元的修改
execute_single_unit_changes(
minio_client, bucket_name, object_name, new_object_name,
minio_affected_markdowns, local_affected_markdowns,
local_affected_images, filename, new_filename
)
else:
print(f" ✗ 已取消修改: {rel_path}")
else:
# 打印已处理的文件(已经是目标后缀)
rel_path = object_name.replace(f"{base_path}/{season_dir}/", "")
print(f" 已是目标后缀{date_suffix}: {rel_path}")
except S3Error as exc:
print(f"处理目录 {current_path} 时发生错误: {exc}")
return rename_mappings
def show_markdown_changes(minio_client, bucket_name, md_file, old_filename, new_filename, source):
"""
显示markdown文件中的具体修改内容
"""
try:
# 下载markdown文件内容
response = minio_client.get_object(bucket_name, md_file)
content = response.read().decode('utf-8')
response.close()
response.release_conn()
# 查找包含旧文件名的行
lines = content.split('\n')
for i, line in enumerate(lines):
if old_filename in line:
# 显示修改前后的内容
old_line = line
new_line = line.replace(old_filename, new_filename)
print(f"{i + 1}:")
print(f" 原内容: {old_line}")
print(f" 新内容: {new_line}")
except S3Error as exc:
print(f" 获取{source}markdown文件内容时发生错误: {exc}")
def show_markdown_changes_local(md_file, old_filename, new_filename, source):
"""
显示本地markdown文件中的具体修改内容
"""
try:
# 读取markdown文件内容
with open(md_file, 'r', encoding='utf-8') as f:
content = f.read()
# 查找包含旧文件名的行
lines = content.split('\n')
for i, line in enumerate(lines):
if old_filename in line:
# 显示修改前后的内容
old_line = line
new_line = line.replace(old_filename, new_filename)
print(f"{i + 1}:")
print(f" 原内容: {old_line}")
print(f" 新内容: {new_line}")
except Exception as exc:
print(f" 获取{source}markdown文件内容时发生错误: {exc}")
def execute_single_unit_changes(
minio_client, bucket_name, old_object_name, new_object_name,
minio_affected_markdowns, local_affected_markdowns,
local_affected_images, old_filename, new_filename
):
"""
执行单个单元的修改
"""
try:
# 1. 重命名MinIO中的图片文件
# 使用正确的CopySource格式
copy_source = {"bucket": bucket_name, "object": old_object_name}
# 尝试使用新的API调用方式
try:
# 方法1: 使用copy_object方法
minio_client.copy_object(bucket_name, new_object_name, copy_source)
print(
f" ✓ MinIO图片重命名完成: {os.path.basename(old_object_name)} -> {os.path.basename(new_object_name)}")
except Exception as copy_exc:
# 如果方法1失败尝试方法2: 使用get_object和put_object组合
try:
print(f" 尝试备用方法重命名MinIO图片...")
# 先下载对象
response = minio_client.get_object(bucket_name, old_object_name)
object_data = response.read()
response.close()
response.release_conn()
# 再上传为新对象需要包装成BytesIO
from io import BytesIO
minio_client.put_object(
bucket_name,
new_object_name,
io.BytesIO(object_data), # 包装成BytesIO对象
len(object_data)
)
print(
f" ✓ MinIO图片重命名完成(备用方法): {os.path.basename(old_object_name)} -> {os.path.basename(new_object_name)}")
except Exception as alt_exc:
print(f" ✗ MinIO图片重命名失败 {old_object_name}: {alt_exc}")
# 如果两种方法都失败,跳过这个文件的后续操作
return
# 删除原对象
try:
minio_client.remove_object(bucket_name, old_object_name)
except S3Error as rm_exc:
print(f" ✗ 删除MinIO原对象失败 {old_object_name}: {rm_exc}")
# 2. 更新MinIO中的markdown文件
for md_file in minio_affected_markdowns:
try:
print(f" → 正在处理MinIO markdown: {os.path.basename(md_file)}")
# 下载markdown文件内容
response = minio_client.get_object(bucket_name, md_file)
content = response.read().decode('utf-8')
response.close()
response.release_conn()
# 显示更新前的内容片段(调试用)
print(f" 原内容包含旧文件名: {old_filename in content}")
# 替换所有匹配的内容
new_content = content.replace(old_filename, new_filename)
# 验证替换是否生效
if new_content == content:
print(f" ⚠ 警告: 内容未发生变化,可能未找到匹配项")
else:
print(f" 内容已替换: {old_filename}{new_filename}")
# 上传修改后的内容
content_bytes = new_content.encode('utf-8')
# 先删除旧文件某些MinIO版本需要先删除
try:
minio_client.remove_object(bucket_name, md_file)
print(f" 已删除旧markdown文件")
except Exception as rm_exc:
print(f" 删除旧markdown文件时出错继续执行: {rm_exc}")
# 重新上传
minio_client.put_object(
bucket_name,
md_file,
io.BytesIO(content_bytes),
length=len(content_bytes),
content_type='text/plain; charset=utf-8'
)
# 验证上传结果
verify_response = minio_client.get_object(bucket_name, md_file)
verify_content = verify_response.read().decode('utf-8')
verify_response.close()
verify_response.release_conn()
if new_filename in verify_content:
print(f" ✓ MinIO markdown更新完成并验证成功: {os.path.basename(md_file)}")
else:
print(f" ✗ MinIO markdown验证失败: 更新后仍未包含新文件名")
except S3Error as exc:
print(f" ✗ MinIO markdown更新失败 {md_file}: {exc}")
except Exception as exc:
print(f" ✗ MinIO markdown更新失败未知错误 {md_file}: {exc}")
# 3. 重命名本地图片文件
for img_file in local_affected_images:
try:
new_img_file = os.path.join(os.path.dirname(img_file), new_filename)
os.rename(img_file, new_img_file)
print(f" ✓ 本地图片重命名完成: {os.path.basename(img_file)} -> {os.path.basename(new_filename)}")
except Exception as exc:
print(f" ✗ 本地图片重命名失败 {img_file}: {exc}")
# 4. 更新本地markdown文件
for md_file in local_affected_markdowns:
try:
# 读取markdown文件内容
with open(md_file, 'r', encoding='utf-8') as f:
content = f.read()
# 替换所有匹配的内容
content = content.replace(old_filename, new_filename)
# 写入修改后的内容
with open(md_file, 'w', encoding='utf-8') as f:
f.write(content)
print(f" ✓ 本地markdown更新完成: {os.path.basename(md_file)}")
except Exception as exc:
print(f" ✗ 本地markdown更新失败 {md_file}: {exc}")
except Exception as exc:
print(f" ✗ 执行单元修改时发生错误: {exc}")
def find_minio_markdown_files(minio_client, bucket_name, base_path):
"""
查找MinIO中所有markdown文件
"""
markdown_files = []
try:
# 递归查找所有markdown文件
objects = minio_client.list_objects(bucket_name, prefix=base_path, recursive=True)
for obj in objects:
object_name = obj.object_name
if object_name.endswith('_md.txt') or object_name.endswith('_md.md'):
markdown_files.append(object_name)
except S3Error as exc:
print(f"查找MinIO中markdown文件时发生错误: {exc}")
print(f"找到 {len(markdown_files)} 个MinIO中的markdown文件")
return markdown_files
def find_local_markdown_files(local_base_path):
"""
查找本地所有markdown文件
"""
markdown_files = []
try:
# 递归查找所有markdown文件
for root, dirs, files in os.walk(local_base_path):
for file in files:
if file.endswith('_md.txt') or file.endswith('_md.md'):
markdown_files.append(os.path.join(root, file))
except Exception as exc:
print(f"查找本地markdown文件时发生错误: {exc}")
print(f"找到 {len(markdown_files)} 个本地markdown文件")
return markdown_files
def confirm_single_change(change_type):
"""
确认单个更改
"""
while True:
response = input(f"是否确认此{change_type}? (y/n): ").strip().lower()
if response in ['y', 'yes']:
return True
elif response in ['n', 'no']:
return False
else:
print("请输入 'y''n'")
def show_changes_and_confirm(rename_mappings, minio_markdown_updates, local_markdown_updates):
"""
显示所有更改并请求用户确认
"""
print("\n" + "=" * 80)
print("更改摘要:")
print("=" * 80)
# 显示图片重命名摘要
print(f"\n1. MinIO图片文件重命名 ({len(rename_mappings)} 个文件):")
for i, (old_path, new_path) in enumerate(list(rename_mappings.items())):
print(f" {i + 1}. {os.path.basename(old_path)} -> {os.path.basename(new_path)}")
# 显示MinIO markdown更新摘要
print(f"\n2. MinIO Markdown文件更新 ({len(minio_markdown_updates)} 个文件):")
for i, (md_path, update_info) in enumerate(list(minio_markdown_updates.items())):
print(
f" {i + 1}. {os.path.basename(md_path)}: {update_info['old_filename']} -> {update_info['new_filename']}")
# 显示本地markdown更新摘要
print(f"\n3. 本地Markdown文件更新 ({len(local_markdown_updates)} 个文件):")
for i, (md_path, update_info) in enumerate(list(local_markdown_updates.items())):
if update_info.get('type') == 'markdown':
print(
f" {i + 1}. {os.path.basename(md_path)}: {update_info['old_filename']} -> {update_info['new_filename']}")
elif update_info.get('type') == 'image':
# 这是本地图片文件
print(f" {i + 1}. {os.path.basename(md_path)} -> {update_info['new_filename']}: 图片重命名")
else:
# 兼容旧格式
if md_path.endswith('.txt') or md_path.endswith('.md'):
print(
f" {i + 1}. {os.path.basename(md_path)}: {update_info['old_filename']} -> {update_info['new_filename']}")
else:
print(f" {i + 1}. {os.path.basename(md_path)}: 图片重命名")
# 请求用户确认
print("\n" + "=" * 80)
print(
f"总计: {len(rename_mappings)} 个MinIO图片文件将被重命名, {len(minio_markdown_updates)} 个MinIO markdown文件将被更新, {len(local_markdown_updates)} 个本地文件将被更新")
print("=" * 80)
while True:
response = input("\n是否执行这些更改? (y/n): ").strip().lower()
if response in ['y', 'yes']:
return True
elif response in ['n', 'no']:
return False
else:
print("请输入 'y''n'")
def execute_changes(minio_client, bucket_name, rename_mappings, minio_markdown_updates, local_markdown_updates):
"""
执行所有更改(这里实际上已经执行过了,只是显示完成信息)
"""
print("\n所有更改已完成!")
def is_image_file(filename):
"""
检查文件是否是图片
"""
image_extensions = ['.jpg', '.JPG', '.jpeg', '.JPEG', '.png', '.PNG', '.gif', '.GIF', '.bmp', '.BMP']
return any(filename.lower().endswith(ext) for ext in image_extensions)
def main():
"""
主函数
"""
# 获取日期后缀参数
import sys
if len(sys.argv) > 1:
date_suffix = sys.argv[1]
if not date_suffix.startswith('_'):
date_suffix = '_' + date_suffix
else:
date_suffix = '_1114' # 默认值
print(f"使用日期后缀: {date_suffix}")
print("提示: 可以通过命令行参数修改后缀,例如: python script.py _1203")
print("=" * 80)
# 初始化MinIO客户端
minio_client = Minio(
"1.13.185.116:9000",
access_key="rag_flow", # 替换为您的access key
secret_key="infini_rag_flow", # 替换为您的secret key
secure=False
)
# 配置参数
bucket_name = "exhibit-photo" # 替换为您的bucket名称
base_path = "bj_yuanlin/category" # MinIO中的基础路径
season_dirs = ["chun_pic_md", "xia_pic_md", "qiu_pic_md", "dong_pic_md"] # 季节目录
local_base_path = "." # 本地基础路径,可以根据需要修改
print("\n开始扫描MinIO和本地文件系统...")
print(f"MinIO基础路径: {base_path}")
print(f"季节目录: {', '.join(season_dirs)}")
print(f"本地基础路径: {local_base_path}")
print(f"日期后缀: {date_suffix}")
try:
rename_mappings = process_minio_and_local_objects(
minio_client, bucket_name, base_path, season_dirs, local_base_path, date_suffix
)
if rename_mappings:
print(f"\n处理完成!共重命名 {len(rename_mappings)} 个MinIO图片文件")
else:
print(f"\n未执行任何更改")
except Exception as e:
print(f"处理过程中出现错误: {e}")
if __name__ == "__main__":
main()

File diff suppressed because one or more lines are too long

124
demo/Convert_Pinyin.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
demo/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

1106
demo/js/app.js Normal file

File diff suppressed because one or more lines are too long

16699
demo/js/chunk-vendors.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="/favicon.ico">
<title>museum-admin</title>
<script src="Convert_Pinyin.js"></script>
<script defer src="./js/chunk-vendors.js"></script>
<script defer src="./js/app.js"></script></head>
<body>
<noscript>
<strong>We're sorry but museum-admin doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app" style="margin:0;padding:0;overflow-y: hidden;overflow-x:hidden"></div>
<!-- built files will be auto injected -->
</body>
</html>

File diff suppressed because one or more lines are too long

11765
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@
},
"dependencies": {
"core-js": "^3.8.3",
"element-plus": "^2.10.2",
"vue": "^3.2.13"
},
"devDependencies": {

100
src/API/api.js Normal file
View File

@@ -0,0 +1,100 @@
export const BASE_API_URL= "http://1.13.185.116:9380/api/v1";
export const TEST_API_URL= "http://1.13.185.116:9580";
export const WEIXIN_WS_URL= "wss://ragflow.szzysztech.com/ws_tts"
// eslint-disable-next-line no-unused-vars
export const chatId="9cb04ecead5111ef99e80242ac120006";
// eslint-disable-next-line no-unused-vars
export const DEVICE_API_URL = "http://1.13.185.116:9480/monitor"
// eslint-disable-next-line no-unused-vars
export const exhibit_tts_bucket_name = "exhibit-tts"
// eslint-disable-next-line no-unused-vars
export const exhibit_photo_bucket_name="exhibit-photo"
// eslint-disable-next-line no-unused-vars
export const exhibit_md_bucket_name="exhibit-photo"
// eslint-disable-next-line no-unused-vars
export async function chatWithAI(messages) {
const response = await fetch(`${TEST_API_URL}/chat/completion`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': 'your_app_secret_key' // 如果启用了API密钥验证
},
body: JSON.stringify({
messages: messages,
temperature: 0.8
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(`API error: ${error.detail}`);
}
return response.json();
}
export async function api_getMesumList(){
const response = await fetch(`${BASE_API_URL}/mesum/list`, {
method: 'GET',
headers: {'Content-Type': 'application/json',
'Authorization': 'Bearer ragflow-NhZTY5Y2M4YWQ1MzExZWY4Zjc3MDI0Mm'}
})
const result = await response.json()
const {data} = result
return data;
}
export async function api_getMesumAntiques(museumId)
{
const response = await fetch(`${BASE_API_URL}/mesum/antique/${museumId}`, {
method: 'GET',
headers: {'Content-Type': 'application/json'}
})
const result = await response.json()
const {data} = result
return data
}
export async function api_getAntiqueDetail(museum_id,antique_id)
{
const response = await fetch(`${BASE_API_URL}/mesum/antique_detail/${museum_id}/${antique_id}`, {
method: 'GET',
headers: {'Content-Type': 'application/json',
'Authorization': 'Bearer ragflow-NhZTY5Y2M4YWQ1MzExZWY4Zjc3MDI0Mm'}
})
const result = await response.json()
const {data} = result
return data
}
export async function account_login({user,password}){
const response = await fetch(`${TEST_API_URL}/system_admin/login`, {
method: 'POST',
headers: {'Content-Type': 'application/json',
'Authorization': 'Bearer ragflow-NhZTY5Y2M4YWQ1MzExZWY4Zjc3MDI0Mm'},
body:JSON.stringify({user:user,password:password})
})
const result = await response.json()
const {data} = result
return data;
}
export async function api_getSubscriptions(museumId){
let url = `${TEST_API_URL}/system_admin/get_subscriptions`
if(museumId) url = url + `?museum_id=${museumId}`
const response = await fetch(url, {
method: 'GET',
headers: {'Content-Type': 'application/json',
'Authorization': 'Bearer ragflow-NhZTY5Y2M4YWQ1MzExZWY4Zjc3MDI0Mm'}
})
const result = await response.json()
const {data} = result
return data;
}

View File

@@ -1,15 +1,120 @@
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
<div id="mainContainer" class="container">
<navi-menu v-if="isAuthed" :menu_authed="menu_authed" @navi-menu-select="naviMenuSelect"
:defaultIndex="defaultActive">
</navi-menu>
<exhibit-detail v-if="activeMenuIndex==='1' && isAuthed && hasDataAdminMenu"
:museumListData="museumListData" :antiqueListData="museumAntiqueListData">
</exhibit-detail>
<device-monitor v-if="activeMenuIndex==='2' && isAuthed && hasDeviceMenu">
</device-monitor>
<location-manager v-if="activeMenuIndex==='3' && isAuthed && hasLocationMenu">
</location-manager>
<museum-subscriptions v-if="activeMenuIndex==='4' && isAuthed && hasSubscriptionsMenu"
:museum_authed="museum_authed" >
</museum-subscriptions>
<user-login v-if="!isAuthed" :loginDialogVisible="!isAuthed" @confirm-login="confirmLogin">
</user-login>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
import NaviMenu from "@/components/NaviMenu";
import ExhibitDetail from "@/components/ExhibitDetail";
import DeviceMonitor from "@/components/DeviceMonitor";
import LocationManager from "@/components/LocationManager";
import UserLogin from "@/components/UserLogin";
import MuseumSubscriptions from "@/components/MuseumSubscriptions"
export default {
name: 'App',
components: {
HelloWorld
NaviMenu,
ExhibitDetail,
DeviceMonitor,
LocationManager,
UserLogin,
MuseumSubscriptions
},
data() {
return {
museumListData:[],
museumAntiqueListData:[],
activeMenuIndex:'1',
isAuthed:false,
menu_authed:[],
museum_authed:[],
defaultActive:'1'
};
},
created() {
},
async mounted() {
},
methods:{
// eslint-disable-next-line no-unused-vars
naviMenuSelect(key){
this.activeMenuIndex = key;
console.log("app vue navi menu",key)
},
confirmLogin(response){
this.isAuthed = true; //已经授权
// eslint-disable-next-line no-prototype-builtins
if(response.hasOwnProperty('menu_authed') && Array.isArray(response.menu_authed))
this.menu_authed = response.menu_authed
if(this.menu_authed.includes('data_admin')){
this.defaultActive = '1';
this.activeMenuIndex = '1';
}
else if(this.menu_authed.length>0 && this.menu_authed[0] == 'get_subscriptions'){
this.defaultActive = '4';
this.activeMenuIndex = '4';
}
else if(this.menu_authed.length>0 && this.menu_authed[0] == 'location_admin'){
this.defaultActive = '3';
this.activeMenuIndex = '3';
}
// eslint-disable-next-line no-prototype-builtins
if(response.hasOwnProperty('museum_authed') && Array.isArray(response.museum_authed))
this.museum_authed = response.museum_authed
console.log("App.vue confirm-login",response,this.menu_authed,this.museum_authed)
}
},
computed:{
hasSubscriptionsMenu(){
if(this.menu_authed && this.menu_authed.includes('get_subscriptions'))
return true;
else
return false
},
hasDataAdminMenu(){
if(this.menu_authed && this.menu_authed.includes('data_admin'))
return true;
else
return false
},
hasLocationMenu(){
if(this.menu_authed && this.menu_authed.includes('location_admin'))
return true;
else
return false
},
hasDeviceMenu(){
if(this.menu_authed && this.menu_authed.includes('device_admin'))
return true;
else
return false
}
}
}
</script>
@@ -21,6 +126,18 @@ export default {
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
margin:0;
padding: 0;
overflow: hidden;
}
.container {
width: 98vw;height: 97vh; margin: 0px auto; padding: 0px;
display: flex;
flex-direction: row;
overflow-x: hidden;
overflow-y: hidden;
box-sizing: border-box;
}
</style>

View File

View File

@@ -0,0 +1,222 @@
<template>
<el-dialog
:title="formData.formTitle"
v-model="dialogVisible"
width="60%"
align-center
:before-close="handleDialogClose"
>
<div style="width: 58vw;"
v-loading="aiRefining"
element-loading-text="Ai 整理中...">
<div style="width:58vw;height:5vh;display: flex;flex-direction: row;
justify-content: space-between;align-items: center;margin-bottom: 2vh">
<el-select v-model="aiRefineDetailMaxWords" clearable placeholder="请选择最大字数" style="width: 140px">
<el-option
v-for="item in detialMaxWordsOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
<span>{{`整理后${formData.aiRefinedAntiqueDetialText.length}`}}</span>
<span>{{formData.label}}</span>
<el-button style="margin-right: 50px" type="primary" @click="refineAntiqueDetailText">开始</el-button>
</div>
<el-input
type="textarea"
:rows="8"
placeholder="支持多行输入"
v-model="formData.aiRefinedAntiqueDetialText"
style="width: 56vw"
></el-input>
<div style="width:58vw;height:5vh;display: flex;flex-direction: row;
justify-content: space-between;align-items: center;margin-top: 3vh">
<el-button style="margin-left: 50px" @click="closeDialog">取消修改</el-button>
<el-button style="margin-right: 50px" type="primary" @click="handleConfirmCommitAiRefinedText">确定修改</el-button>
</div>
<div style="margin-top: 10px;margin-bottom: 10px;color: blue" >
{{`原始文本,共${formData.aiOriginAntiqueDetailText.length }`}}
</div>
<div style="width:98%;height:25vh;max-height:25vh;overflow-y: auto;border: 1px solid lightgray;">
{{formData.aiOriginAntiqueDetailText}}
</div>
</div>
</el-dialog>
</template>
<script>
// eslint-disable-next-line no-unused-vars
import {BASE_API_URL,TEST_API_URL,chatWithAI} from "@/API/api";
export default {
name: "AiTextEditor",
props: {'mesumSelectedId':{default:-1}, 'mesumSelected':{},'initData':{},'visible':{default:false},
'photo_prefix':{default:'temp'}},
data() {
return {
formData: {
action:"update",
mesum_id:1,
id:"",
text: '',
label:"",
category:"",
formTitle:"修改讲解点",
aiRefinedAntiqueDetialText:"",
},
aiRefining:false,
dialogVisible:false,
aiRefineDetailMaxWords:400, //500字的音频将近2分钟所以将文字控制在400字音频1:40秒
detialMaxWordsOptions:[{label:'限制300字',value:300},{label:'限制400字',value:400},{label:'限制500字',value:500},{label:'限制600字',value:600}],
};
},
created() {
},
mounted() {
},
methods: {
refineAntiqueDetailText(){
this.aiRefining = true;
const conversation = [
{ role: "system", content: `你是一名展品或者文物讲解专家,请将用户的提供的解说词,整理一下,`+
`字数超过${this.aiRefineDetailMaxWords-30},但少于${this.aiRefineDetailMaxWords},输出内容不要分段` },
{ role: "user", content: `${this.formData.aiOriginAntiqueDetailText}` }
];
chatWithAI(conversation)
.then(response => {
console.log("AI回复:", response.message.content);
this.aiRefining = false;
this.formData.aiRefinedAntiqueDetialText = response.message.content
this.formData.text = response.message.content
})
.catch(error => {
console.error("请求失败:", error.message);
this.aiRefining = false;
});
},
handleConfirmCommitAiRefinedText(){
this.submitUpdateAntique(this.aiRefineForm)
this.closeDialog()
},
async submitUpdateAntique(new_data){
let antique_data = {
mesum_id:this.mesumSelectedId,
//根据传入参数中包含的key在下面程序动态设置
//label:this.formData.label,
//combined:this.formData.text,
//category:this.formData.category
}
// eslint-disable-next-line no-prototype-builtins
if(new_data.hasOwnProperty('label'))
antique_data['label'] = new_data['label']
// eslint-disable-next-line no-prototype-builtins
if(new_data.hasOwnProperty('text'))
antique_data['combined'] = new_data['text']
// eslint-disable-next-line no-prototype-builtins
if(new_data.hasOwnProperty('category'))
antique_data['category'] = new_data['category']
this.$confirm('确认更改?')
.then( async () => {
const response = await fetch(`${BASE_API_URL}/mesum/antique/update/${this.mesumSelectedId}/${new_data.id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ragflow-NhZTY5Y2M4YWQ1MzExZWY4Zjc3MDI0Mm'
},
body: JSON.stringify(antique_data)
})
const result = await response.json()
const {data} = result
if (data && data.update){
this.triggerRefreshAntiques();
this.$message('更新成功');
this.closeDialog()
}
console.log("update antique", data)
}).catch(() =>{
})
},
handleDialogClose(done){
/*this.$confirm('确认关闭?')
.then(_ => {
done();
})
.catch(_ => {});
*/
done();
this.closeDialog()
},
closeDialog(){
this.dialogVisible = false
this.$emit("update:close",true)
},
triggerRefreshAntiques(){
// 传递展品ID用于单独刷新该展品
this.$emit("update:refresh", this.formData.id)
}
},
watch:{
visible: {
handler(newVal) {
this.dialogVisible = newVal
if(newVal){
this.formData= {...this.formData,...this.initData}
this.formData.aiRefinedAntiqueDetialText = "";
this.formData.text = ""
}
console.log("watch visible",newVal,this.initData)
},
immediate: true
}
},
computed: {
}
}
</script>
<style scoped>
.dialog-footer {
display: flex;
justify-content: flex-end;
}
.el_dialog--header{
height:30px;
padding: 0px;
}
.el-dialog__header {
background: #f0f9eb !important;
padding: 2px 2px !important;
border-bottom: 1px solid #ebeef5;
}
.el-dialog__title {
margin: 0;
padding: 0;
color: #67C23A !important;
font-size: 16px !important;
}
.el-dialog__headerbtn {
margin-top: 3px !important;
top: 2px !important; /* 根据需求调整数值 */
}
.form-group { padding: 10px; margin-bottom: 5px;border-bottom: 2px solid #eee; }
label { display: block; margin-bottom: 5px; }
input, select, textarea { width: 100%; padding: 8px; }
button { padding: 10px 20px; margin:5px; cursor: pointer; }
.audio-player { margin: 20px 0; height:40px}
.flex-row { display: flex; gap: 15px; align-items: center; }
.error-message { color: red; margin-top: 10px; }
.success-message { color: green; margin-top: 10px; }
.upload-section { margin-top: 20px; border-top: 2px solid #eee; padding-top: 20px;height: 10vh }
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,251 @@
<template>
<div class="content-container">
<div style="width: 20vw;display: flex;justify-content: center" >设备日志管理</div>
<el-row class="device-log-container">
<el-col :span="8" >
<div class="device-list-container">
<div style="display: flex;flex-direction: row">
<el-select v-model="mesumSelectedId" clearable placeholder="请选择博物馆" @change="mesumSelectedChange"
default-first-option style="width: 160px">
<el-option
v-for="item in mesumOptions"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
<el-button @click="refreshDeviceList" size="default">刷新设备列表</el-button>
</div>
<div class="device-list-device">
<div v-for="(device,index) in currentDeviceList" :key="index"
:class="deviceIsSelected(device.device_id)? 'device-item device-item-selected':'device-item'" @click="deviceClick(device)">
{{device.device_id}}
</div>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="device-list-container">
<div style="display: flex;flex-direction: row;justify-content: flex-start;align-items: center">
<el-button :disabled="true">log文件列表</el-button>
<span>{{deviceSelected?.device_sn}}</span>
</div>
<div class="device-log-list">
<div v-for="(item,index) in currentDeviceLogList" :key="index" class="log-file-item">
<div style="margin-top: 5px">{{item}} </div>
<el-button size="default" style="width: 10vw;margin-bottom: 5px" @click="downloadLogFile(item)">下载</el-button>
</div>
</div>
</div>
</el-col>
<el-col :span="10">
</el-col>
</el-row>
</div>
</template>
<script>
import {DEVICE_API_URL,api_getMesumList} from "@/API/api"
export default {
name: "DeviceMonitor",
data() {
return {
mesumSelectedId:-1,
mesumOptions:[],
currentDeviceList:[],
deviceSelected:null,
currentDeviceLogList:[],
};
},
created() {
},
mounted() {
this.getMesumList()
},
methods: {
// eslint-disable-next-line no-unused-vars
async refreshDeviceList(){
const response = await this.getDeviceList(this.mesumSelectedId)
const {devices} = response
this.currentDeviceList = []
this.currentDeviceLogList.splice(0,this.currentDeviceLogList.length) //清空log文件列表
// eslint-disable-next-line no-unused-vars
//const device_ids = devices.map((item)=>{return item.device_id})
this.currentDeviceList.splice(0,0,...devices)
},
async getDeviceList(museum_id){
const response = await fetch(`${DEVICE_API_URL}/device_list?museum_id=${museum_id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ragflow-NhZTY5Y2M4YWQ1MzExZWY4Zjc3MDI0Mm'
},
})
const result = await response.json()
return result;
},
async deviceClick(device){
this.currentDeviceLogList.splice(0,this.currentDeviceLogList.length)
this.deviceSelected = device;
const response = await fetch(`${DEVICE_API_URL}/log_list?device_id=${device.device_id}`, {
method: 'GET',
headers: {'Content-Type': 'application/json'},
})
const result = await response.json()
const {success,files} = result
if(success){
this.currentDeviceLogList.splice(0,this.currentDeviceLogList.length)
this.currentDeviceLogList.splice(0,0,...files)
console.log(this.currentDeviceLogList)
}
},
async downloadLogFile(fileName){
const deviceId=this.deviceSelected.device_id ;
try {
// 构造请求URL
const url = new URL(`${DEVICE_API_URL}/get_log_file`);
url.searchParams.set('device_id', deviceId);
url.searchParams.set('file_name', fileName);
// 发起GET请求
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
// 处理响应
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || '下载失败');
}
// 创建可下载的Blob
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
// 创建隐藏的<a>标签触发下载
const a = document.createElement('a');
a.href = downloadUrl;
a.download = deviceId + '_'+fileName;
document.body.appendChild(a);
a.click();
// 清理资源
window.URL.revokeObjectURL(downloadUrl);
document.body.removeChild(a);
} catch (error) {
console.error('文件下载失败:', error);
// 显示错误提示
alert(`文件下载失败: ${error.message}`);
}
},
// eslint-disable-next-line no-unused-vars
async mesumSelectedChange(value){
},
async getMesumList(){
const data = await api_getMesumList()
if(data && Array.isArray(data) && data.length>=1){
this.mesumOptions = []
this.mesumOptions = data
this.mesumSelected = data[0]
this.mesumSelectedId = data[0].id
}
console.log(data)
},
},
watch:{
},
computed: {
deviceIsSelected(){
return (item)=>{
return item === this.deviceSelected?.device_id
}
},
}
}
</script>
<style scoped>
.content-container{
width: 88vw;
height: 97vh;
box-sizing: border-box;
overflow-y: hidden;
}
.device-log-container{
width: 85vw;
height: 86vh;
}
.device-list-container{
height: 86vh;
width: 96%;
border-right: 1px solid lightgray;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
}
.device-list-device{
overflow-y: auto;
width: 94%;
border: 1px solid lightblue;
height:81vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
}
.device-item{
border: 1px solid lightcoral;
border-radius: 5px;
margin-top: 5px;
height:3vh;
width: 90%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.device-item-selected{
background: lightcyan;
}
.device-log-list{
overflow-y: auto;
width: 94%;
border: 1px solid lightblue;
height:81vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
}
.log-file-item{
height: 10vh;
width: 80%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
border-radius: 6px;
border: 1px solid lightgray;
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,231 @@
<template>
<view class="audio-wave-container" :style="containerStyle">
<view class="audio-wave" :style="[waveStyle]">
<view
v-for="(bar, index) in bars"
:key="index"
class="wave-bar"
:style="[getBarStyle(bar, index)]"
></view>
</view>
</view>
</template>
<script>
export default {
name: 'DynamicAudioWave',
props: {
// 容器样式
containerWidth: {
type: String,
default: '300rpx'
},
containerHeight: {
type: String,
default: '80rpx'
},
// 声浪条配置
barCount: {
type: Number,
default: 10
},
barWidth: {
type: String,
default: '6rpx'
},
barSpacing: {
type: String,
default: '8rpx'
},
barColor: {
type: String,
default: '#87CEEB'
},
// 高度配置
minHeight: {
type: Number,
default: 20
},
maxHeight: {
type: Number,
default: 40
},
// 动画配置
animationSpeed: {
type: Number,
default: 2 // 波浪速度,值越大越快
},
// 是否开启动画
isAnimating: {
type: Boolean,
default: true
}
},
data() {
return {
bars: [],
animationTimer: null,
animationTime: 0 // 动画时间计数器
}
},
computed: {
containerStyle() {
return {
width: this.containerWidth,
height: this.containerHeight,
border: '1px solid blue',
position: 'relative',
overflow: 'hidden'
}
},
waveStyle() {
return {
position: 'absolute',
left: '0',
top: '0',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}
}
},
watch: {
isAnimating: {
immediate: true,
handler(newVal) {
if (newVal) {
this.startAnimation()
} else {
this.stopAnimation()
}
}
},
barCount: {
immediate: true,
handler() {
this.initBars()
}
}
},
methods: {
// 初始化bars数据
initBars() {
this.bars = Array.from({ length: this.barCount }, (_, index) => ({
index: index,
currentHeight: this.minHeight,
phase: (index / this.barCount) * Math.PI * 2 // 相位偏移,创建波浪效果
}))
},
// 获取bar样式
getBarStyle(bar, index) {
return {
width: this.barWidth,
height: bar.currentHeight + 'rpx',
backgroundColor: this.barColor,
borderRadius: '4rpx',
opacity: '0.8',
marginLeft: index === 0 ? '0' : this.barSpacing,
transition: 'height 0.1s ease-out' // 平滑过渡
}
},
// 开始动画
startAnimation() {
this.stopAnimation()
this.animationTime = 0
const animate = () => {
if (!this.isAnimating) return
this.animationTime += 0.1
this.updateBarHeights()
this.animationTimer = setInterval(animate, 50) // 20fps
}
animate()
},
// 停止动画
stopAnimation() {
if (this.animationTimer) {
clearInterval(this.animationTimer)
this.animationTimer = null
}
},
// 更新所有bar的高度
updateBarHeights() {
this.bars.forEach((bar,index) => {
// 使用正弦函数创建波浪效果
const wave = Math.sin(this.animationTime * this.animationSpeed + bar.phase)
// 将正弦值(-1到1)映射到高度范围
const normalized = (wave + 1) / 2 // 转换为0到1
bar.currentHeight = Math.round(
this.minHeight + normalized * (this.maxHeight - this.minHeight)
)
this.bars.splice(index,1,bar)
})
},
// 动态更新声浪高度(外部调用)
setBarHeights(heights) {
if (heights && heights.length === this.barCount) {
this.stopAnimation()
this.bars.forEach((bar, index) => {
let height = heights[index]
if (typeof height === 'string') {
height = parseInt(height)
}
bar.currentHeight = Math.max(this.minHeight, Math.min(this.maxHeight, height || this.minHeight))
this.bars.splice(index,1,bar)
})
}
},
// 随机生成声浪高度
simulateAudio() {
const heights = Array.from({ length: this.barCount }, () => {
return Math.round(Math.random() * (this.maxHeight - this.minHeight) + this.minHeight)
})
this.setBarHeights(heights)
},
// 模拟真实波浪效果
simulateWave() {
//this.isAnimating = true
this.startAnimation()
}
},
mounted() {
this.initBars()
if (this.isAnimating) {
this.startAnimation()
}
},
beforeUnmount() {
this.stopAnimation()
}
}
</script>
<style scoped>
.audio-wave-container {
box-sizing: border-box;
}
.audio-wave {
box-sizing: border-box;
}
.wave-bar {
box-sizing: border-box;
flex-shrink: 0;
}
</style>

View File

@@ -0,0 +1,819 @@
<template>
<div class="content-container">
<div class="title" ref="refTitle">
<div class="flex-row">
<el-select v-model="museumCategorySelectedId" clearable placeholder="请选择目录"
@change="mesumCategorySelectedChange" style="width: 140px" >
<el-option
v-for="item in mesumCategoryOptions"
:key="item.id"
:label="item.label"
:value="item.id">
</el-option>
</el-select>
</div>
<el-input placeholder="请输入标题搜索" v-model="labelFilter" clearable style="width: 200px;" >
<template #append>
<el-button icon="el-icon-search" @click="filterByLabel"></el-button>
</template>
</el-input>
<h3>讲解点查看与修改</h3>
<span>{{antiqueFiltered.length}}</span>
<el-button round type="primary" size="medium" @click="insertAntiqueTrigger">增加</el-button>
<div class="flex-row">
<el-select v-model="mesumSelectedId" clearable placeholder="请选择博物馆" @change="mesumSelectedChange"
default-first-option style="width: 140px">
<el-option
v-for="item in mesumOptions"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</div>
</div>
<el-row >
<el-col :span="24">
<el-table
:data="antiqueFiltered"
border
:height="tableHeight"
highlight-current-row
style="width: 100%"
@current-change="handleCurrentChange">
<el-table-column
prop="id"
label="编号"
width="100">
</el-table-column>
<el-table-column
prop="sort_order"
label="序号"
width="120">
<template #default="scope">
<div class="editable-cell">
<div
v-if="!isRowEditing(scope.row)"
class="display-value"
@click.stop="startRowEdit(scope.row)"
>
{{ scope.row.sort_order }}
</div>
<el-input
v-else
v-model.number="editingRowData.sort_order"
type="number"
size="mini"
class="edit-input"
@keyup.enter="saveRowEdit"
@keyup.esc="cancelRowEdit"
/>
</div>
</template>
</el-table-column>
<el-table-column
prop="label"
label="标题"
width="150">
</el-table-column>
<el-table-column
prop="category"
label="目录"
width="260">
</el-table-column>
<el-table-column
prop="combined"
label="解说词"
width="350">
<template #default="scope">
<div style="display: flex;flex-direction: column;align-items: center;justify-content: flex-start">
<el-popover
placement="top-start"
title="详细内容"
width="600"
trigger="click"
:content="scope.row.combined">
<template #reference>
<div>{{antiqueText(scope.row)}}</div>
</template>
</el-popover>
<div v-if="orginTextGT400" >
<el-button v-if="scope.row.combined.length>400" size="small" @click="triggerAiRefine(scope.row)"
style="margin-top: 10px;border:1px solid lightblue">
{{ `${scope.row.combined.length}大于400字-->AI整理`}}
</el-button>
<span v-if="scope.row.orgin_text && Math.abs(scope.row.orgin_text.length-scope.row.combined.length)>20" style="color:blue">经过整理</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column
prop="ttsUrl_adult"
label="音频地址(成年)"
width="300">
<template #default="scope">
<div style="display: flex;flex-direction: column;align-items: center;justify-content: space-between">
<div>{{scope.row.ttsUrl_adult}}</div>
</div>
<audio v-if="rowSelected(scope.row) && ttsUrl_valid(scope.row.ttsUrl_adult )" class="audio-player" :src="scope.row.ttsUrl_adult" controls></audio>
</template>
</el-table-column>
<el-table-column
prop="ttsUrl_child"
label="音频地址(童声)"
width="300">
<template #default="scope">
<div style="display: flex;flex-direction: column;align-items: center;justify-content: space-between">
<div>{{scope.row.ttsUrl_child}}</div>
</div>
<audio v-if="rowSelected(scope.row) && ttsUrl_valid(scope.row.ttsUrl_child )" class="audio-player" :src="scope.row.ttsUrl_child" controls></audio>
</template>
</el-table-column>
<el-table-column
fixed="right"
label="操作"
width="220">
<template #default="scope">
<div v-if="isRowEditing(scope.row)" style="display: flex;flex-direction: row;align-items: center;justify-content: space-between">
<el-button
size="small"
type="success"
@click.stop="saveRowEdit"
>
保存
</el-button>
<el-button
size="small"
@click.stop="cancelRowEdit"
>
取消
</el-button>
</div>
<div v-else style="display: flex;flex-direction: row;align-items: center;justify-content: space-between">
<el-button type="primary" size="small" @click.stop="editAntiqueTrigger(scope.row)" >编辑</el-button>
<el-button type="success" size="small" @click.stop="editMarkdownTrigger(scope.row)" >MD</el-button>
<el-button type="danger" size="small" @click.stop="removeAntiqueInServer(scope.row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
<detail-editor :visible="dialogVisible" :initData="form" :mesumSelectedId="mesumSelectedId"
:photo_prefix="mesumSelected.photo_prefix" @update:close="dialogVisible = false"
@update:refresh="handleRefreshAntique">
</detail-editor>
<ai-text-editor :visible="AiRefineDialogVisible" :initData="aiRefineForm" :mesumSelectedId="mesumSelectedId"
:photo_prefix="mesumSelected.photo_prefix" @update:close="AiRefineDialogVisible = false"
@update:refresh="handleRefreshAntique">
</ai-text-editor>
<markdown-editor :visible="markdownDialogVisible" :initData="markdownForm" :mesumSelectedId="mesumSelectedId"
:photo_prefix="mesumSelected.photo_prefix" @update:close="markdownDialogVisible = false"
@update:refresh="handleRefreshAntique">
</markdown-editor>
</div>
</template>
<script>
import {api_getMesumList,
api_getMesumAntiques,
api_getAntiqueDetail,
BASE_API_URL,
exhibit_tts_bucket_name,
exhibit_photo_bucket_name} from "@/API/api.js"
import DetailEditor from "@/components/DetailEditor";
import AiTextEditor from "@/components/AiTextEditor";
import MarkdownEditor from "@/components/MarkdownEditor";
export default {
name: "ExhibitDetail",
props: {'museumListData':{default:[]}, 'resource':{},'mode':{},'antiqueListData':{default:[]}},
inject:{'root':{default:null},
'getProject':{default:null},'getSchedule':{default:null}},
components:{
DetailEditor,
AiTextEditor,
MarkdownEditor
},
data() {
return {
form: {
action:"update",
mesum_id:1,
id:"",
text: '',
label:"",
category:"",
category_l2:null,
tags: null,
voiceType: 'zh_male_ruyaqingnian_mars_bigtts',
ttsUrl_cn:"",
photo_url:null,
photoFileObject:null,
formTitle:"修改讲解点",
},
upload: {
minioPath: 'mp3',
isUploading: false
},
uploadFile_adult:null,
uploadFile_child:null,
mesumOptions:[ ],
mesumCategoryOptions:[],
mesum_antiques:[],
antiqueSelectedRow:null,
dialogVisible:false,
mesumSelectedId:null,
mesumSelected:{id:1},
museumCategorySelectedId:null,
currentDeviceList:[],
deviceSelected:null,
currentDeviceLogList:[],
labelFilter:"",
enableLabelFilter:false,
tableHeight:200,
AiRefineDialogVisible:false,
aiRefineForm: {
id:"",
text: '',
aiOriginAntiqueDetailText:""
},
labelToRefine:"",
//
// Markdown 编辑器相关
markdownDialogVisible: false,
markdownForm: {
id: "",
label: "",
mesum_id: 1,
md_file_url: "",
md_tts_url: ""
},
//
// 20250920 增加和行编辑有关的变量和函数
editingRow: null, // 当前正在编辑的行原始数据
editingRowData: {}, // 编辑中的数据副本
originalRowData: {} // 原始数据备份(用于取消编辑)
};
},
created() {
},
async mounted() {
await this.getMesumList();
await this.getMesumAntiques(true);
// 初始获取高度
this.updateTableHeight()
// 监听窗口大小变化
window.addEventListener('resize', this.updateTableHeight)
},
beforeUnmount() {
// 组件销毁时移除监听
window.removeEventListener('resize', this.updateTableHeight)
},
methods: {
// eslint-disable-next-line no-unused-vars
async mesumCategorySelectedChange(_value){
this.enableLabelFilter = false
},
filterByLabel(){
this.enableLabelFilter = true
},
async mesumSelectedChange(value){
this.form.mesum_id = value
this.mesumSelected = this.mesumOptions.find((item)=>{
return item.id === this.mesumSelectedId
})
await this.getMesumAntiques(true);
},
handleCurrentChange(val) {
this.antiqueSelectedRow = val;
// eslint-disable-next-line no-empty
//if(this.antiqueSelectedRow){
// this.startRowEdit(val)
//}
},
// eslint-disable-next-line no-unused-vars
triggerAiRefine(row){
if(!row.orgin_text || row.orgin_text==""){
this.$message.error("没有找到原始的解说词,或者原始解说词为空");
return
}
this.aiRefineForm.aiOriginAntiqueDetailText = row.orgin_text
this.aiRefineForm.id = row.id
this.aiRefineForm.label = row.label
this.aiRefineForm.mesum_id = this.mesumSelectedId;
this.AiRefineDialogVisible = true;
},
async insertAntiqueTrigger(){
this.form.formTitle = "新增讲解点"
this.form.action='insert'
this.form.text =""
this.form.label = "";
this.form.category = "";
this.form.id = ""
this.dialogVisible = true
},
// 触发 Markdown 编辑器
editMarkdownTrigger(row) {
this.markdownForm.id = row.id;
this.markdownForm.label = row.label;
this.markdownForm.mesum_id = this.mesumSelectedId;
this.markdownForm.md_file_url = row.md_file_url || "";
this.markdownForm.md_tts_url = row.md_tts_url || "";
this.markdownDialogVisible = true;
console.log("editMarkdownTrigger", row);
},
// eslint-disable-next-line no-unused-vars
editAntiqueTrigger(row){
this.form.formTitle = "修改讲解点"
this.uploadFile_adult = null
this.uploadFile_child = null
this.form.action='update'
this.form.text = row.combined;
this.form.label = row.label;
this.form.category = row.category;
this.form.category_l2 = row.category_l2;
this.form.tags = row?.tags;
this.form.id = row.id;
this.form.ttsUrl_adult = row.ttsUrl_adult
this.form.ttsUrl_child = row.ttsUrl_child
this.form.photo_url = row.photo_url
this.form.photoFileObject=null
this.dialogVisible = true
console.log("editAntiqueTrigger")
},
async removeMinioObject(bucket,fileName){
// 使用fetch发送请求
fetch(`${BASE_API_URL}/minio/rm`, {
method: 'POST',
body: JSON.stringify({
bucket:bucket,
file_name:fileName
}),
headers: {
// fetch会自动设置Content-Type为multipart/form-data无需手动指定
"Authorization": "Bearer ragflow-NhZTY5Y2M4YWQ1MzExZWY4Zjc3MDI0Mm"
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json(); // 如果服务器返回JSON数据
})
.then(res => {
const {data} = res
if(data?.rm){
this.$message.success('删除TTS url成功');
}
})
.catch(error => {
console.error('删除minio object 失败:', error);
//this.$message.error('删除minio object 失败');
});
},
// eslint-disable-next-line no-unused-vars
async removeAntiqueInServer(row){
this.$confirm('确认删除?')
.then( async () => {
try{
//先删除tts
if(row.ttsUrl_adult){
// eslint-disable-next-line no-unused-vars
const [baseUrl_adult,file_name_adult]=row.ttsUrl_adult.split(`${exhibit_tts_bucket_name}/`)
if(file_name_adult && file_name_adult!=="")
await this.removeMinioObject(exhibit_tts_bucket_name,file_name_adult)
}
if(row.ttsUrl_child){
// eslint-disable-next-line no-unused-vars
const [baseUrl_child,file_name_child]=row.ttsUrl_child.split(`${exhibit_tts_bucket_name}/`)
if(file_name_child && file_name_child!=="")
await this.removeMinioObject(exhibit_tts_bucket_name,file_name_child)
}
//删除展品图片
if(row.photo_url){
// eslint-disable-next-line no-unused-vars
const [baseUrl_child,file_name_child]=row.photo_url.split(`${exhibit_photo_bucket_name}/`)
if(file_name_child && file_name_child!=="")
await this.removeMinioObject(exhibit_photo_bucket_name,file_name_child)
}
// eslint-disable-next-line no-empty
}finally {
}
const response = await fetch(`${BASE_API_URL}/mesum/antique/rm/${this.mesumSelectedId}/${row.id}`, {
method: 'POST',
headers: {'Content-Type': 'application/json',
'Authorization': 'Bearer ragflow-NhZTY5Y2M4YWQ1MzExZWY4Zjc3MDI0Mm'},
body:JSON.stringify({})
})
const result = await response.json()
const {data} = result
if(data && data.rm){
this.getMesumAntiques()
this.$message('删除成功');
}
console.log("remove antique",data)
})
.catch(() => {});
},
updateTableHeight() {
const titleRect= this.$refs.refTitle.getBoundingClientRect()
this.tableHeight = window.innerHeight-titleRect.bottom-16
},
async getMesumList(){
const data = await api_getMesumList()
if(data && Array.isArray(data) && data.length>=1){
this.mesumOptions = []
this.mesumOptions = data
this.mesumSelected = data[0]
this.mesumSelectedId = data[0].id
this.form.mesum_id = data[0].id
}
console.log(data)
},
async getMesumAntiques(updateCategory){
const data = await api_getMesumAntiques(this.mesumSelected.id)
if(data){
console.log(data)
const len = this.mesum_antiques.length;
this.mesum_antiques.splice(0,len)
this.mesum_antiques.splice(0,0,...data['anqituqes']);
console.log(this.mesum_antiques)
this.mesumCategoryOptions.splice(0,this.mesumCategoryOptions.length);
if(data['categories'] && Array.isArray(data['categories'])){
const options = data['categories'].map((name,index)=>{
return {id:index,label:name}
})
options.push({id:options.length,label:"全部"})
this.mesumCategoryOptions.splice(0,0,...options)
if(options.length && updateCategory ){
this.museumCategorySelectedId = this.mesumCategoryOptions[0].id
}
}
//this.updateFormContent(); //如果正在编辑,需要跟新对话框内容
}
},
// 处理展品刷新事件
async handleRefreshAntique(antiqueId) {
if (typeof antiqueId === 'number' || typeof antiqueId === 'string') {
let hasTargetId = this.mesum_antiques.some(item => item.id === Number(antiqueId));
// 如果传入的是展品ID只刷新该展品
if(hasTargetId)
await this.refreshSingleAntique(antiqueId)
else
await this.getMesumAntiques(false)
} else {
// 否则刷新整个列表(兼容旧逻辑或新增/删除情况)
await this.getMesumAntiques(false)
}
},
// 新增:只更新单个展品数据
async refreshSingleAntique(antiqueId) {
if (!antiqueId) {
console.warn('展品ID为空无法刷新')
return
}
try {
// 调用API获取单个展品详情
const updatedData = await api_getAntiqueDetail(this.mesumSelectedId, antiqueId)
if (updatedData) {
// 在当前列表中查找并更新该展品
const index = this.mesum_antiques.findIndex(item => item.id === antiqueId)
if (index !== -1) {
// 使用Vue3响应式更新方式确保视图刷新
this.mesum_antiques.splice(index, 1, updatedData)
console.log(`展品 ${antiqueId} 已更新`)
this.$message.success('展品数据已刷新')
} else {
console.warn(`未找到展品 ${antiqueId},刷新整个列表`)
// 如果找不到(例如新增的展品),则重新获取整个列表
await this.getMesumAntiques(false)
}
}
} catch (error) {
console.error('刷新展品数据失败:', error)
this.$message.error('刷新失败,请重试')
}
},
// 20250920 增加和行编辑有关的变量和函数
// 开始编辑行
async startRowEdit(row) {
// 如果已经在编辑其他行,先保存或取消
if (this.editingRow && this.editingRow.id !== row.id) {
// 检查当前编辑的行数据是否有变化
const hasChanges = this.isRowDataChanged(this.originalRowData, this.editingRowData)
if (hasChanges) {
// 有变化,提示用户是否保存
try {
await this.$confirm('当前编辑的内容尚未保存,是否保存?', '提示', {
confirmButtonText: '保存',
cancelButtonText: '不保存',
type: 'warning'
})
// 用户选择保存
await this.saveRowEdit()
} catch (error) {
if (error === 'cancel') {
// 用户选择不保存,直接取消编辑
this.cancelRowEdit()
} else {
// 用户关闭提示框,中止切换编辑行
return
}
}
} else {
// 没有变化,直接取消当前编辑
this.cancelRowEdit()
}
}
// 开始编辑新行
this.startRowEditInternal(row)
},
// 实际开始编辑行的内部逻辑
startRowEditInternal(row) {
this.editingRow = { ...row }
this.editingRowData = { ...row }
this.originalRowData = { ...row }
},
// 保存行编辑
async saveRowEdit() {
if (!this.editingRow) return
try {
// 验证行数据
if (!this.validateRowData()) {
return
}
// 调用API更新后台
await this.updateRowInBackend(this.editingRow.id, {'sort_order':this.editingRowData.sort_order})
// 更新前端表格数据 - 只刷新当前修改的展品
await this.refreshSingleAntique(this.editingRow.id)
this.$message.success('保存成功')
this.cancelRowEdit()
} catch (error) {
this.$message.error('保存失败:' + error.message)
}
},
// 取消行编辑
cancelRowEdit() {
if (this.editingRow) {
// 恢复原始行数据
}
this.editingRow = null
this.editingRowData = {}
this.originalRowData = {}
},
// 验证行数据
validateRowData() {
/*if (!this.editingRowData.category || this.editingRowData.category.trim() === '') {
this.$message.warning('分类不能为空')
return false
}
*/
// 验证序号不能为空
if (this.editingRowData.sort_order === null || this.editingRowData.sort_order === undefined || this.editingRowData.sort_order === '') {
this.$message.warning('序号不能为空')
return false
}
if (this.editingRowData.sort_order < 0) {
this.$message.warning('序号不能为负数')
return false
}
return true
},
// 调用后台API更新行数据
// eslint-disable-next-line no-unused-vars
async updateRowInBackend(id, new_data) {
let antique_data = {
mesum_id:this.mesumSelectedId,
//根据传入参数中包含的key在下面程序动态设置
//label:this.formData.label,
//combined:this.formData.text,
//category:this.formData.category
}
// eslint-disable-next-line no-prototype-builtins
if(new_data.hasOwnProperty('label'))
antique_data['label'] = new_data['label']
// eslint-disable-next-line no-prototype-builtins
if(new_data.hasOwnProperty('text'))
antique_data['combined'] = new_data['text']
// eslint-disable-next-line no-prototype-builtins
if(new_data.hasOwnProperty('category'))
antique_data['category'] = new_data['category']
// eslint-disable-next-line no-prototype-builtins
if(new_data.hasOwnProperty('sort_order'))
antique_data['sort_order'] = new_data['sort_order']
this.$confirm('确认更改?')
.then( async () => {
const response = await fetch(`${BASE_API_URL}/mesum/antique/update/${this.mesumSelectedId}/${id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ragflow-NhZTY5Y2M4YWQ1MzExZWY4Zjc3MDI0Mm'
},
body: JSON.stringify(antique_data)
})
const result = await response.json()
const {data} = result
if (data && data.update){
// 只刷新单个展品数据
await this.refreshSingleAntique(id);
this.$message('更新成功');
this.closeDialog()
}
console.log("update antique", data)
}).catch(() =>{
})
},
// 处理行点击事件
handleRowClick(row, column, event) {
// 防止点击按钮时触发双击编辑
if (event.target.tagName !== 'BUTTON' && event.detail === 2) {
this.startRowEdit(row)
}
},
// 处理外部点击事件(保存编辑)
handleOutsideClick(event) {
if (this.editingRow && !event.target.closest('.editable-cell')) {
this.saveRowEdit()
}
},
// 判断行数据是否有变化
isRowDataChanged(original, current) {
// 检查所有可编辑字段是否发生变化
const fieldsToCheck = ['sort_order']
return fieldsToCheck.some(field => {
const originalValue = original[field]
const currentValue = current[field]
// 处理null和undefined的情况
if (originalValue === null || originalValue === undefined) {
return currentValue !== null && currentValue !== undefined
}
// 处理字符串类型(去除前后空格比较)
if (typeof originalValue === 'string') {
return originalValue.trim() !== String(currentValue || '').trim()
}
// 其他类型直接比较
return originalValue !== currentValue
})
},
},
watch:{
},
computed: {
rowSelected(){
return (row)=>{
if(this.antiqueSelectedRow && (this.antiqueSelectedRow.id==row.id))
return true;
else
return false
}
},
ttsUrl_valid(){
return (url)=>{
if(url && url!=='')
return true
else
return false
}
},
deviceIsSelected(){
return (item)=>{
return item === this.deviceSelected?.device_id
}
},
antiqueFiltered(){
if(this.enableLabelFilter){
if(this.labelFilter && this.labelFilter!=='') {
return this.mesum_antiques.filter((item)=>{
return item.label.includes(this.labelFilter)
})
}else
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.enableLabelFilter = false
}
if(this.museumCategorySelectedId == null )
return this.mesum_antiques;
else if(this.museumCategorySelectedId == (this.mesumCategoryOptions.length-1))
{
return this.mesum_antiques;
}
else{
console.log("categoryId",this.museumCategorySelectedId)
return this.mesum_antiques.filter((item)=>{
return item.category === this.mesumCategoryOptions[this.museumCategorySelectedId].label
})
}
},
antiqueText(){
return (row)=>{
const text = row.combined || ""
if(text.length<20) return text;
else
return text.substring(0,20) +"..."
}
},
orginTextGT400(){
return(row)=>{
let result = false;
if(!row.orgin_text) return false;
if(row && row.combined.length>400) result = true;
if(row.orgin_text && row.combined && row.orgin_text.length!==row.combined.length)
result = true;
return result;
}
},
// 判断当前行是否正在编辑
isRowEditing() {
return (row)=>{
return this.editingRow && this.editingRow.id === row.id
}
},
}
}
</script>
<style scoped>
.content-container{
width: 88vw;
height: 97vh;
box-sizing: border-box;
overflow-y: hidden;
}
.title{width:100%;display: flex;align-items: center;justify-content: space-between;}
.flex-row { display: flex; gap: 15px; align-items: center; }
.form-group { padding: 10px; margin-bottom: 5px;border-bottom: 2px solid #eee; }
label { display: block; margin-bottom: 5px; }
input, select, textarea { width: 100%; padding: 8px; }
button { padding: 10px 20px; margin:5px; cursor: pointer; }
.audio-player { margin: 20px 0; height:40px}
.editable-cell {
min-height: 32px;
display: flex;
align-items: center;
}
.display-value {
width: 100%;
padding: 8px;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s;
}
.display-value:hover {
background-color: #f5f7fa;
}
.edit-input {
width: 100%;
}
</style>

View File

@@ -0,0 +1,243 @@
<template>
<div class="content-container">
<div class="title" ref="refTitle"/>
<el-row v-if="0" >
<el-col :span="24">
<el-table
:data="museumListData"
border
:height="tableHeight"
highlight-current-row
style="width: 100%"
@current-change="handleCurrentChange">
<el-table-column
prop="id"
label="编号"
width="100">
</el-table-column>
<el-table-column
prop="name"
label="名称"
width="150">
</el-table-column>
<el-table-column
prop="brief"
label="简介"
width="460">
</el-table-column>
<el-table-column
prop="address"
label="地址"
width="350">
</el-table-column>
<el-table-column
prop="photo_prefix"
label="prefix"
width="180">
</el-table-column>
<el-table-column
fixed="right"
label="操作"
width="180">
<template #default="scope">
<div style="display: flex;flex-direction: row;align-items: center;justify-content: space-between">
<el-button type="primary" size="small" @click="editMuseumTrigger(scope.row)" >编辑</el-button>
<el-button type="danger" size="small" @click="removeMuseumInServer(scope.row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
</div>
</template>
<script>
import {api_getMesumAntiques, api_getMesumList} from "@/API/api";
export default {
name: "LocationManager",
components: {
},
data() {
return {
activeNaviMenuIndex:'1',
museumListData:[],
tableHeight:500,
isWaveAnimating: true
};
},
created() {
},
async mounted() {
await this.getMesumList();
this.updateTableHeight()
// 监听窗口大小变化
window.addEventListener('resize', this.updateTableHeight)
},
beforeUnmount() {
// 组件销毁时移除监听
window.removeEventListener('resize', this.updateTableHeight)
},
methods: {
async getMesumList(){
const data = await api_getMesumList()
if(data && Array.isArray(data) && data.length>=1){
this.museumListData.splice(0,this.museumListData.length)
this.museumListData.splice(0,0,...data)
this.mesumSelected = data[0]
this.mesumSelectedId = data[0].id
}
console.log(data)
},
async getMesumAntiques(updateCategory){
const data = await api_getMesumAntiques(this.mesumSelected.id)
if(data){
console.log(data)
const len = this.mesum_antiques.length;
this.mesum_antiques.splice(0,len)
this.mesum_antiques = data['anqituqes']
console.log(this.mesum_antiques)
this.mesumCategoryOptions.splice(0,this.mesumCategoryOptions.length);
if(data['categories'] && Array.isArray(data['categories'])){
const options = data['categories'].map((name,index)=>{
return {id:index,label:name}
})
options.push({id:options.length,label:"全部"})
this.mesumCategoryOptions.splice(0,0,...options)
if(options.length && updateCategory ){
this.museumCategorySelectedId = this.mesumCategoryOptions[0].id
}
}
//this.updateFormContent(); //如果正在编辑,需要跟新对话框内容
}
},
// eslint-disable-next-line no-unused-vars
handleCurrentChange(value){
},
// eslint-disable-next-line no-unused-vars
editMuseumTrigger(row){
},
// eslint-disable-next-line no-unused-vars
removeMuseumInServer(row){
},
updateTableHeight() {
const titleRect= this.$refs.refTitle.getBoundingClientRect()
this.tableHeight = window.innerHeight-titleRect.bottom-16
},
},
watch:{
},
computed: {
}
}
</script>
<style scoped>
.content-container{
width: 88vw;
height: 97vh;
box-sizing: border-box;
overflow-y: hidden;
}
.title{
width:100%;
height: 5vh;
display: flex;align-items: center;justify-content: space-between;
}
.flex-row { display: flex; gap: 15px; align-items: center; }
.wave-bars {
display: flex;
align-items: flex-end;
margin-bottom: 20px;
}
.audio-wave {
width: 100%;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
}
.wave-bar {
width: 3px;
margin: 0 2px;
background-color: lightblue;
transition: height 0.1s ease-in-out;
animation: waveAnimation 1s ease-in-out infinite;
opacity: 0.8;
/* 设置初始高度,避免抖动 */
height: 20px;
}
.wave-bar:nth-child(1) {
animation-delay: 0s;
}
.wave-bar:nth-child(2) {
animation-delay: 0.1s;
}
.wave-bar:nth-child(3) {
animation-delay: 0.2s;
}
.wave-bar:nth-child(4) {
animation-delay: 0.3s;
}
.wave-bar:nth-child(5) {
animation-delay: 0.4s;
}
.wave-bar:nth-child(6) {
animation-delay: 0.5s;
}
.wave-bar:nth-child(7) {
animation-delay: 0.6s;
}
.wave-bar:nth-child(8) {
animation-delay: 0.7s;
}
.wave-bar:nth-child(9) {
animation-delay: 0.8s;
}
.wave-bar:nth-child(10) {
animation-delay: 0.9s;
}
@keyframes waveAnimation {
0%, 100% {
height: 20px;
}
25%,75% {
height: 30px;
}
50% {
height: 40px;
}
}
</style>

View File

@@ -0,0 +1,885 @@
<template>
<el-dialog
title="Markdown 文件与TTS编辑"
v-model="dialogVisible"
width="85%"
align-center
:before-close="handleDialogClose"
>
<div class="input-section">
<div class="form-group">
<el-row>
<el-col :span="24" style="border: 1px solid lightgray; padding: 10px;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
<span style="font-weight: bold;">展品ID: {{formData.id}} - {{formData.label}}</span>
<div style="display: flex; gap: 10px; align-items: center;">
<span>Markdown文件地址:</span>
<a v-if="formData.md_file_url" :href="formData.md_file_url" target="_blank" style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;">
{{formData.md_file_url}}
</a>
<span v-else style="color: #999;">未上传</span>
</div>
</div>
<!-- Markdown 文件上传区域 -->
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 20px; padding: 10px; background: #f5f7fa; border-radius: 4px;">
<el-upload
ref="uploadMdRef"
action=""
:auto-upload="false"
:limit="1"
:on-change="handleMdFileChange"
:on-exceed="handleMdFileExceed"
:show-file-list="false"
accept=".md,.markdown,.txt">
<template #trigger>
<el-button type="primary" size="small">选择Markdown文件</el-button>
</template>
</el-upload>
<span v-if="mdFile" style="color: #67C23A;">{{mdFile.name}}</span>
<el-button
size="small"
type="success"
:disabled="!mdFile"
@click="submitMdFileUpload">
上传并更新
</el-button>
<el-button
size="small"
type="danger"
:disabled="!formData.md_file_url"
@click="removeMdFileUrl">
删除Markdown地址
</el-button>
<el-button
size="small"
type="info"
:disabled="!formData.md_file_url"
@click="loadMdContent">
读取Markdown内容
</el-button>
<el-button
size="small"
type="warning"
:disabled="!formData.md_file_url || !mdContent || mdContent.length === 0"
@click="saveMdContent">
保存修改到MinIO
</el-button>
</div>
<!-- Markdown 内容编辑区域 -->
<div style="margin-bottom: 20px;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
<span style="font-weight: bold;">Markdown 内容编辑:</span>
<span style="color: #909399; font-size: 12px;">字符数: {{mdContent.length}}</span>
</div>
<textarea
v-model="mdContent"
rows="15"
placeholder="请先上传Markdown文件或读取现有内容..."
style="width: 100%; font-family: monospace; padding: 10px; border: 1px solid #dcdfe6; border-radius: 4px;">
</textarea>
</div>
</el-col>
</el-row>
</div>
<!-- TTS 音频生成与上传区域 -->
<div style="display: flex; align-items: center; justify-content: space-between; color: blue; margin: 10px 0;">
<div>Markdown TTS 音频</div>
</div>
<el-row v-loading="isTTSGenerating" element-loading-text="音频生成中...">
<el-col :span="24" style="border: 1px solid gray; padding: 15px;">
<div style="display: flex; flex-direction: row; align-items: center; justify-content: flex-start; margin-bottom: 15px;">
<span style="margin-right: 20px; display: inline-block; width: 200px;">TTS音频地址:</span>
<span style="display: inline-block; flex: 1;">
<a v-if="formData.md_tts_url" :href="formData.md_tts_url" target="_blank">{{formData.md_tts_url}}</a>
<span v-else style="color: #999;">未生成</span>
</span>
<el-button
type="danger"
size="default"
:disabled="!formData.md_tts_url"
@click="removeMdTtsUrl">
删除音频地址
</el-button>
</div>
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 15px;">
<div style="display: flex; justify-content: flex-start; align-items: center;">
<label>音色选择</label>
<el-select
v-model="voiceSelected_md"
clearable
placeholder="请选择音色"
@change="voiceSelectedChangeMd"
allow-create
default-first-option
style="width: 140px;">
<el-option
v-for="item in voiceOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</div>
<el-button
type="primary"
:disabled="!mdContent || mdContent.length === 0"
@click="generateMdTTS">
生成语音
</el-button>
<audio class="audio-player" ref="audioPlayerMd" :src="preview.audioSrc_md" controls></audio>
<el-button
type="primary"
:disabled="!preview.downReady_md"
@click="downloadMdTTS">
下载语音
</el-button>
</div>
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 15px;">
<div style="width: 90%; display: flex; justify-content: flex-start; align-items: center;">
<span>下载文件名</span>
<el-input v-model="downloadFilename_md"/>
</div>
<div style="display: flex; justify-content: flex-start; align-items: center;">
<span style="display: inline-block; width: 100px;">{{downloadPromptMd}}</span>
</div>
</div>
<!-- 上传区域 -->
<div class="upload-section">
<el-row v-show="preview.visible_md" align="middle" style="height: 5vh">
<el-col :span="14">
<div style="margin-left: 10px; width: 100%; height: 5vh; display: flex; align-items: center;">
<el-upload
action=""
:auto-upload="false"
:on-change="handleUploadChangeMd"
:show-file-list="false"
style="display: flex; align-items: center">
<template #trigger>
<el-button type="primary">选取文件</el-button>
</template>
<div style="display: inline-block; width: 200px;">{{uploadFileName_md}}</div>
</el-upload>
</div>
</el-col>
<el-col :span="8">
<el-button
:disabled="!uploadFile_md"
style="width: 90%;"
size="default"
type="success"
@click="submitMdTTSUpload">
上传并修改音频地址
</el-button>
</el-col>
</el-row>
</div>
</el-col>
</el-row>
</div>
<!-- 结果展示 -->
<div class="result-section">
<div v-if="result.error" class="error-message">
错误{{ result.error }}
</div>
<div v-if="result.success" class="success-message">
<p>操作成功</p>
</div>
</div>
</el-dialog>
</template>
<script>
import {
BASE_API_URL,
WEIXIN_WS_URL,
chatId,
exhibit_tts_bucket_name,
exhibit_md_bucket_name,
exhibit_photo_bucket_name
} from "@/API/api";
export default {
name: "MarkdownEditor",
props: {
'mesumSelectedId': {default: -1},
'mesumSelected': {},
'initData': {},
'visible': {default: false},
'photo_prefix': {default: 'temp'}
},
inject: {
'root': {default: null},
'getProject': {default: null},
'getSchedule': {default: null}
},
data() {
return {
formData: {
mesum_id: 1,
id: "",
label: "",
md_file_url: "",
md_tts_url: ""
},
mdFile: null,
mdContent: "",
preview: {
visible_md: false,
downReady_md: false,
audioSrc_md: '',
tempBlob_md: null
},
uploadFile_md: null,
result: {
error: null,
success: false,
minioUrl: ''
},
isTTSGenerating: false,
voiceSelected_md: "sambert-zhichu-v1@Tongyi-Qianwen",
downloadFilename_md: "md_tts.mp3",
voiceOptions: [
{"value": "cosyvoice-v1/longyuan@Tongyi-Qianwen", "label": "亲切女生"},
{"value": "sambert-zhiru-v1@Tongyi-Qianwen", "label": "新闻女生"},
{"value": "cosyvoice-v1/longhua@Tongyi-Qianwen", "label": "活泼女童"},
{"value": "cosyvoice-v1/longxiang@Tongyi-Qianwen", "label": "解说男声"},
{"value": "cosyvoice-v1/longyue@Tongyi-Qianwen", "label": "longyue"},
{"value": "cosyvoice-v1/longwan@Tongyi-Qianwen", "label": "longwan"},
{"value": "sambert-zhichu-v1@Tongyi-Qianwen", "label": "舌尖男声"},
{"value": "sambert-zhiying-v1@Tongyi-Qianwen", "label": "软萌童声"},
],
dialogVisible: false,
downloadStateMd: 'idle',
};
},
created() {
},
mounted() {
},
methods: {
// Markdown 文件选择
handleMdFileChange(file) {
this.mdFile = file.raw;
console.log("选择的Markdown文件:", file.name);
},
handleMdFileExceed(files) {
this.$refs.uploadMdRef.clearFiles();
const newFile = files[0];
this.$refs.uploadMdRef.handleStart(newFile);
this.handleMdFileChange({raw: newFile, name: newFile.name});
},
// 上传 Markdown 文件到 MinIO
async submitMdFileUpload() {
if (!this.mdFile) {
this.$message.warning('请先选择Markdown文件');
return;
}
try {
// 如果已有MD文件先删除旧的MinIO对象
await this.removeMdFileFromMinio(this.formData.md_file_url);
// 上传新文件
const formData = new FormData();
formData.append('file', this.mdFile);
// 使用原始文件名,不重命名
const fileName = this.mdFile.name;
formData.append('bucket', exhibit_md_bucket_name); // 使用专门的bucket存放markdown
formData.append('file_name', `${this.photo_prefix}/category/${fileName}`);
const response = await fetch(`${BASE_API_URL}/minio/put`, {
method: 'POST',
body: formData,
headers: {
"Authorization": "Bearer ragflow-NhZTY5Y2M4YWQ1MzExZWY4Zjc3MDI0Mm"
}
});
if (!response.ok) {
throw new Error('上传失败');
}
const res = await response.json();
const {data} = res;
if (data?.put) {
// 更新数据库
await this.updateAntiqueMdFile(data?.url);
this.$message.success('Markdown文件上传成功');
this.mdFile = null;
if (this.$refs.uploadMdRef) {
this.$refs.uploadMdRef.clearFiles();
}
}
} catch (error) {
console.error('上传Markdown文件失败:', error);
this.$message.error('上传Markdown文件失败');
}
},
// 读取 Markdown 文件内容
async loadMdContent() {
if (!this.formData.md_file_url) {
this.$message.warning('没有Markdown文件URL');
return;
}
try {
// 添加时间戳参数避免浏览器缓存
const url = this.formData.md_file_url + '?t=' + Date.now();
const response = await fetch(url);
if (!response.ok) {
throw new Error('读取文件失败');
}
const content = await response.text();
this.mdContent = content;
this.$message.success('Markdown内容加载成功');
} catch (error) {
console.error('读取Markdown内容失败:', error);
this.$message.error('读取Markdown内容失败');
}
},
// 保存 Markdown 内容修改到 MinIO
async saveMdContent() {
if (!this.formData.md_file_url) {
this.$message.warning('没有Markdown文件URL');
return;
}
if (!this.mdContent || this.mdContent.length === 0) {
this.$message.warning('Markdown内容为空');
return;
}
try {
const loading = this.$loading({
lock: true,
text: '正在保存Markdown内容到MinIO...',
background: 'rgba(0, 0, 0, 0.7)'
});
// 从URL中解析出bucket和文件名
let bucket = exhibit_md_bucket_name;
let fileName = '';
const urlParts = this.formData.md_file_url.split(`${exhibit_md_bucket_name}/`);
if (urlParts.length > 1) {
fileName = urlParts[1];
} else {
// 兼容旧的photo bucket格式
const photoUrlParts = this.formData.md_file_url.split(`${exhibit_photo_bucket_name}/`);
if (photoUrlParts.length > 1) {
bucket = exhibit_photo_bucket_name;
fileName = photoUrlParts[1];
} else {
throw new Error('无法解析文件路径');
}
}
// 将Markdown内容转换为Blob
const blob = new Blob([this.mdContent], { type: 'text/markdown' });
const file = new File([blob], fileName.split('/').pop() || 'content.md', { type: 'text/markdown' });
// 先删除旧文件根据经验记忆某些MinIO版本需要先删除再上传
await this.removeMinioObject(bucket, fileName);
// 上传新内容
const formData = new FormData();
formData.append('file', file);
formData.append('bucket', bucket);
formData.append('file_name', fileName);
const response = await fetch(`${BASE_API_URL}/minio/put`, {
method: 'POST',
body: formData,
headers: {
"Authorization": "Bearer ragflow-NhZTY5Y2M4YWQ1MzExZWY4Zjc3MDI0Mm"
}
});
if (!response.ok) {
throw new Error('上传失败');
}
const res = await response.json();
const {data} = res;
if (data?.put) {
// 验证上传是否成功:下载文件并检查内容
const verifyResponse = await fetch(this.formData.md_file_url + '?t=' + Date.now());
if (verifyResponse.ok) {
const verifyContent = await verifyResponse.text();
if (verifyContent === this.mdContent) {
loading.close();
this.$message.success('Markdown内容已成功保存到MinIO并验证通过');
} else {
loading.close();
this.$message.warning('Markdown内容已上传但验证发现内容可能未完全更新请刷新后重试');
}
} else {
loading.close();
this.$message.success('Markdown内容已保存到MinIO');
}
} else {
loading.close();
this.$message.error('保存失败:服务器返回失败状态');
}
} catch (error) {
console.error('保存Markdown内容到MinIO失败:', error);
this.$message.error('保存Markdown内容失败: ' + error.message);
}
},
// 从MD文件URL中删除MinIO对象独立函数
async removeMdFileFromMinio(md_file_url) {
if (!md_file_url || md_file_url === '') {
return;
}
const urlParts = md_file_url.split(`${exhibit_md_bucket_name}/`);
if (urlParts.length > 1) {
const fileName = urlParts[1];
if (fileName && fileName !== "") {
await this.removeMinioObject(exhibit_md_bucket_name, fileName);
console.log('已删除Markdown文件:', fileName);
}
} else {
// 兼容旧的URL格式或其他bucket
const photoUrlParts = md_file_url.split(`${exhibit_photo_bucket_name}/`);
if (photoUrlParts.length > 1) {
const fileName = photoUrlParts[1];
if (fileName && fileName !== "") {
await this.removeMinioObject(exhibit_photo_bucket_name, fileName);
console.log('已删除Markdown文件(photo bucket):', fileName);
}
}
}
},
// 删除 Markdown 文件URL
async removeMdFileUrl() {
this.$confirm('确认删除Markdown文件地址')
.then(async () => {
// 调用独立的删除函数
await this.removeMdFileFromMinio(this.formData.md_file_url);
await this.updateAntiqueMdFile("");
this.mdContent = "";
this.$message.success('删除Markdown文件地址成功');
}).catch(() => {});
},
// 更新数据库中的 md_file_url
async updateAntiqueMdFile(md_file_url) {
let antique_data = {
mesum_id: this.mesumSelectedId,
md_file_url: md_file_url
};
const response = await fetch(`${BASE_API_URL}/mesum/antique/update/${this.formData.mesum_id}/${this.formData.id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ragflow-NhZTY5Y2M4YWQ1MzExZWY4Zjc3MDI0Mm'
},
body: JSON.stringify(antique_data)
});
const result = await response.json();
const {data} = result;
if (data && data.update) {
this.formData.md_file_url = md_file_url;
this.triggerRefreshAntiques();
return true;
}
return false;
},
// 生成 Markdown TTS
async generateMdTTS() {
if (!this.mdContent || this.mdContent.length === 0) {
this.$message.warning('Markdown内容为空无法生成语音');
return;
}
this.downloadFilename_md = 'md_tts_' + window.pinyin.getFullChars(this.formData.label) + '.mp3';
try {
this.isTTSGenerating = true;
this.preview.audioSrc_md = null;
this.preview.downReady_md = false;
const wsUrl = `${WEIXIN_WS_URL}/tts/chats/${chatId}/tts/x-tts-type-is-TextToTts`;
const ws = new WebSocket(wsUrl);
let audioChunks = [];
ws.onopen = () => {
ws.send(JSON.stringify({
service_type: "TextToTts",
text: this.mdContent,
params: {
tts_stream_format: 'mp3',
tts_sample_rate: 22050,
model_name: this.voiceSelected_md,
delay_gen_audio: true
}
}));
};
ws.onmessage = async (event) => {
if (typeof event.data === 'string') {
try {
const message = JSON.parse(event.data);
if (message.error) {
this.showError(message.error);
} else if (message.status === "completed") {
ws.close();
}
} catch (e) {
console.log("Non-JSON message:", event.data);
}
} else {
audioChunks.push(event.data);
console.log("收到音频数据", event.data.size);
}
};
ws.onclose = () => {
this.isTTSGenerating = false;
if (audioChunks.length > 0) {
const blob = new Blob(audioChunks, {type: 'audio/mpeg'});
const url = URL.createObjectURL(blob);
if (this.preview.audioSrc_md) {
URL.revokeObjectURL(this.preview.audioSrc_md);
}
this.preview.audioSrc_md = url;
this.preview.tempBlob_md = blob;
this.preview.downReady_md = true;
this.preview.visible_md = false;
}
};
ws.onerror = (error) => {
this.isTTSGenerating = false;
this.showError("WebSocket连接错误: " + error.message);
};
} catch (error) {
this.isTTSGenerating = false;
this.showError(error.message);
}
},
// 下载 Markdown TTS
downloadMdTTS() {
this.downloadFile(this.preview.audioSrc_md, this.downloadFilename_md);
},
downloadFile(content, filename) {
let blob = this.preview.tempBlob_md;
this.downloadStateMd = "download";
if (!blob) {
this.showError("没有可下载的音频");
return;
}
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
document.body.removeChild(a);
this.preview.visible_md = true;
this.downloadStateMd = "finished";
this.resetDownloadState();
},
// 音色选择变化
// eslint-disable-next-line no-unused-vars
voiceSelectedChangeMd(value) {
this.preview.visible_md = false;
this.preview.audioSrc_md = "";
},
// 处理上传文件变化
handleUploadChangeMd(file) {
console.log(file);
this.uploadFile_md = file.raw;
},
// 提交 TTS 上传
submitMdTTSUpload() {
const formData = new FormData();
formData.append('file', this.uploadFile_md);
formData.append('bucket', exhibit_tts_bucket_name);
formData.append('file_name', `${this.photo_prefix}/${this.uploadFileName_md}`);
fetch(`${BASE_API_URL}/minio/put`, {
method: 'POST',
body: formData,
headers: {
"Authorization": "Bearer ragflow-NhZTY5Y2M4YWQ1MzExZWY4Zjc3MDI0Mm"
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(res => {
const {data} = res;
if (data?.put) {
this.updateAntiqueMdTts(data?.url);
this.$message.success('上传成功');
}
})
.catch(error => {
console.error('上传失败:', error);
this.$message.error('上传失败');
});
},
// 删除 Markdown TTS URL
async removeMdTtsUrl() {
this.$confirm('确认删除Markdown TTS音频地址')
.then(async () => {
if (this.formData.md_tts_url && this.formData.md_tts_url !== '') {
const urlParts = this.formData.md_tts_url.split(`${exhibit_tts_bucket_name}/`);
if (urlParts.length > 1) {
const fileName = urlParts[1];
if (fileName && fileName !== "") {
await this.removeMinioObject(exhibit_tts_bucket_name, fileName);
}
}
}
if (await this.updateAntiqueMdTts("")) {
this.$message('删除音频地址成功');
} else {
this.$message('删除音频地址失败');
}
}).catch(() => {
});
},
// 更新数据库中的 md_tts_url
async updateAntiqueMdTts(tts_url) {
let antique_data = {
mesum_id: this.mesumSelectedId,
md_tts_url: tts_url
};
const response = await fetch(`${BASE_API_URL}/mesum/antique/update/${this.formData.mesum_id}/${this.formData.id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ragflow-NhZTY5Y2M4YWQ1MzExZWY4Zjc3MDI0Mm'
},
body: JSON.stringify(antique_data)
});
const result = await response.json();
const {data} = result;
if (data && data.update) {
this.formData.md_tts_url = tts_url;
this.triggerRefreshAntiques();
return true;
}
return false;
},
// 删除 MinIO 对象
async removeMinioObject(bucket, fileName) {
fetch(`${BASE_API_URL}/minio/rm`, {
method: 'POST',
body: JSON.stringify({
bucket: bucket,
file_name: fileName
}),
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer ragflow-NhZTY5Y2M4YWQ1MzExZWY4Zjc3MDI0Mm"
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(res => {
const {data} = res;
if (data?.rm) {
console.log('删除MinIO对象成功');
}
})
.catch(error => {
console.error('删除minio object 失败:', error);
});
},
resetDownloadState() {
setTimeout(() => {
this.downloadStateMd = 'idle';
}, 3000);
},
showError(message) {
this.result.error = message;
this.result.success = false;
setTimeout(() => this.result.error = null, 5000);
},
handleDialogClose(done) {
const audioMd = this.$refs.audioPlayerMd;
if (audioMd) {
audioMd.pause();
}
done();
this.closeDialog();
},
closeDialog() {
this.dialogVisible = false;
this.$emit("update:close", true);
},
triggerRefreshAntiques() {
this.$emit("update:refresh", this.formData.id);
}
},
watch: {
visible: {
handler(newVal) {
this.dialogVisible = newVal;
if (newVal) {
this.formData = {...this.formData, ...this.initData};
this.mdContent = "";
this.mdFile = null;
this.downloadStateMd = 'idle';
this.downloadFilename_md = 'md_tts_' + window.pinyin.getFullChars(this.formData.label || 'default') + '.mp3';
}
console.log("watch visible", newVal, this.initData);
},
immediate: true
},
mesumSelectedId: {
handler(newVal) {
console.log("watch mesumSelectedId", newVal);
if (newVal == 2) {
this.voiceSelected_md = "sambert-zhichu-v1@Tongyi-Qianwen";
} else if (newVal == 1) {
this.voiceSelected_md = "cosyvoice-v1/longyuan@Tongyi-Qianwen";
} else {
this.voiceSelected_md = "sambert-zhichu-v1@Tongyi-Qianwen";
}
},
immediate: true
}
},
computed: {
uploadFileName_md() {
if (this.uploadFile_md && this.uploadFile_md.name)
return this.uploadFile_md.name;
else
return "";
},
downloadPromptMd() {
if (this.downloadStateMd === 'idle') return "";
if (this.downloadStateMd === 'download') return "下载中...";
if (this.downloadStateMd === 'save') return "存储中...";
if (this.downloadStateMd === 'finished') return "完成";
return "";
}
}
}
</script>
<style scoped>
.dialog-footer {
display: flex;
justify-content: flex-end;
}
.el-dialog__header {
background: #f0f9eb !important;
padding: 2px 2px !important;
border-bottom: 1px solid #ebeef5;
}
.el-dialog__title {
margin: 0;
padding: 0;
color: #67C23A !important;
font-size: 16px !important;
}
.el-dialog__headerbtn {
margin-top: 3px !important;
top: 2px !important;
}
.form-group {
padding: 5px;
margin-bottom: 5px;
border-bottom: 2px solid #eee;
}
label {
display: block;
margin-bottom: 5px;
}
input, select, textarea {
width: 100%;
padding: 8px;
}
button {
padding: 10px 20px;
margin: 5px;
cursor: pointer;
}
.audio-player {
margin: 20px 0;
height: 40px
}
.error-message {
color: red;
margin-top: 10px;
}
.success-message {
color: green;
margin-top: 10px;
}
.upload-section {
margin-top: 20px;
border-top: 2px solid #eee;
padding-top: 20px;
height: 10vh
}
</style>

View File

@@ -0,0 +1,356 @@
<template>
<div class="content-container">
<div class="title" ref="refTitle">
<div class="flex-row">
<el-select v-model="museumCategorySelectedId" clearable placeholder="请选择目录"
@change="mesumCategorySelectedChange" style="width: 140px" >
<el-option
v-for="item in mesumCategoryOptions"
:key="item.id"
:label="item.label"
:value="item.id">
</el-option>
</el-select>
</div>
<el-input placeholder="请输入标题搜索" v-model="labelFilter" clearable style="width: 200px;" >
<template #append>
<el-button icon="el-icon-search" @click="filterByLabel"></el-button>
</template>
</el-input>
<h3>用户订阅管理</h3>
<span>总金额:{{totalAmount}}</span>
<div class="flex-row">
<el-select v-model="mesumSelectedId" clearable placeholder="请选择博物馆" @change="museumSelectedChange"
default-first-option style="width: 140px">
<el-option
v-for="item in mesumOptions"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
</div>
</div>
<el-row >
<el-col :span="24">
<el-table
:data="subscriptionsFiltered"
border
:height="tableHeight"
highlight-current-row
style="width: 100%"
@current-change="handleCurrentChange">
<el-table-column
prop="id"
label="编号"
width="60">
</el-table-column>
<el-table-column
prop="openid"
label="openid"
width="280">
</el-table-column>
<el-table-column
prop="phone"
label="phone"
width="150">
</el-table-column>
<el-table-column
prop="template_name"
label="订阅项目"
width="150">
</el-table-column>
<el-table-column
prop="price"
label="金额"
width="100">
</el-table-column>
<el-table-column
prop="template_validity_type"
label="类型"
width="100">
</el-table-column>
<el-table-column
prop="museum_name"
label="场地(所)名称"
width="150">
</el-table-column>
<el-table-column
prop="start_date"
label="开始时间"
width="200">
</el-table-column>
<el-table-column
prop="end_date"
label="失效时间"
width="200">
</el-table-column>
<el-table-column
prop="order_id"
label="商户订单号"
width="120">
</el-table-column>
<el-table-column
prop="transaction_id"
label="微信支付单号"
width="140">
</el-table-column>
<el-table-column
prop="template_description"
label="说明"
width="300">
</el-table-column>
<el-table-column
fixed="right"
label="操作"
width="100">
<!--
<template #default="scope">
<div style="display: flex;flex-direction: row;align-items: center;justify-content: space-between">
<el-button type="primary" size="small" @click="editAntiqueTrigger(scope.row)" >编辑</el-button>
<el-button type="danger" size="small" @click="removeAntiqueInServer(scope.row)">删除</el-button>
</div>
</template>
-->
</el-table-column>
</el-table>
</el-col>
</el-row>
</div>
</template>
<script>
import {api_getSubscriptions } from "@/API/api.js"
export default {
name: "MuseumSubscriptions",
props: {'museum_authed':{default:[]}, 'resource':{},'mode':{},'antiqueListData':{default:[]}},
inject:{'root':{default:null},
'getProject':{default:null},'getSchedule':{default:null}},
components:{
},
data() {
return {
form: {
action:"update",
mesum_id:1,
id:"",
text: '',
label:"",
category:"",
voiceType: 'zh_male_ruyaqingnian_mars_bigtts',
ttsUrl_cn:"",
photoFileObject:null,
formTitle:"修改讲解点",
},
upload: {
minioPath: 'mp3',
isUploading: false
},
uploadFile_adult:null,
uploadFile_child:null,
mesumOptions:[ ],
mesumCategoryOptions:[],
museumSubscriptions:[],
antiqueSelectedRow:null,
dialogVisible:false,
mesumSelectedId:null,
museumSelected:{id:1},
museumCategorySelectedId:null,
currentDeviceList:[],
deviceSelected:null,
currentDeviceLogList:[],
labelFilter:"",
enableLabelFilter:false,
tableHeight:200,
AiRefineDialogVisible:false,
aiRefineForm: {
id:"",
text: '',
aiOriginAntiqueDetailText:""
},
labelToRefine:"",
//
};
},
created() {
},
async mounted() {
await this.ConstructMuseumOptions(this.museum_authed);
await this.getMuseumSubscriptions(this.museumSelected);
// 初始获取高度
this.updateTableHeight()
// 监听窗口大小变化
window.addEventListener('resize', this.updateTableHeight)
},
beforeUnmount() {
// 组件销毁时移除监听
window.removeEventListener('resize', this.updateTableHeight)
},
methods: {
// eslint-disable-next-line no-unused-vars
async mesumCategorySelectedChange(_value){
this.enableLabelFilter = false
},
filterByLabel(){
this.enableLabelFilter = true
},
async museumSelectedChange(value){
this.form.mesum_id = value
this.museumSelected = this.mesumOptions.find((item)=>{
return item.id === this.mesumSelectedId
})
await this.getMuseumSubscriptions(this.museumSelected);
},
handleCurrentChange(val) {
this.antiqueSelectedRow = val;
// eslint-disable-next-line no-empty
if(this.antiqueSelectedRow){
}
},
// eslint-disable-next-line no-unused-vars
triggerAiRefine(row){
if(!row.orgin_text || row.orgin_text==""){
this.$message.error("没有找到原始的解说词,或者原始解说词为空");
return
}
this.aiRefineForm.aiOriginAntiqueDetailText = row.orgin_text
this.aiRefineForm.id = row.id
this.aiRefineForm.label = row.label
this.aiRefineForm.mesum_id = this.mesumSelectedId;
this.AiRefineDialogVisible = true;
},
async insertAntiqueTrigger(){
},
// eslint-disable-next-line no-unused-vars
editAntiqueTrigger(row){
this.form.formTitle = "修改讲解点"
this.uploadFile_adult = null
this.uploadFile_child = null
this.form.action='update'
this.form.text = row.combined;
this.form.label = row.label;
this.form.category = row.category;
this.form.id = row.id;
this.form.ttsUrl_adult = row.ttsUrl_adult
this.form.ttsUrl_child = row.ttsUrl_child
this.form.photo_url = row.photo_url
this.form.photoFileObject=null
this.dialogVisible = true
console.log("editAntiqueTrigger")
},
// eslint-disable-next-line no-unused-vars
removeAntiqueInServer(row){
},
updateTableHeight() {
const titleRect= this.$refs.refTitle.getBoundingClientRect()
this.tableHeight = window.innerHeight-titleRect.bottom-16
},
async ConstructMuseumOptions(data){
if(data && Array.isArray(data) && data.length>=1){
this.mesumOptions = []
this.mesumOptions = data
this.museumSelected = data[0]
this.mesumSelectedId = data[0].id
this.form.mesum_id = data[0].id
}
console.log(data)
},
async getMuseumSubscriptions(museum){
// eslint-disable-next-line no-prototype-builtins
if(museum && museum.hasOwnProperty('id')) {
const data = await api_getSubscriptions(museum.id);
console.log("api_getSubscriptions ", data)
if (data) {
console.log(data)
const len = this.museumSubscriptions.length;
this.museumSubscriptions.splice(0, len)
this.museumSubscriptions.splice(0, 0, ...data);
console.log(this.museumSubscriptions)
}
}
}
},
watch:{
},
computed: {
rowSelected(){
return (row)=>{
if(this.antiqueSelectedRow && (this.antiqueSelectedRow.id==row.id))
return true;
else
return false
}
},
ttsUrl_valid(){
return (url)=>{
if(url && url!=='')
return true
else
return false
}
},
subscriptionsFiltered(){
if(this.enableLabelFilter){
if(this.labelFilter && this.labelFilter!=='') {
return this.museumSubscriptions.filter((item)=>{
return item.label.includes(this.labelFilter)
})
}else
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.enableLabelFilter = false
}
if(this.museumCategorySelectedId == null )
return this.museumSubscriptions;
else if(this.museumCategorySelectedId == (this.mesumCategoryOptions.length-1))
{
return this.museumSubscriptions;
}
else{
console.log("categoryId",this.museumCategorySelectedId)
return this.museumSubscriptions.filter((item)=>{
return item.category === this.mesumCategoryOptions[this.museumCategorySelectedId].label
})
}
},
totalAmount(){
return parseFloat(this.subscriptionsFiltered.reduce((total, item) => total + Number(item.price), 0).toFixed(2));
}
}
}
</script>
<style scoped>
.content-container{
width: 88vw;
height: 97vh;
box-sizing: border-box;
overflow-y: hidden;
}
.title{width:100%;display: flex;align-items: center;justify-content: space-between;}
.flex-row { display: flex; gap: 15px; align-items: center; }
.form-group { padding: 10px; margin-bottom: 5px;border-bottom: 2px solid #eee; }
label { display: block; margin-bottom: 5px; }
input, select, textarea { width: 100%; padding: 8px; }
button { padding: 10px 20px; margin:5px; cursor: pointer; }
.audio-player { margin: 20px 0; height:40px}
</style>

133
src/components/NaviMenu.vue Normal file
View File

@@ -0,0 +1,133 @@
<template>
<div class="navi-menu">
<div style="width:93%;height:5vh;position:absolute;top:0;border-bottom:1px solid lightgray">
</div>
<el-menu
:default-active="defaultActive"
@open="handleMenuOpen"
@select="handleMenuSelect"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
mode ="vertical"
style="top:7vh;width:9vw;position: absolute;border-right: none">
<el-menu-item v-if="hasDataAdminMenu" index="1" >
<span style="margin-right: 16px">&#x1F4D6;</span>
<!-- 修改这里 -->
<template #title>
<span>讲解词管理</span>
</template>
</el-menu-item>
<el-menu-item v-if="hasDeviceMenu" index="2">
<span style="margin-right: 20px">&#x1F4F1;</span>
<template #title>
<span>设备在线</span>
</template>
</el-menu-item>
<el-menu-item v-if="hasLocationMenu" index="3" >
<span style="margin-right: 20px">&#x1F3DB;</span>
<template #title>
<span>场所管理</span>
</template>
</el-menu-item>
<el-menu-item v-if="hasSubscriptionsMenu" index="4" >
<span style="margin-right: 20px">&#x1F3DB;</span>
<template #title>
<span>订阅管理</span>
</template>
</el-menu-item>
</el-menu>
<div style="width:93%;height:5vh;position:absolute;bottom:0;border-top:1px solid lightgray">
</div>
</div>
</template>
<script>
export default {
name: "NaviMenu",
props: {'task':{default:null}, 'resource':{},'mode':{},'base':{default:'jira'},'menu_authed':[],
'defaultIndex': {default:'1'}},
inject:{'root':{default:null},
'getProject':{default:null},'getSchedule':{default:null}},
data() {
return {
activeNaviMenuIndex:'1',
defaultActive:'1',
};
},
created() {
},
mounted() {
},
methods: {
// eslint-disable-next-line no-unused-vars
handleMenuOpen(key, keyPath){
},
// eslint-disable-next-line no-unused-vars
handleMenuSelect(key, keyPath){
this.$emit("navi-menu-select",key)
this.activeNaviMenuIndex = key
if(key=='1'){
this.$nextTick(()=>{
//this.updateTableHeight()
})
}
}
},
watch:{
defaultIndex: {
handler(newVal) {
console.log("NaviMenu watch defaultIndex",newVal)
this.defaultActive = newVal
},
immediate: true
},
},
computed: {
hasSubscriptionsMenu(){
if(this.menu_authed && this.menu_authed.includes('get_subscriptions'))
return true;
else
return false
},
hasDataAdminMenu(){
if(this.menu_authed && this.menu_authed.includes('data_admin'))
return true;
else
return false
},
hasLocationMenu(){
if(this.menu_authed && this.menu_authed.includes('location_admin'))
return true;
else
return false
},
hasDeviceMenu(){
if(this.menu_authed && this.menu_authed.includes('device_admin'))
return true;
else
return false
}
}
}
</script>
<style scoped>
.navi-menu{
flex-shrink: unset;
width: 10vw;
height: 97vh;
margin: 0;
padding: 0;
box-sizing: border-box;
position: relative;
border-right: 1px solid lightgray;
background: #545C64;
}
</style>

View File

@@ -0,0 +1,128 @@
<template>
<div>
<el-dialog v-model="dialogVisible" width="20%" center custom-class="loginDialog" :show-close="false">
<div style="text-align: center;width: 100%">
<h3>华小博智能导览管理系统</h3>
</div>
<el-form :model="form" ref="loginForm" autocomplete="off" label-width="80px">
<el-form-item label="用户名" prop="username" >
<!--
<el-input v-model="form.username" autocomplete="off"></el-input>
-->
<el-select
v-model="form.username" filterable allow-create default-first-option placeholder="请输入用户名">
<el-option
v-for="item in availableUser"
:key="item.value"
:label="item.name"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="密码" prop="password" >
<el-input v-model="form.password" type="text" class="pwd">
<template #suffix>
<i class="el-icon-view" @click="hiddenPwd"></i>
</template>
</el-input>
</el-form-item>
<el-form-item >
<div style="width:100%;display:flex;align-items: center;align-content: center;justify-content: center;">
<el-button type="primary" @click="handleLogin">登录</el-button>
<el-button @click="cancelLogin">取消</el-button>
</div>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
import {account_login } from "@/API/api.js"
export default {
props:{'loginDialogVisible':{default:true},
'initData':{username:"",password: ""},
},
data() {
return {
dialogVisible: false,
form: {
username: '',
password: '',
},
availableUser:[ {name:'语料管理',value:'15901055018'},
{name:'纪晓岚馆方',value:'19967851231'},
{name:'系统管理员',value:'18676776176'}],
passwordinputtype:'text',
showPwd:false
}
},
methods: {
cancelLogin(){
this.$emit('login-event',"cancel-login",null)
},
handleLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
account_login({user:this.form.username,password:this.form.password}).then(response => {
response = response.data || response;
if(response.status === 'success') {
this.$emit("confirm-login",response)
this.$message({type: 'success', message: '登录成功!'})
} else {
this.$message({type: 'error', message: '用户名或者密码错误!'});
}
});
}
})
},
hiddenPwd() {
this.showPwd = !this.showPwd // 默认为false
if (this.showPwd) {
document.getElementsByClassName('pwd')[0].style.webkitTextSecurity = 'none'
} else {
document.getElementsByClassName('pwd')[0].style.webkitTextSecurity = 'disc'
}
}
},
watch:{
loginDialogVisible: {
handler(newVal) {
console.log("UserLogin watch loginDialogVisible",newVal)
this.dialogVisible = newVal
if(newVal){
this.formData= {...this.formData,...this.initData}
this.downloadStateAdult = 'idle'
this.downloadStateChild = 'idle'
}
},
immediate: true
},
},
mounted() {
},
created() {
console.log("user login created")
}
}
</script>
<style>
.loginDialog .el-dialog__body {
padding: 2px 2px 2px 2px;
}
.pwd {
-webkit-text-security: disc;
}
/deep/.el-input__suffix {
-webkit-text-security: none;
}
</style>

View File

@@ -0,0 +1,518 @@
<template>
<div class="content-container">
<div class="title" ref="refTitle">
<div class="flex-row">
<el-select
v-model="museumCategorySelectedId"
clearable
placeholder="请选择目录"
@change="mesumCategorySelectedChange"
style="width: 140px"
>
<el-option
v-for="item in mesumCategoryOptions"
:key="item.id"
:label="item.label"
:value="item.id"
/>
</el-select>
</div>
<el-input
placeholder="请输入标题搜索"
v-model="labelFilter"
clearable
style="width: 200px"
>
<template #append>
<el-button icon="el-icon-search" @click="filterByLabel"></el-button>
</template>
</el-input>
<h3>讲解点查看与修改</h3>
<span>{{ antiqueFiltered.length }}</span>
<el-button round type="primary" size="medium" @click="insertAntiqueTrigger"
>增加</el-button
>
<div class="flex-row">
<el-select
v-model="mesumSelectedId"
clearable
placeholder="请选择博物馆"
@change="mesumSelectedChange"
default-first-option
style="width: 140px"
>
<el-option
v-for="item in mesumOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</div>
</div>
<el-row>
<el-col :span="24">
<el-table
:data="antiqueFiltered"
border
:height="tableHeight"
highlight-current-row
style="width: 100%"
@current-change="handleCurrentChange"
>
<el-table-column prop="id" label="编号" width="100" />
<el-table-column prop="label" label="标题" width="150" />
<el-table-column prop="category" label="目录" width="260" />
<el-table-column prop="combined" label="解说词" width="350">
<template #default="scope">
<div
style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
"
>
<el-popover
placement="top-start"
title="详细内容"
width="600"
trigger="click"
:content="scope.row.combined"
>
<template #reference>
<div>{{ antiqueText(scope.row) }}</div>
</template>
</el-popover>
<div v-if="orginTextGT400(scope.row)">
<el-button
v-if="scope.row.combined.length > 400"
size="small"
@click="triggerAiRefine(scope.row)"
style="margin-top: 10px; border: 1px solid lightblue"
>
{{ `${scope.row.combined.length}大于400字-->AI整理` }}
</el-button>
<span
v-if="
scope.row.orgin_text &&
Math.abs(
scope.row.orgin_text.length - scope.row.combined.length
) > 20
"
style="color: blue"
>经过整理</span
>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="ttsUrl_adult" label="音频地址(成年)" width="300">
<template #default="scope">
<div
style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
"
>
<div>{{ scope.row.ttsUrl_adult }}</div>
</div>
<audio
v-if="
rowSelected(scope.row) && ttsUrl_valid(scope.row.ttsUrl_adult)
"
class="audio-player"
:src="scope.row.ttsUrl_adult"
controls
></audio>
</template>
</el-table-column>
<el-table-column prop="ttsUrl_child" label="音频地址(童声)" width="300">
<template #default="scope">
<div
style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
"
>
<div>{{ scope.row.ttsUrl_child }}</div>
</div>
<audio
v-if="
rowSelected(scope.row) && ttsUrl_valid(scope.row.ttsUrl_child)
"
class="audio-player"
:src="scope.row.ttsUrl_child"
controls
></audio>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="180">
<template #default="scope">
<div
style="
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
"
>
<el-button
type="primary"
size="small"
@click="editAntiqueTrigger(scope.row)"
>编辑</el-button
>
<el-button
type="danger"
size="small"
@click="removeAntiqueInServer(scope.row)"
>删除</el-button
>
</div>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
<detail-editor
:visible="dialogVisible"
:init-data="form"
:mesum-selected-id="mesumSelectedId"
:photo_prefix="mesumSelected.photo_prefix"
@update:close="dialogVisible = false"
@update:refresh="getMesumAntiques"
></detail-editor>
<ai-text-editor
:visible="AiRefineDialogVisible"
:init-data="aiRefineForm"
:mesum-selected-id="mesumSelectedId"
:photo_prefix="mesumSelected.photo_prefix"
@update:close="AiRefineDialogVisible = false"
@update:refresh="getMesumAntiques"
></ai-text-editor>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
import { api_getMesumList, api_getMesumAntiques } from "@/API/api.js";
import DetailEditor from "@/components/DetailEditor.vue";
import AiTextEditor from "@/components/AiTextEditor.vue";
interface MesumOption {
id: number;
name: string;
photo_prefix?: string;
}
interface CategoryOption {
id: number;
label: string;
}
interface AntiqueItem {
id: string;
label: string;
category: string;
combined: string;
orgin_text?: string;
ttsUrl_adult: string;
ttsUrl_child: string;
photo_url?: string;
}
// Props
const props = defineProps<{
museumListData?: Array<any>;
resource?: any;
mode?: any;
antiqueListData?: Array<any>;
}>();
// Inject
const root = inject("root", null);
const getProject = inject("getProject", null);
const getSchedule = inject("getSchedule", null);
// Refs
const refTitle = ref<HTMLElement | null>(null);
const museumCategorySelectedId = ref<number | null>(null);
const mesumOptions = ref<MesumOption[]>([]);
const mesumCategoryOptions = ref<CategoryOption[]>([]);
const mesum_antiques = ref<AntiqueItem[]>([]);
const antiqueSelectedRow = ref<AntiqueItem | null>(null);
const dialogVisible = ref(false);
const mesumSelectedId = ref<number | null>(null);
const mesumSelected = ref<MesumOption>({ id: 1, name: "" });
const labelFilter = ref("");
const enableLabelFilter = ref(false);
const tableHeight = ref(200);
const AiRefineDialogVisible = ref(false);
// Form data
const form = ref({
action: "update",
mesum_id: 1,
id: "",
text: "",
label: "",
category: "",
voiceType: "zh_male_ruyaqingnian_mars_bigtts",
ttsUrl_cn: "",
photoFileObject: null,
formTitle: "修改讲解点",
ttsUrl_adult: "",
ttsUrl_child: "",
photo_url: "",
});
const upload = ref({
minioPath: "mp3",
isUploading: false,
});
const uploadFile_adult = ref(null);
const uploadFile_child = ref(null);
const aiRefineForm = ref({
id: "",
text: "",
aiOriginAntiqueDetailText: "",
label: "",
mesum_id: null as number | null,
});
const labelToRefine = ref("");
// Computed properties
const antiqueFiltered = computed(() => {
if (enableLabelFilter.value) {
if (labelFilter.value && labelFilter.value !== "") {
return mesum_antiques.value.filter((item) => {
return item.label.includes(labelFilter.value);
});
} else {
enableLabelFilter.value = false;
}
}
if (museumCategorySelectedId.value === null) return mesum_antiques.value;
if (
museumCategorySelectedId.value ===
mesumCategoryOptions.value.length - 1
) {
return mesum_antiques.value;
} else {
return mesum_antiques.value.filter((item) => {
return (
item.category ===
mesumCategoryOptions.value[museumCategorySelectedId.value as number]
.label
);
});
}
});
// Methods
const rowSelected = (row: AntiqueItem) => {
return antiqueSelectedRow.value && antiqueSelectedRow.value.id === row.id;
};
const ttsUrl_valid = (url: string) => {
return url && url !== "";
};
const deviceIsSelected = (item: any) => {
return item === antiqueSelectedRow.value?.id;
};
const antiqueText = (row: AntiqueItem) => {
const text = row.combined || "";
if (text.length < 20) return text;
else return text.substring(0, 20) + "...";
};
const orginTextGT400 = (row: AntiqueItem) => {
let result = false;
if (!row.orgin_text) return false;
if (row && row.combined.length > 400) result = true;
if (
row.orgin_text &&
row.combined &&
row.orgin_text.length !== row.combined.length
)
result = true;
return result;
};
const mesumCategorySelectedChange = (value: number) => {
enableLabelFilter.value = false;
};
const filterByLabel = () => {
enableLabelFilter.value = true;
};
const mesumSelectedChange = (value: number) => {
form.value.mesum_id = value;
mesumSelected.value =
mesumOptions.value.find((item) => item.id === mesumSelectedId.value) ||
mesumOptions.value[0];
getMesumAntiques(true);
};
const handleCurrentChange = (val: AntiqueItem) => {
antiqueSelectedRow.value = val;
};
const triggerAiRefine = (row: AntiqueItem) => {
if (!row.orgin_text || row.orgin_text == "") {
ElMessage.error("没有找到原始的解说词,或者原始解说词为空");
return;
}
aiRefineForm.value.aiOriginAntiqueDetailText = row.orgin_text;
aiRefineForm.value.id = row.id;
aiRefineForm.value.label = row.label;
aiRefineForm.value.mesum_id = mesumSelectedId.value;
AiRefineDialogVisible.value = true;
};
const insertAntiqueTrigger = () => {
form.value.formTitle = "新增讲解点";
form.value.action = "insert";
form.value.text = "";
form.value.label = "";
form.value.category = "";
form.value.id = "";
dialogVisible.value = true;
};
const editAntiqueTrigger = (row: AntiqueItem) => {
form.value.formTitle = "修改讲解点";
uploadFile_adult.value = null;
uploadFile_child.value = null;
form.value.action = "update";
form.value.text = row.combined;
form.value.label = row.label;
form.value.category = row.category;
form.value.id = row.id;
form.value.ttsUrl_adult = row.ttsUrl_adult;
form.value.ttsUrl_child = row.ttsUrl_child;
form.value.photo_url = row.photo_url;
form.value.photoFileObject = null;
dialogVisible.value = true;
};
const removeAntiqueInServer = (row: AntiqueItem) => {
// 删除逻辑实现
};
const updateTableHeight = () => {
if (refTitle.value) {
const titleRect = refTitle.value.getBoundingClientRect();
tableHeight.value = window.innerHeight - titleRect.bottom - 16;
}
};
const getMesumList = async () => {
const data = await api_getMesumList();
if (data && Array.isArray(data) && data.length >= 1) {
mesumOptions.value = data;
mesumSelected.value = data[0];
mesumSelectedId.value = data[0].id;
form.value.mesum_id = data[0].id;
}
};
const getMesumAntiques = async (updateCategory: boolean) => {
if (!mesumSelectedId.value) return;
const data = await api_getMesumAntiques(mesumSelectedId.value);
if (data) {
mesum_antiques.value = data["anqituqes"] || [];
mesumCategoryOptions.value = [];
if (data["categories"] && Array.isArray(data["categories"])) {
const options = data["categories"].map((name: string, index: number) => {
return { id: index, label: name };
});
options.push({ id: options.length, label: "全部" });
mesumCategoryOptions.value = options;
if (options.length && updateCategory) {
museumCategorySelectedId.value = mesumCategoryOptions.value[0].id;
}
}
}
};
// Lifecycle hooks
onMounted(async () => {
await getMesumList();
await getMesumAntiques(true);
updateTableHeight();
window.addEventListener("resize", updateTableHeight);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", updateTableHeight);
});
</script>
<style scoped>
.content-container {
width: 88vw;
height: 97vh;
box-sizing: border-box;
overflow-y: hidden;
}
.title {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.flex-row {
display: flex;
gap: 15px;
align-items: center;
}
.form-group {
padding: 10px;
margin-bottom: 5px;
border-bottom: 2px solid #eee;
}
label {
display: block;
margin-bottom: 5px;
}
input,
select,
textarea {
width: 100%;
padding: 8px;
}
button {
padding: 10px 20px;
margin: 5px;
cursor: pointer;
}
.audio-player {
margin: 20px 0;
height: 40px;
}
</style>

View File

@@ -1,4 +1,15 @@
import { createApp } from 'vue'
import { createApp} from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
createApp(App).mount('#app')
const app = createApp(App)
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(ElementPlus)
app.mount('#app')

2091
yarn.lock

File diff suppressed because it is too large Load Diff