增加了markdown文件的上传和相关参数的修改
This commit is contained in:
673
add_1114_to_minio_file.py
Normal file
673
add_1114_to_minio_file.py
Normal 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图片语法 
|
||||
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()
|
||||
1
demo/2.15.14_lib_index.js
Normal file
1
demo/2.15.14_lib_index.js
Normal file
File diff suppressed because one or more lines are too long
124
demo/Convert_Pinyin.js
Normal file
124
demo/Convert_Pinyin.js
Normal file
File diff suppressed because one or more lines are too long
1
demo/element-ui@2.15.12/lib/index.js
Normal file
1
demo/element-ui@2.15.12/lib/index.js
Normal file
File diff suppressed because one or more lines are too long
BIN
demo/element-ui@2.15.12/lib/theme-chalk/fonts/element-icons.ttf
Normal file
BIN
demo/element-ui@2.15.12/lib/theme-chalk/fonts/element-icons.ttf
Normal file
Binary file not shown.
BIN
demo/element-ui@2.15.12/lib/theme-chalk/fonts/element-icons.woff
Normal file
BIN
demo/element-ui@2.15.12/lib/theme-chalk/fonts/element-icons.woff
Normal file
Binary file not shown.
1
demo/element-ui@2.15.12/lib/theme-chalk/index.css
Normal file
1
demo/element-ui@2.15.12/lib/theme-chalk/index.css
Normal file
File diff suppressed because one or more lines are too long
BIN
demo/favicon.ico
Normal file
BIN
demo/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
1106
demo/js/app.js
Normal file
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
16699
demo/js/chunk-vendors.js
Normal file
File diff suppressed because one or more lines are too long
19
demo/mesum_antique_vue.html
Normal file
19
demo/mesum_antique_vue.html
Normal 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>
|
||||
1
demo/theme-chalk_index.css
Normal file
1
demo/theme-chalk_index.css
Normal file
File diff suppressed because one or more lines are too long
11765
package-lock.json
generated
Normal file
11765
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
100
src/API/api.js
Normal 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;
|
||||
}
|
||||
127
src/App.vue
127
src/App.vue
@@ -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>
|
||||
|
||||
0
src/add_1114_to_minio_file.py
Normal file
0
src/add_1114_to_minio_file.py
Normal file
222
src/components/AiTextEditor.vue
Normal file
222
src/components/AiTextEditor.vue
Normal 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>
|
||||
1209
src/components/DetailEditor.vue
Normal file
1209
src/components/DetailEditor.vue
Normal file
File diff suppressed because it is too large
Load Diff
251
src/components/DeviceMonitor.vue
Normal file
251
src/components/DeviceMonitor.vue
Normal 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>
|
||||
231
src/components/DynamicAudioWave.vue
Normal file
231
src/components/DynamicAudioWave.vue
Normal 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>
|
||||
819
src/components/ExhibitDetail.vue
Normal file
819
src/components/ExhibitDetail.vue
Normal 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>
|
||||
243
src/components/LocationManager.vue
Normal file
243
src/components/LocationManager.vue
Normal 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>
|
||||
|
||||
885
src/components/MarkdownEditor.vue
Normal file
885
src/components/MarkdownEditor.vue
Normal 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>
|
||||
356
src/components/MuseumSubscriptions.vue
Normal file
356
src/components/MuseumSubscriptions.vue
Normal 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
133
src/components/NaviMenu.vue
Normal 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">📖</span>
|
||||
<!-- 修改这里 -->
|
||||
<template #title>
|
||||
<span>讲解词管理</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="hasDeviceMenu" index="2">
|
||||
<span style="margin-right: 20px">📱</span>
|
||||
<template #title>
|
||||
<span>设备在线</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="hasLocationMenu" index="3" >
|
||||
<span style="margin-right: 20px">🏛</span>
|
||||
<template #title>
|
||||
<span>场所管理</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="hasSubscriptionsMenu" index="4" >
|
||||
<span style="margin-right: 20px">🏛</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>
|
||||
128
src/components/UserLogin.vue
Normal file
128
src/components/UserLogin.vue
Normal 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>
|
||||
|
||||
518
src/components/vue3/ExhibitDetail_vue3.vue
Normal file
518
src/components/vue3/ExhibitDetail_vue3.vue
Normal 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>
|
||||
15
src/main.js
15
src/main.js
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user