|
|
<!DOCTYPE html> |
|
|
<html lang="zh-CN"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>OpenWebUI 模型头像URL修正工具 | 精致美化版</title> |
|
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
|
<style> |
|
|
|
|
|
:root { |
|
|
--primary-color: #4A6CF7; |
|
|
--primary-hover: #3a5af5; |
|
|
--secondary-color: #6c757d; |
|
|
--success-color: #10b981; |
|
|
--warning-color: #f59e0b; |
|
|
--error-color: #ef4444; |
|
|
--background-color: #f8fafc; |
|
|
--card-bg: #ffffff; |
|
|
--text-color: #1e293b; |
|
|
--light-text: #64748b; |
|
|
--border-color: #e2e8f0; |
|
|
--gap: 20px; |
|
|
--card-padding: 25px; |
|
|
--button-font-size: 14px; |
|
|
--border-radius: 12px; |
|
|
--box-shadow: 0 10px 25px -5px rgba(0,0,0,0.05), 0 10px 10px -5px rgba(0,0,0,0.02); |
|
|
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|
|
--switch-bg: #e2e8f0; |
|
|
--switch-active: var(--primary-color); |
|
|
} |
|
|
|
|
|
|
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif; |
|
|
line-height: 1.6; |
|
|
padding: 20px; |
|
|
max-width: 1200px; |
|
|
margin: 20px auto; |
|
|
background-color: var(--background-color); |
|
|
background-image: linear-gradient(135deg, #f0f4ff 0%, #f8fafc 100%); |
|
|
color: var(--text-color); |
|
|
} |
|
|
|
|
|
|
|
|
.header { |
|
|
text-align: center; |
|
|
margin-bottom: 30px; |
|
|
padding: 20px; |
|
|
} |
|
|
|
|
|
.header h1 { |
|
|
font-size: 2.2rem; |
|
|
font-weight: 700; |
|
|
margin-bottom: 8px; |
|
|
background: linear-gradient(90deg, var(--primary-color), #8b5cf6); |
|
|
-webkit-background-clip: text; |
|
|
-webkit-text-fill-color: transparent; |
|
|
position: relative; |
|
|
display: inline-block; |
|
|
} |
|
|
|
|
|
.header h1::after { |
|
|
content: ''; |
|
|
position: absolute; |
|
|
bottom: -10px; |
|
|
left: 50%; |
|
|
transform: translateX(-50%); |
|
|
width: 80px; |
|
|
height: 4px; |
|
|
background: linear-gradient(90deg, var(--primary-color), #8b5cf6); |
|
|
border-radius: 2px; |
|
|
} |
|
|
|
|
|
.header p { |
|
|
font-size: 1.1rem; |
|
|
color: var(--light-text); |
|
|
max-width: 700px; |
|
|
margin: 0 auto; |
|
|
line-height: 1.7; |
|
|
} |
|
|
|
|
|
|
|
|
.main-tabs { |
|
|
display: flex; |
|
|
gap: 8px; |
|
|
margin-bottom: var(--gap); |
|
|
background: var(--card-bg); |
|
|
border-radius: var(--border-radius); |
|
|
padding: 6px; |
|
|
box-shadow: var(--box-shadow); |
|
|
} |
|
|
|
|
|
.main-tab-button { |
|
|
flex: 1; |
|
|
border: none; |
|
|
background: none; |
|
|
padding: 14px 20px; |
|
|
font-size: 16px; |
|
|
font-weight: 600; |
|
|
color: var(--light-text); |
|
|
cursor: pointer; |
|
|
border-radius: 8px; |
|
|
transition: var(--transition); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
gap: 8px; |
|
|
} |
|
|
|
|
|
.main-tab-button:hover { |
|
|
background: #f1f5f9; |
|
|
color: var(--primary-color); |
|
|
} |
|
|
|
|
|
.main-tab-button.active { |
|
|
background: var(--primary-color); |
|
|
color: white; |
|
|
box-shadow: 0 4px 6px -1px rgba(74, 108, 247, 0.2), 0 2px 4px -1px rgba(74, 108, 247, 0.06); |
|
|
} |
|
|
|
|
|
|
|
|
.tab-content { |
|
|
background-color: var(--card-bg); |
|
|
border-radius: var(--border-radius); |
|
|
box-shadow: var(--box-shadow); |
|
|
padding: var(--card-padding); |
|
|
margin-bottom: var(--gap); |
|
|
border: 1px solid var(--border-color); |
|
|
transition: var(--transition); |
|
|
} |
|
|
|
|
|
.tab-content:hover { |
|
|
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.08), 0 10px 10px -5px rgba(0,0,0,0.03); |
|
|
} |
|
|
|
|
|
.section-title { |
|
|
font-size: 1.3rem; |
|
|
font-weight: 600; |
|
|
margin-bottom: 20px; |
|
|
padding-bottom: 12px; |
|
|
border-bottom: 1px solid var(--border-color); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 10px; |
|
|
color: var(--primary-color); |
|
|
} |
|
|
|
|
|
.section-title i { |
|
|
font-size: 1.2rem; |
|
|
} |
|
|
|
|
|
|
|
|
.upload-section .input-group { |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.upload-section label { |
|
|
font-size: 15px; |
|
|
font-weight: 500; |
|
|
color: var(--text-color); |
|
|
margin-bottom: 10px; |
|
|
display: block; |
|
|
} |
|
|
|
|
|
|
|
|
.file-upload-container { |
|
|
position: relative; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 15px; |
|
|
} |
|
|
|
|
|
.file-upload-btn { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
gap: 10px; |
|
|
padding: 16px 24px; |
|
|
background: var(--primary-color); |
|
|
color: white; |
|
|
border-radius: var(--border-radius); |
|
|
cursor: pointer; |
|
|
transition: var(--transition); |
|
|
border: none; |
|
|
font-size: 16px; |
|
|
font-weight: 600; |
|
|
box-shadow: 0 4px 6px -1px rgba(74, 108, 247, 0.2), 0 2px 4px -1px rgba(74, 108, 247, 0.06); |
|
|
} |
|
|
|
|
|
.file-upload-btn:hover { |
|
|
background: var(--primary-hover); |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 6px 10px -1px rgba(74, 108, 247, 0.3), 0 4px 6px -1px rgba(74, 108, 247, 0.1); |
|
|
} |
|
|
|
|
|
.file-upload-btn i { |
|
|
font-size: 18px; |
|
|
} |
|
|
|
|
|
.file-name { |
|
|
font-size: 14px; |
|
|
padding: 12px 15px; |
|
|
background: #f8fafc; |
|
|
border-radius: var(--border-radius); |
|
|
border: 1px dashed var(--border-color); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.file-name i { |
|
|
color: var(--success-color); |
|
|
} |
|
|
|
|
|
.file-name.empty { |
|
|
color: var(--light-text); |
|
|
} |
|
|
|
|
|
.file-name.empty i { |
|
|
color: var(--light-text); |
|
|
} |
|
|
|
|
|
|
|
|
.options-container { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 15px; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.option-item { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: space-between; |
|
|
padding: 15px; |
|
|
background: #f8fafc; |
|
|
border-radius: var(--border-radius); |
|
|
border: 1px solid var(--border-color); |
|
|
transition: var(--transition); |
|
|
} |
|
|
|
|
|
.option-item:hover { |
|
|
border-color: var(--primary-color); |
|
|
background: #eef2ff; |
|
|
} |
|
|
|
|
|
.option-label { |
|
|
font-size: 15px; |
|
|
font-weight: 500; |
|
|
color: var(--text-color); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 12px; |
|
|
} |
|
|
|
|
|
.option-label i { |
|
|
color: var(--primary-color); |
|
|
font-size: 18px; |
|
|
} |
|
|
|
|
|
.switch { |
|
|
position: relative; |
|
|
display: inline-block; |
|
|
width: 50px; |
|
|
height: 26px; |
|
|
} |
|
|
|
|
|
.switch input { |
|
|
opacity: 0; |
|
|
width: 0; |
|
|
height: 0; |
|
|
} |
|
|
|
|
|
.slider { |
|
|
position: absolute; |
|
|
cursor: pointer; |
|
|
top: 0; |
|
|
left: 0; |
|
|
right: 0; |
|
|
bottom: 0; |
|
|
background-color: var(--switch-bg); |
|
|
transition: var(--transition); |
|
|
border-radius: 34px; |
|
|
} |
|
|
|
|
|
.slider:before { |
|
|
position: absolute; |
|
|
content: ""; |
|
|
height: 20px; |
|
|
width: 20px; |
|
|
left: 3px; |
|
|
bottom: 3px; |
|
|
background-color: white; |
|
|
transition: var(--transition); |
|
|
border-radius: 50%; |
|
|
} |
|
|
|
|
|
input:checked + .slider { |
|
|
background-color: var(--switch-active); |
|
|
} |
|
|
|
|
|
input:checked + .slider:before { |
|
|
transform: translateX(24px); |
|
|
} |
|
|
|
|
|
|
|
|
.action-buttons { |
|
|
display: flex; |
|
|
gap: var(--gap); |
|
|
margin-bottom: 20px; |
|
|
flex-wrap: wrap; |
|
|
} |
|
|
|
|
|
.button { |
|
|
border: none; |
|
|
border-radius: var(--border-radius); |
|
|
padding: 14px 24px; |
|
|
font-size: 15px; |
|
|
cursor: pointer; |
|
|
transition: var(--transition); |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
gap: 8px; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.button:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.05); |
|
|
} |
|
|
|
|
|
.button-primary { |
|
|
background-color: var(--primary-color); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.button-primary:hover { |
|
|
background-color: var(--primary-hover); |
|
|
} |
|
|
|
|
|
.button-success { |
|
|
background-color: var(--success-color); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.button-success:hover { |
|
|
background-color: #059669; |
|
|
} |
|
|
|
|
|
.button-secondary { |
|
|
background-color: #e2e8f0; |
|
|
color: var(--text-color); |
|
|
} |
|
|
|
|
|
.button-secondary:hover { |
|
|
background-color: #cbd5e1; |
|
|
} |
|
|
|
|
|
.button-danger { |
|
|
background-color: var(--error-color); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.button-danger:hover { |
|
|
background-color: #dc2626; |
|
|
} |
|
|
|
|
|
|
|
|
.result-subtabs { |
|
|
display: flex; |
|
|
gap: 8px; |
|
|
margin-bottom: 20px; |
|
|
background: #f8fafc; |
|
|
border-radius: var(--border-radius); |
|
|
padding: 6px; |
|
|
} |
|
|
|
|
|
.result-subtab-button { |
|
|
flex: 1; |
|
|
border: none; |
|
|
background: none; |
|
|
padding: 12px 20px; |
|
|
font-size: 14px; |
|
|
font-weight: 500; |
|
|
color: var(--light-text); |
|
|
cursor: pointer; |
|
|
border-radius: 8px; |
|
|
transition: var(--transition); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
gap: 8px; |
|
|
} |
|
|
|
|
|
.result-subtab-button:hover { |
|
|
background: #eef2ff; |
|
|
color: var(--primary-color); |
|
|
} |
|
|
|
|
|
.result-subtab-button.active { |
|
|
background: var(--primary-color); |
|
|
color: white; |
|
|
box-shadow: 0 4px 6px -1px rgba(74, 108, 247, 0.2), 0 2px 4px -1px rgba(74, 108, 247, 0.06); |
|
|
} |
|
|
|
|
|
.log-container { |
|
|
max-height: 300px; |
|
|
overflow-y: auto; |
|
|
padding: 15px; |
|
|
border: 1px solid var(--border-color); |
|
|
border-radius: var(--border-radius); |
|
|
margin-bottom: 15px; |
|
|
background: #f8fafc; |
|
|
} |
|
|
|
|
|
.log-summary { |
|
|
font-size: 15px; |
|
|
font-weight: 600; |
|
|
margin-bottom: 15px; |
|
|
padding: 12px 15px; |
|
|
border-radius: var(--border-radius); |
|
|
background: #e0e7ff; |
|
|
color: var(--primary-color); |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
flex-wrap: wrap; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.log-summary span { |
|
|
background: white; |
|
|
padding: 5px 12px; |
|
|
border-radius: 20px; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.log-item { |
|
|
font-size: 14px; |
|
|
padding: 12px 15px; |
|
|
margin-bottom: 10px; |
|
|
border-radius: 8px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 10px; |
|
|
transition: var(--transition); |
|
|
} |
|
|
|
|
|
.log-item:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05), 0 2px 4px -1px rgba(0,0,0,0.02); |
|
|
} |
|
|
|
|
|
.log-item i { |
|
|
font-size: 16px; |
|
|
} |
|
|
|
|
|
.log-update { |
|
|
background-color: #d1fae5; |
|
|
color: var(--success-color); |
|
|
border-left: 4px solid var(--success-color); |
|
|
} |
|
|
|
|
|
.log-skip { |
|
|
background-color: #fffbeb; |
|
|
color: var(--warning-color); |
|
|
border-left: 4px solid var(--warning-color); |
|
|
} |
|
|
|
|
|
.log-notfound { |
|
|
background-color: #fee2e2; |
|
|
color: var(--error-color); |
|
|
border-left: 4px solid var(--error-color); |
|
|
} |
|
|
|
|
|
.output-container pre { |
|
|
font-size: 14px; |
|
|
padding: 15px; |
|
|
border: 1px solid var(--border-color); |
|
|
border-radius: var(--border-radius); |
|
|
background-color: #f8fafc; |
|
|
max-height: 300px; |
|
|
overflow-y: auto; |
|
|
margin-top: 10px; |
|
|
line-height: 1.5; |
|
|
font-family: 'Fira Code', 'Consolas', monospace; |
|
|
} |
|
|
|
|
|
|
|
|
.rules-section .rules-subtabs { |
|
|
display: flex; |
|
|
gap: 8px; |
|
|
margin-bottom: 20px; |
|
|
background: #f8fafc; |
|
|
border-radius: var(--border-radius); |
|
|
padding: 6px; |
|
|
} |
|
|
|
|
|
.rules-subtab-button { |
|
|
flex: 1; |
|
|
border: none; |
|
|
background: none; |
|
|
padding: 12px 20px; |
|
|
font-size: 14px; |
|
|
font-weight: 500; |
|
|
color: var(--light-text); |
|
|
cursor: pointer; |
|
|
border-radius: 8px; |
|
|
transition: var(--transition); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
gap: 8px; |
|
|
} |
|
|
|
|
|
.rules-subtab-button:hover { |
|
|
background: #eef2ff; |
|
|
color: var(--primary-color); |
|
|
} |
|
|
|
|
|
.rules-subtab-button.active { |
|
|
background: var(--primary-color); |
|
|
color: white; |
|
|
box-shadow: 0 4px 6px -1px rgba(74, 108, 247, 0.2), 0 2px 4px -1px rgba(74, 108, 247, 0.06); |
|
|
} |
|
|
|
|
|
|
|
|
.rules-list { |
|
|
max-height: 300px; |
|
|
overflow-y: auto; |
|
|
padding: 15px; |
|
|
border: 1px solid var(--border-color); |
|
|
border-radius: var(--border-radius); |
|
|
margin-bottom: 20px; |
|
|
background: #f8fafc; |
|
|
} |
|
|
|
|
|
.rules-list-header { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
margin-bottom: 15px; |
|
|
padding-bottom: 10px; |
|
|
border-bottom: 1px solid var(--border-color); |
|
|
} |
|
|
|
|
|
.rules-list-header h4 { |
|
|
font-size: 16px; |
|
|
font-weight: 600; |
|
|
color: var(--text-color); |
|
|
} |
|
|
|
|
|
.rules-count { |
|
|
background: var(--primary-color); |
|
|
color: white; |
|
|
padding: 3px 10px; |
|
|
border-radius: 20px; |
|
|
font-size: 13px; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
.rules-list-item { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
padding: 15px; |
|
|
margin-bottom: 12px; |
|
|
border: 1px solid var(--border-color); |
|
|
border-radius: var(--border-radius); |
|
|
background-color: white; |
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.03); |
|
|
transition: var(--transition); |
|
|
} |
|
|
|
|
|
.rules-list-item:hover { |
|
|
transform: translateX(3px); |
|
|
border-left: 3px solid var(--primary-color); |
|
|
} |
|
|
|
|
|
.rules-list-item .keyword { |
|
|
font-weight: 600; |
|
|
font-size: 15px; |
|
|
color: var(--text-color); |
|
|
background: #e0e7ff; |
|
|
padding: 3px 10px; |
|
|
border-radius: 20px; |
|
|
display: inline-block; |
|
|
} |
|
|
|
|
|
.rules-list-item .url { |
|
|
font-size: 14px; |
|
|
color: var(--secondary-color); |
|
|
text-decoration: none; |
|
|
white-space: nowrap; |
|
|
overflow: hidden; |
|
|
text-overflow: ellipsis; |
|
|
display: block; |
|
|
margin-top: 10px; |
|
|
padding-left: 5px; |
|
|
} |
|
|
|
|
|
.rules-list-item .delete-btn { |
|
|
padding: 6px 12px; |
|
|
font-size: 13px; |
|
|
background-color: var(--error-color); |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 6px; |
|
|
cursor: pointer; |
|
|
transition: var(--transition); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 5px; |
|
|
} |
|
|
|
|
|
.rules-list-item .delete-btn:hover { |
|
|
background-color: #dc2626; |
|
|
transform: translateY(-2px); |
|
|
} |
|
|
|
|
|
|
|
|
.add-rule-form { |
|
|
padding: 20px; |
|
|
border: 1px solid var(--border-color); |
|
|
border-radius: var(--border-radius); |
|
|
margin-bottom: 20px; |
|
|
background: white; |
|
|
box-shadow: var(--box-shadow); |
|
|
} |
|
|
|
|
|
.add-rule-form h4 { |
|
|
font-size: 16px; |
|
|
margin-bottom: 15px; |
|
|
color: var(--text-color); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.add-rule-form label { |
|
|
font-size: 14px; |
|
|
font-weight: 500; |
|
|
color: var(--text-color); |
|
|
margin-bottom: 8px; |
|
|
display: block; |
|
|
} |
|
|
|
|
|
.add-rule-form input[type="text"] { |
|
|
width: 100%; |
|
|
padding: 12px 15px; |
|
|
border: 1px solid var(--border-color); |
|
|
border-radius: var(--border-radius); |
|
|
font-size: 14px; |
|
|
margin-bottom: 15px; |
|
|
transition: var(--transition); |
|
|
} |
|
|
|
|
|
.add-rule-form input[type="text"]:focus { |
|
|
outline: none; |
|
|
border-color: var(--primary-color); |
|
|
box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.2); |
|
|
} |
|
|
|
|
|
|
|
|
.raw-editor textarea { |
|
|
width: 100%; |
|
|
height: 200px; |
|
|
padding: 15px; |
|
|
border: 1px solid var(--border-color); |
|
|
border-radius: var(--border-radius); |
|
|
font-size: 14px; |
|
|
resize: vertical; |
|
|
margin-bottom: 20px; |
|
|
font-family: 'Fira Code', 'Consolas', monospace; |
|
|
line-height: 1.5; |
|
|
transition: var(--transition); |
|
|
} |
|
|
|
|
|
.raw-editor textarea:focus { |
|
|
outline: none; |
|
|
border-color: var(--primary-color); |
|
|
box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.2); |
|
|
} |
|
|
|
|
|
.rules-action-buttons { |
|
|
display: flex; |
|
|
gap: 12px; |
|
|
flex-wrap: wrap; |
|
|
} |
|
|
|
|
|
|
|
|
.status { |
|
|
padding: 15px 20px; |
|
|
border-radius: var(--border-radius); |
|
|
font-size: 15px; |
|
|
margin-bottom: var(--gap); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 12px; |
|
|
box-shadow: var(--box-shadow); |
|
|
position: relative; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.status::before { |
|
|
content: ''; |
|
|
position: absolute; |
|
|
left: 0; |
|
|
top: 0; |
|
|
height: 100%; |
|
|
width: 4px; |
|
|
} |
|
|
|
|
|
.status.success { |
|
|
background-color: #d1fae5; |
|
|
color: var(--success-color); |
|
|
} |
|
|
|
|
|
.status.success::before { |
|
|
background-color: var(--success-color); |
|
|
} |
|
|
|
|
|
.status.error { |
|
|
background-color: #fee2e2; |
|
|
color: var(--error-color); |
|
|
} |
|
|
|
|
|
.status.error::before { |
|
|
background-color: var(--error-color); |
|
|
} |
|
|
|
|
|
.status i { |
|
|
font-size: 18px; |
|
|
} |
|
|
|
|
|
|
|
|
.footer { |
|
|
text-align: center; |
|
|
margin-top: 40px; |
|
|
padding: 20px; |
|
|
color: var(--light-text); |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
.footer a { |
|
|
color: var(--primary-color); |
|
|
text-decoration: none; |
|
|
} |
|
|
|
|
|
.footer a:hover { |
|
|
text-decoration: underline; |
|
|
} |
|
|
|
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.main-tabs { |
|
|
flex-direction: column; |
|
|
gap: 5px; |
|
|
} |
|
|
|
|
|
.result-subtabs { |
|
|
flex-direction: column; |
|
|
gap: 5px; |
|
|
} |
|
|
|
|
|
.rules-subtabs { |
|
|
flex-direction: column; |
|
|
gap: 5px; |
|
|
} |
|
|
|
|
|
.header h1 { |
|
|
font-size: 1.8rem; |
|
|
} |
|
|
|
|
|
.header p { |
|
|
font-size: 1rem; |
|
|
} |
|
|
|
|
|
.action-buttons, .rules-action-buttons { |
|
|
flex-direction: column; |
|
|
} |
|
|
|
|
|
.button { |
|
|
width: 100%; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div id="app"> |
|
|
|
|
|
<div class="header"> |
|
|
<h1><i class="fas fa-cogs"></i> OpenWebUI 模型头像URL修正工具</h1> |
|
|
<p>自动检测并修正模型配置文件中的头像URL,支持自定义规则管理,提升OpenWebUI界面美观度</p> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div v-if="statusMessage.text" :class="['status', statusMessage.type]"> |
|
|
<i :class="statusMessage.icon"></i> |
|
|
<span>{{ statusMessage.text }}</span> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="main-tabs"> |
|
|
<button @click="activeMainTab='upload'" :class="['main-tab-button', { active: activeMainTab==='upload' }]"> |
|
|
<i class="fas fa-upload"></i> 上传与结果 |
|
|
</button> |
|
|
<button @click="activeMainTab='rules'" :class="['main-tab-button', { active: activeMainTab==='rules' }]"> |
|
|
<i class="fas fa-list"></i> 规则管理 |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="tab-content"> |
|
|
|
|
|
<div v-if="activeMainTab==='upload'"> |
|
|
<div class="upload-section"> |
|
|
<h3 class="section-title"><i class="fas fa-file-import"></i> 模型文件处理</h3> |
|
|
|
|
|
|
|
|
<div class="input-group"> |
|
|
<label>选择模型JSON文件:</label> |
|
|
<div class="file-upload-container"> |
|
|
<input type="file" id="jsonFile" @change="handleFileChange" accept=".json" ref="fileInput" style="display: none;"> |
|
|
<button class="file-upload-btn" @click="triggerFileInput"> |
|
|
<i class="fas fa-folder-open"></i> 选择JSON文件 |
|
|
</button> |
|
|
<div :class="['file-name', fileContent ? '' : 'empty']"> |
|
|
<i :class="fileContent ? 'fas fa-check-circle' : 'fas fa-info-circle'"></i> |
|
|
{{ fileContent ? originalFileName : '未选择文件' }} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="options-container"> |
|
|
<div class="option-item"> |
|
|
<div class="option-label"> |
|
|
<i class="fas fa-binoculars"></i> |
|
|
<span>仅预览模式(不修改文件)</span> |
|
|
</div> |
|
|
<label class="switch"> |
|
|
<input type="checkbox" v-model="isDryRun"> |
|
|
<span class="slider"></span> |
|
|
</label> |
|
|
</div> |
|
|
<div class="option-item"> |
|
|
<div class="option-label"> |
|
|
<i class="fas fa-sync-alt"></i> |
|
|
<span>覆盖已有非默认URL</span> |
|
|
</div> |
|
|
<label class="switch"> |
|
|
<input type="checkbox" v-model="overwriteExisting"> |
|
|
<span class="slider"></span> |
|
|
</label> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="action-buttons"> |
|
|
<button @click="processFile" class="button button-primary" :disabled="!fileContent"> |
|
|
<i class="fas fa-bolt"></i> {{ isDryRun ? '开始预览' : '开始处理' }} |
|
|
</button> |
|
|
<button @click="downloadJson" class="button button-success" :disabled="!outputJson"> |
|
|
<i class="fas fa-download"></i> 下载修正文件 |
|
|
</button> |
|
|
<button @click="resetFile" class="button button-secondary"> |
|
|
<i class="fas fa-redo"></i> 重置 |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div v-if="logs.length > 0 || outputJson"> |
|
|
<h3 class="section-title"><i class="fas fa-chart-bar"></i> 处理结果</h3> |
|
|
|
|
|
<div class="result-subtabs"> |
|
|
<button @click="activeResultSubtab='logs'" :class="['result-subtab-button', { active: activeResultSubtab==='logs' }]"> |
|
|
<i class="fas fa-list"></i> 处理日志 |
|
|
</button> |
|
|
<button @click="activeResultSubtab='output'" :class="['result-subtab-button', { active: activeResultSubtab==='output' }]"> |
|
|
<i class="fas fa-code"></i> 输出JSON |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div v-if="activeResultSubtab==='logs'" class="log-container"> |
|
|
<div class="log-summary"> |
|
|
处理结果:共{{ totalModels }}条模型 |
|
|
<span>更新: {{ updatedCount }}</span> |
|
|
<span>跳过: {{ skippedCount }}</span> |
|
|
<span>未匹配: {{ notFoundCount }}</span> |
|
|
</div> |
|
|
<div v-for="(log, index) in logs" :key="index" :class="['log-item', `log-${log.type}`]"> |
|
|
<i :class="log.icon"></i> |
|
|
<span>{{ log.message }}</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div v-if="activeResultSubtab==='output' && !isDryRun" class="output-container"> |
|
|
<pre>{{ outputJson }}</pre> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div v-if="activeMainTab==='rules'"> |
|
|
<div class="rules-section"> |
|
|
<h3 class="section-title"><i class="fas fa-sliders-h"></i> 规则管理</h3> |
|
|
|
|
|
|
|
|
<div class="rules-subtabs"> |
|
|
<button @click="activeRulesSubtab='list'" :class="['rules-subtab-button', { active: activeRulesSubtab==='list' }]"> |
|
|
<i class="fas fa-th-list"></i> 现有规则 |
|
|
</button> |
|
|
<button @click="activeRulesSubtab='raw'" :class="['rules-subtab-button', { active: activeRulesSubtab==='raw' }]"> |
|
|
<i class="fas fa-code"></i> 原始JSON |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div v-if="activeRulesSubtab==='list'"> |
|
|
<div class="rules-list"> |
|
|
<div class="rules-list-header"> |
|
|
<h4>当前规则列表</h4> |
|
|
<div class="rules-count">{{ providerMap.length }} 条规则</div> |
|
|
</div> |
|
|
<div v-if="providerMap.length === 0" class="empty-rules"> |
|
|
<p style="font-size:14px; color:var(--light-text); text-align:center; padding:20px;"> |
|
|
<i class="fas fa-inbox" style="font-size:24px; margin-bottom:10px;"></i><br> |
|
|
暂无规则,请添加新规则 |
|
|
</p> |
|
|
</div> |
|
|
<div v-for="(rule, index) in providerMap" :key="index" class="rules-list-item"> |
|
|
<div> |
|
|
<span class="keyword">{{ rule[0] }}</span> |
|
|
<a :href="rule[1]" target="_blank" class="url">{{ rule[1] }}</a> |
|
|
</div> |
|
|
<button @click="deleteRule(index)" class="delete-btn"> |
|
|
<i class="fas fa-trash-alt"></i> 删除 |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="add-rule-form"> |
|
|
<h4><i class="fas fa-plus-circle"></i> 添加新规则</h4> |
|
|
<label> |
|
|
匹配关键词(如"gpt"): |
|
|
<input type="text" v-model="newKeyword" placeholder="请输入关键词" required> |
|
|
</label> |
|
|
<label> |
|
|
头像URL(完整链接): |
|
|
<input type="text" v-model="newUrl" placeholder="请输入图片URL" required> |
|
|
</label> |
|
|
<button @click="addRule" class="button button-primary" :disabled="!newKeyword || !newUrl"> |
|
|
<i class="fas fa-save"></i> 添加规则 |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div v-if="activeRulesSubtab==='raw'"> |
|
|
<div class="raw-editor"> |
|
|
<textarea v-model="rulesText" placeholder="规则格式:[['关键词1','URL1'],['关键词2','URL2']]"></textarea> |
|
|
</div> |
|
|
<div class="rules-action-buttons"> |
|
|
<button @click="saveRules" class="button button-primary"> |
|
|
<i class="fas fa-save"></i> 保存规则 |
|
|
</button> |
|
|
<button @click="importRules" class="button button-secondary"> |
|
|
<i class="fas fa-file-import"></i> 导入规则 |
|
|
</button> |
|
|
<button @click="exportRules" class="button button-secondary"> |
|
|
<i class="fas fa-file-export"></i> 导出规则 |
|
|
</button> |
|
|
<button @click="resetRules" class="button button-danger"> |
|
|
<i class="fas fa-undo"></i> 重置为默认 |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="footer"> |
|
|
<p>图像 CDN 服务由 lobechat 提供 再次感谢 lobechat 提供的图像 CDN 服务</p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
const { createApp, ref, reactive, watch, onMounted } = Vue; |
|
|
|
|
|
|
|
|
const DEFAULT_RULES = [ |
|
|
["openrouter/", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openrouter.svg"], |
|
|
["aliyun/", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/aliyun.svg"], |
|
|
["gemini_pipe_new.", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini.svg"], |
|
|
["gpt", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"], |
|
|
["openai", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"], |
|
|
["o1-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"], |
|
|
["o3-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"], |
|
|
["whisper", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/openai.svg"], |
|
|
["text-embedding-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle.svg"], |
|
|
["tts-", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle.svg"], |
|
|
["dall-e", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/dalle.svg"], |
|
|
["claude", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/claude.svg"], |
|
|
["gemini", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini.svg"], |
|
|
["ernie", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/wenxin.svg"], |
|
|
["baidu", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/wenxin.svg"], |
|
|
["command", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/cohere.svg"], |
|
|
["cohere", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/cohere.svg"], |
|
|
["deepseek", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek.svg"], |
|
|
["grok", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/grok.svg"], |
|
|
["llama", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/meta.svg"], |
|
|
["meta", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/meta.svg"], |
|
|
["groq", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/groq.svg"], |
|
|
["qwq", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen.svg"], |
|
|
["qvq", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen.svg"], |
|
|
["qwen", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qwen.svg"], |
|
|
["abab", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax.svg"], |
|
|
["minimax", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/minimax.svg"], |
|
|
["mistral", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral.svg"], |
|
|
["kimi", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg"], |
|
|
["moonshot", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg"], |
|
|
["ollama", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama.svg"], |
|
|
["hunyuan", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/hunyuan.svg"], |
|
|
["tencent", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/hunyuan.svg"], |
|
|
["yi", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/yi.svg"], |
|
|
["glm", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qingyan.svg"], |
|
|
["zhipu", "https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/qingyan.svg"], |
|
|
["open-mixtral","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral.svg"], |
|
|
["ministral","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral.svg"], |
|
|
["codestral","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral.svg"], |
|
|
["pixtral","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/mistral.svg"], |
|
|
["doubao","https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/doubao.svg"] |
|
|
]; |
|
|
|
|
|
|
|
|
const STORAGE_KEY = "openwebui_avatar_rules"; |
|
|
|
|
|
createApp({ |
|
|
setup() { |
|
|
|
|
|
const activeMainTab = ref("upload"); |
|
|
const activeResultSubtab = ref("logs"); |
|
|
const activeRulesSubtab = ref("list"); |
|
|
|
|
|
|
|
|
const fileContent = ref(null); |
|
|
const originalFileName = ref(""); |
|
|
const isDryRun = ref(false); |
|
|
const overwriteExisting = ref(false); |
|
|
const providerMap = ref([]); |
|
|
const rulesText = ref(""); |
|
|
const newKeyword = ref(""); |
|
|
const newUrl = ref(""); |
|
|
const logs = ref([]); |
|
|
const outputJson = ref(""); |
|
|
const error = ref(""); |
|
|
const statusMessage = reactive({ text: "", type: "success", icon: "" }); |
|
|
const fileInput = ref(null); |
|
|
|
|
|
|
|
|
const totalModels = ref(0); |
|
|
const updatedCount = ref(0); |
|
|
const skippedCount = ref(0); |
|
|
const notFoundCount = ref(0); |
|
|
|
|
|
|
|
|
onMounted(() => { |
|
|
loadRules(); |
|
|
}); |
|
|
|
|
|
|
|
|
watch(providerMap, (newRules) => { |
|
|
rulesText.value = JSON.stringify(newRules, null, 2); |
|
|
}, { deep: true }); |
|
|
|
|
|
|
|
|
const loadRules = () => { |
|
|
try { |
|
|
const stored = localStorage.getItem(STORAGE_KEY); |
|
|
providerMap.value = stored ? JSON.parse(stored) : [...DEFAULT_RULES]; |
|
|
showStatus("规则加载成功", "success", "fas fa-check-circle"); |
|
|
} catch (e) { |
|
|
console.error("加载规则失败:", e); |
|
|
providerMap.value = [...DEFAULT_RULES]; |
|
|
showStatus("加载规则失败,使用默认规则", "error", "fas fa-exclamation-circle"); |
|
|
} |
|
|
}; |
|
|
|
|
|
const saveRules = () => { |
|
|
try { |
|
|
if (activeRulesSubtab.value === "raw") { |
|
|
const parsed = JSON.parse(rulesText.value); |
|
|
if (!Array.isArray(parsed) || !parsed.every(item => Array.isArray(item) && item.length === 2)) { |
|
|
throw new Error("规则格式错误,请检查JSON结构"); |
|
|
} |
|
|
providerMap.value = parsed; |
|
|
} |
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(providerMap.value)); |
|
|
showStatus("规则保存成功", "success", "fas fa-check-circle"); |
|
|
} catch (e) { |
|
|
console.error("保存规则失败:", e); |
|
|
showStatus(`保存失败:${e.message}`, "error", "fas fa-exclamation-circle"); |
|
|
} |
|
|
}; |
|
|
|
|
|
const importRules = () => { |
|
|
const input = document.createElement("input"); |
|
|
input.type = "file"; |
|
|
input.accept = ".json"; |
|
|
input.onchange = (e) => { |
|
|
const file = e.target.files[0]; |
|
|
if (!file) return; |
|
|
const reader = new FileReader(); |
|
|
reader.onload = (event) => { |
|
|
try { |
|
|
const rules = JSON.parse(event.target.result); |
|
|
if (Array.isArray(rules) && rules.every(item => Array.isArray(item) && item.length === 2)) { |
|
|
providerMap.value = rules; |
|
|
saveRules(); |
|
|
showStatus("规则导入成功", "success", "fas fa-check-circle"); |
|
|
} else { |
|
|
throw new Error("导入文件格式错误,需为规则JSON数组"); |
|
|
} |
|
|
} catch (e) { |
|
|
console.error("导入失败:", e); |
|
|
showStatus(`导入失败:${e.message}`, "error", "fas fa-exclamation-circle"); |
|
|
} |
|
|
}; |
|
|
reader.readAsText(file); |
|
|
}; |
|
|
input.click(); |
|
|
}; |
|
|
|
|
|
const exportRules = () => { |
|
|
const blob = new Blob([JSON.stringify(providerMap.value, null, 2)], { type: "application/json" }); |
|
|
const url = URL.createObjectURL(blob); |
|
|
const a = document.createElement("a"); |
|
|
a.href = url; |
|
|
a.download = "openwebui_avatar_rules.json"; |
|
|
a.click(); |
|
|
URL.revokeObjectURL(url); |
|
|
showStatus("规则导出成功", "success", "fas fa-check-circle"); |
|
|
}; |
|
|
|
|
|
const resetRules = () => { |
|
|
if (confirm("确定要重置为默认规则吗?当前规则将丢失")) { |
|
|
providerMap.value = [...DEFAULT_RULES]; |
|
|
localStorage.removeItem(STORAGE_KEY); |
|
|
showStatus("规则已重置为默认", "success", "fas fa-check-circle"); |
|
|
} |
|
|
}; |
|
|
|
|
|
const addRule = () => { |
|
|
if (!newKeyword.value || !newUrl.value) { |
|
|
showStatus("关键词和URL不能为空", "error", "fas fa-exclamation-circle"); |
|
|
return; |
|
|
} |
|
|
if (!newUrl.value.startsWith('http')) { |
|
|
showStatus("URL格式无效,必须以http/https开头", "error", "fas fa-exclamation-circle"); |
|
|
return; |
|
|
} |
|
|
const exists = providerMap.value.some(rule => rule[0].toLowerCase() === newKeyword.value.toLowerCase()); |
|
|
if (exists) { |
|
|
showStatus("该关键词已存在,请修改后重试", "error", "fas fa-exclamation-circle"); |
|
|
return; |
|
|
} |
|
|
providerMap.value.push([newKeyword.value, newUrl.value]); |
|
|
newKeyword.value = ""; |
|
|
newUrl.value = ""; |
|
|
saveRules(); |
|
|
showStatus("规则添加成功", "success", "fas fa-check-circle"); |
|
|
}; |
|
|
|
|
|
const deleteRule = (index) => { |
|
|
if (confirm("确定要删除该规则吗?")) { |
|
|
providerMap.value.splice(index, 1); |
|
|
saveRules(); |
|
|
showStatus("规则删除成功", "success", "fas fa-check-circle"); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const triggerFileInput = () => { |
|
|
fileInput.value.click(); |
|
|
}; |
|
|
|
|
|
const handleFileChange = (e) => { |
|
|
const file = e.target.files[0]; |
|
|
if (!file) return; |
|
|
originalFileName.value = file.name; |
|
|
const reader = new FileReader(); |
|
|
reader.onload = (event) => { |
|
|
fileContent.value = event.target.result; |
|
|
error.value = ""; |
|
|
showStatus(`文件 "${file.name}" 加载成功`, "success", "fas fa-check-circle"); |
|
|
}; |
|
|
reader.onerror = () => { |
|
|
showStatus("文件读取失败,请检查文件完整性", "error", "fas fa-exclamation-circle"); |
|
|
}; |
|
|
reader.readAsText(file); |
|
|
}; |
|
|
|
|
|
const resetFile = () => { |
|
|
fileContent.value = null; |
|
|
originalFileName.value = ""; |
|
|
logs.value = []; |
|
|
outputJson.value = ""; |
|
|
fileInput.value.value = ""; |
|
|
showStatus("文件已重置", "success", "fas fa-redo"); |
|
|
}; |
|
|
|
|
|
const processFile = () => { |
|
|
if (!fileContent.value) { |
|
|
showStatus("请先选择模型JSON文件", "error", "fas fa-exclamation-circle"); |
|
|
return; |
|
|
} |
|
|
if (providerMap.value.length === 0) { |
|
|
showStatus("请先添加或导入规则", "error", "fas fa-exclamation-circle"); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const data = JSON.parse(fileContent.value); |
|
|
if (!Array.isArray(data)) { |
|
|
throw new Error("JSON文件格式错误,顶层应为模型数组"); |
|
|
} |
|
|
|
|
|
|
|
|
totalModels.value = data.length; |
|
|
updatedCount.value = 0; |
|
|
skippedCount.value = 0; |
|
|
notFoundCount.value = 0; |
|
|
logs.value = []; |
|
|
|
|
|
|
|
|
const processed = data.map(model => { |
|
|
const clone = { ...model }; |
|
|
const modelId = clone.id || ""; |
|
|
const currentUrl = clone.meta?.profile_image_url || ""; |
|
|
|
|
|
|
|
|
if (clone.base_model_id === undefined) { |
|
|
clone.base_model_id = null; |
|
|
} |
|
|
|
|
|
|
|
|
let newUrl = null; |
|
|
for (const [keyword, url] of providerMap.value) { |
|
|
if (modelId.toLowerCase().includes(keyword.toLowerCase())) { |
|
|
newUrl = url; |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (newUrl) { |
|
|
if (currentUrl === newUrl) { |
|
|
skippedCount.value++; |
|
|
logs.value.push({ |
|
|
type: "skip", |
|
|
message: `[跳过] ${modelId}:已使用正确URL`, |
|
|
icon: "fas fa-redo" |
|
|
}); |
|
|
} else if (currentUrl === "" || currentUrl === "/static/favicon.png" || overwriteExisting.value) { |
|
|
if (!isDryRun.value) { |
|
|
clone.meta = clone.meta || {}; |
|
|
clone.meta.profile_image_url = newUrl; |
|
|
} |
|
|
updatedCount.value++; |
|
|
logs.value.push({ |
|
|
type: "update", |
|
|
message: `[更新] ${modelId}:旧URL→${currentUrl || "空"},新URL→${newUrl}`, |
|
|
icon: "fas fa-sync-alt" |
|
|
}); |
|
|
} else { |
|
|
skippedCount.value++; |
|
|
logs.value.push({ |
|
|
type: "skip", |
|
|
message: `[跳过] ${modelId}:已有非默认URL(未勾选覆盖)`, |
|
|
icon: "fas fa-ban" |
|
|
}); |
|
|
} |
|
|
} else { |
|
|
notFoundCount.value++; |
|
|
logs.value.push({ |
|
|
type: "notfound", |
|
|
message: `[未匹配] ${modelId}:无对应规则`, |
|
|
icon: "fas fa-search" |
|
|
}); |
|
|
} |
|
|
|
|
|
return clone; |
|
|
}); |
|
|
|
|
|
|
|
|
if (!isDryRun.value) { |
|
|
outputJson.value = JSON.stringify(processed, null, 2); |
|
|
} else { |
|
|
outputJson.value = ""; |
|
|
} |
|
|
|
|
|
showStatus(`处理完成:共${totalModels.value}条模型`, "success", "fas fa-check-circle"); |
|
|
} catch (e) { |
|
|
console.error("处理失败:", e); |
|
|
showStatus(`处理失败:${e.message}`, "error", "fas fa-exclamation-circle"); |
|
|
} |
|
|
}; |
|
|
|
|
|
const downloadJson = () => { |
|
|
if (!outputJson.value) return; |
|
|
const blob = new Blob([outputJson.value], { type: "application/json" }); |
|
|
const url = URL.createObjectURL(blob); |
|
|
const a = document.createElement("a"); |
|
|
a.href = url; |
|
|
a.download = `${originalFileName.value.replace(".json", "")}_modified.json`; |
|
|
a.click(); |
|
|
URL.revokeObjectURL(url); |
|
|
showStatus("文件下载成功", "success", "fas fa-download"); |
|
|
}; |
|
|
|
|
|
|
|
|
const showStatus = (message, type = "success", icon = "fas fa-info-circle") => { |
|
|
statusMessage.text = message; |
|
|
statusMessage.type = type; |
|
|
statusMessage.icon = icon; |
|
|
setTimeout(() => { |
|
|
statusMessage.text = ""; |
|
|
}, 4000); |
|
|
}; |
|
|
|
|
|
|
|
|
return { |
|
|
activeMainTab, |
|
|
activeResultSubtab, |
|
|
activeRulesSubtab, |
|
|
fileContent, |
|
|
originalFileName, |
|
|
isDryRun, |
|
|
overwriteExisting, |
|
|
providerMap, |
|
|
rulesText, |
|
|
newKeyword, |
|
|
newUrl, |
|
|
logs, |
|
|
outputJson, |
|
|
error, |
|
|
statusMessage, |
|
|
fileInput, |
|
|
totalModels, |
|
|
updatedCount, |
|
|
skippedCount, |
|
|
notFoundCount, |
|
|
loadRules, |
|
|
saveRules, |
|
|
importRules, |
|
|
exportRules, |
|
|
resetRules, |
|
|
addRule, |
|
|
deleteRule, |
|
|
triggerFileInput, |
|
|
handleFileChange, |
|
|
resetFile, |
|
|
processFile, |
|
|
downloadJson |
|
|
}; |
|
|
} |
|
|
}).mount("#app"); |
|
|
</script> |
|
|
</body> |
|
|
</html> |