You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
514 lines
29 KiB
514 lines
29 KiB
<template>
|
|
<div>
|
|
<!-- 演示横幅 -->
|
|
<div class="mb-4 rounded-lg px-4 py-2.5 text-xs flex items-center gap-2" style="background:#eef2ff;border:1px solid #c7d2fe;color:#4338ca">
|
|
<span class="font-semibold">演示效果</span>
|
|
<span class="text-indigo-400">·</span>
|
|
<span>本页为 UI 演示,数据为本地 mock,未连接后端。确认方向后再接 console 接口。</span>
|
|
</div>
|
|
|
|
<!-- 顶部 -->
|
|
<div class="flex items-center justify-between mb-5">
|
|
<div>
|
|
<h2 class="text-lg font-semibold text-slate-800">系统配置</h2>
|
|
<p class="text-xs text-slate-400 mt-1">平台级服务配置:① 对象存储(固件 / 头像等文件,默认阿里云 OSS);② 邮件服务(生产环节给厂家发通知邮件);③ 翻译服务(后台配置多语言同步)。</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-4" style="align-items:flex-start">
|
|
<!-- 左侧分类 -->
|
|
<div style="width:208px;flex-shrink:0">
|
|
<div class="flex flex-col gap-1.5">
|
|
<div
|
|
v-for="c in categories"
|
|
:key="c.id"
|
|
class="cat-item"
|
|
:class="{ active: curCat === c.id }"
|
|
@click="curCat = c.id"
|
|
>
|
|
<div class="cat-icon" :style="{ background: c.color }">
|
|
<svg width="17" height="17" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="c.icon" /></svg>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="text-sm font-medium text-slate-700">{{ c.label }}</div>
|
|
<div class="text-[11px] text-slate-400 truncate">{{ c.desc }}</div>
|
|
</div>
|
|
<span v-if="svcEnabled(c.id)" class="dot-on" title="已启用"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 右侧详情 -->
|
|
<div class="flex-1 min-w-0">
|
|
<!-- ========== 对象存储 ========== -->
|
|
<div v-if="curCat === 'storage'">
|
|
<!-- 固件存储指向提示 -->
|
|
<div class="rounded-xl px-4 py-3 mb-4 flex items-center gap-3" style="background:linear-gradient(135deg,#ecfdf5,#f0fdfa);border:1px solid #a7f3d0">
|
|
<div style="width:36px;height:36px;border-radius:10px;background:#10b981;display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
|
<svg style="width:19px;height:19px;color:#fff" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" /></svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="text-sm font-semibold text-emerald-800">设备固件上传当前指向:{{ activeProvider.name }}</div>
|
|
<div class="text-[11px] text-emerald-600 mt-0.5">固件包、用户头像等文件均上传至此存储桶。切换默认存储不影响历史文件 URL。</div>
|
|
</div>
|
|
<span class="badge badge-success badge-sm text-white">运行中</span>
|
|
</div>
|
|
|
|
<!-- 厂商选择卡片 -->
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span class="text-xs font-semibold text-slate-500">选择默认对象存储 <span class="text-slate-400 font-normal">(固件 / 头像等文件上传指向此服务)</span></span>
|
|
</div>
|
|
<div class="grid grid-cols-4 gap-3 mb-4">
|
|
<div
|
|
v-for="p in providers"
|
|
:key="p.id"
|
|
class="provider-card"
|
|
:class="{ active: curProvider === p.id, 'is-default': p.id === activeId }"
|
|
@click="curProvider = p.id"
|
|
>
|
|
<div class="flex items-center justify-between">
|
|
<div class="provider-logo" :style="{ background: p.color }">{{ p.short }}</div>
|
|
<span v-if="p.enable" class="text-[10px] text-emerald-500 font-semibold">已配置</span>
|
|
<span v-else class="text-[10px] text-slate-300 font-semibold">未配置</span>
|
|
</div>
|
|
<div class="text-sm font-semibold text-slate-700 mt-2">{{ p.name }}</div>
|
|
<div class="text-[11px] text-slate-400 mb-2.5">{{ p.tag }}</div>
|
|
<label class="default-pick" :class="{ on: p.id === activeId }" @click.stop="setDefaultById(p.id)">
|
|
<input type="radio" class="radio radio-xs radio-primary" :checked="p.id === activeId" @click.stop="setDefaultById(p.id)" />
|
|
<span>{{ p.id === activeId ? '默认使用' : '设为默认' }}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 选中厂商表单 -->
|
|
<div class="bg-white rounded-2xl border border-slate-100 shadow-sm p-6">
|
|
<div class="flex items-center justify-between mb-5">
|
|
<div class="flex items-center gap-3">
|
|
<div class="provider-logo" :style="{ background: editProvider.color, width:'44px', height:'44px', fontSize:'15px' }">{{ editProvider.short }}</div>
|
|
<div>
|
|
<h3 class="text-base font-semibold text-slate-800">{{ editProvider.name }}</h3>
|
|
<p class="text-xs text-slate-400 mt-0.5">{{ editProvider.tag }} · 对象存储</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<label class="flex items-center gap-2 mr-1">
|
|
<span class="text-xs text-slate-500">启用</span>
|
|
<input v-model="editProvider.enable" type="checkbox" class="toggle toggle-primary toggle-sm" />
|
|
</label>
|
|
<button class="btn btn-outline btn-sm" @click="testConn">测试连接</button>
|
|
<button v-if="curProvider !== activeId" class="btn btn-ghost btn-sm text-indigo-600" @click="setDefault">设为默认存储</button>
|
|
<button class="btn btn-primary btn-sm text-white" @click="save">保存</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divider text-xs text-slate-400 my-3">连接配置</div>
|
|
<div class="grid grid-cols-2 gap-x-5 gap-y-4">
|
|
<div v-for="f in editProvider.fields" :key="f.key" class="form-control" :class="{ 'col-span-2': f.wide }">
|
|
<label class="label py-1">
|
|
<span class="label-text text-sm">
|
|
{{ f.label }}<span v-if="f.required" class="text-rose-500 ml-0.5">*</span>
|
|
<span v-if="f.encrypted" class="lock-tag ml-1.5">🔒 加密存储</span>
|
|
</span>
|
|
</label>
|
|
<div class="relative">
|
|
<input
|
|
v-model="f.value"
|
|
:type="f.encrypted && !f._show ? 'password' : 'text'"
|
|
class="input input-bordered input-sm w-full"
|
|
:class="{ 'font-mono': f.mono, 'pr-14': f.encrypted }"
|
|
:placeholder="f.placeholder"
|
|
/>
|
|
<button v-if="f.encrypted" type="button" class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-400 hover:text-slate-700" @click="f._show = !f._show">{{ f._show ? '隐藏' : '显示' }}</button>
|
|
</div>
|
|
<span v-if="f.hint" class="text-[11px] text-slate-400 mt-1">{{ f.hint }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divider text-xs text-slate-400 my-4">上传规则</div>
|
|
<div class="grid grid-cols-3 gap-4">
|
|
<div class="rule-box">
|
|
<div class="text-[11px] text-slate-400">固件包路径前缀</div>
|
|
<div class="font-mono text-sm text-slate-700 mt-1">/firmware/</div>
|
|
</div>
|
|
<div class="rule-box">
|
|
<div class="text-[11px] text-slate-400">头像路径前缀</div>
|
|
<div class="font-mono text-sm text-slate-700 mt-1">/userAvatar/</div>
|
|
</div>
|
|
<div class="rule-box">
|
|
<div class="text-[11px] text-slate-400">签名 URL 有效期</div>
|
|
<div class="font-mono text-sm text-slate-700 mt-1">3600 s</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ========== 邮件服务 ========== -->
|
|
<div v-else-if="curCat === 'mail'">
|
|
<!-- 用途说明 -->
|
|
<div class="rounded-xl px-4 py-3 mb-4 flex items-center gap-3" style="background:#fffbeb;border:1px solid #fde68a">
|
|
<div style="width:36px;height:36px;border-radius:10px;background:#f59e0b;display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
|
<svg style="width:19px;height:19px;color:#fff" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="text-sm font-semibold text-amber-800">生产环节邮件通知</div>
|
|
<div class="text-[11px] text-amber-600 mt-0.5">用于给目标厂家发送授权信息、公码批次、设备出货等通知邮件;账号告警也走此通道。</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-2xl border border-slate-100 shadow-sm p-6">
|
|
<div class="flex items-center justify-between mb-5">
|
|
<div class="flex items-center gap-3">
|
|
<div class="provider-logo" :style="{ background: '#f59e0b', width:'44px', height:'44px', fontSize:'13px' }">SMTP</div>
|
|
<div>
|
|
<h3 class="text-base font-semibold text-slate-800">邮件服务</h3>
|
|
<p class="text-xs text-slate-400 mt-0.5">SMTP 发信</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<label class="flex items-center gap-2 mr-1">
|
|
<span class="text-xs text-slate-500">启用</span>
|
|
<input v-model="mail.enable" type="checkbox" class="toggle toggle-primary toggle-sm" />
|
|
</label>
|
|
<button class="btn btn-primary btn-sm text-white" @click="save">保存</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divider text-xs text-slate-400 my-3">发信配置</div>
|
|
<div class="grid grid-cols-2 gap-x-5 gap-y-4">
|
|
<div class="form-control">
|
|
<label class="label py-1"><span class="label-text text-sm">SMTP 主机 <span class="text-rose-500">*</span></span></label>
|
|
<input v-model="mail.host" class="input input-bordered input-sm font-mono" placeholder="smtp.exmail.qq.com" />
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label py-1"><span class="label-text text-sm">端口 <span class="text-rose-500">*</span></span></label>
|
|
<input v-model="mail.port" class="input input-bordered input-sm font-mono" placeholder="465" />
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label py-1"><span class="label-text text-sm">加密方式</span></label>
|
|
<select v-model="mail.secure" class="select select-bordered select-sm">
|
|
<option v-for="o in secureOptions" :key="o" :value="o">{{ o }}</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label py-1"><span class="label-text text-sm">发件人显示名</span></label>
|
|
<input v-model="mail.fromName" class="input input-bordered input-sm" placeholder="云岩科技" />
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label py-1"><span class="label-text text-sm">发信账号 <span class="text-rose-500">*</span></span></label>
|
|
<input v-model="mail.user" class="input input-bordered input-sm font-mono" placeholder="noreply@yunyan.com" />
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label py-1"><span class="label-text text-sm">授权码 / 密码 <span class="lock-tag ml-1.5">🔒 加密存储</span></span></label>
|
|
<div class="relative">
|
|
<input v-model="mail.pass" :type="mail._showPass ? 'text' : 'password'" class="input input-bordered input-sm w-full font-mono pr-14" placeholder="留空保留原密文" />
|
|
<button type="button" class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-400 hover:text-slate-700" @click="mail._showPass = !mail._showPass">{{ mail._showPass ? '隐藏' : '显示' }}</button>
|
|
</div>
|
|
</div>
|
|
<div class="form-control col-span-2">
|
|
<label class="label py-1"><span class="label-text text-sm">默认抄送 (CC)</span></label>
|
|
<input v-model="mail.cc" class="input input-bordered input-sm font-mono" placeholder="多个邮箱用逗号分隔,可留空" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divider text-xs text-slate-400 my-4">测试发信</div>
|
|
<div class="flex items-end gap-3">
|
|
<div class="form-control flex-1">
|
|
<label class="label py-1"><span class="label-text text-sm">测试收件邮箱</span></label>
|
|
<input v-model="mail.testTo" class="input input-bordered input-sm font-mono" placeholder="填一个邮箱,发送一封测试邮件验证配置" />
|
|
</div>
|
|
<button class="btn btn-outline btn-sm" @click="testMail">发送测试</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ========== 翻译服务 ========== -->
|
|
<div v-else-if="curCat === 'translate'">
|
|
<!-- 用途说明 -->
|
|
<div class="rounded-xl px-4 py-3 mb-4 flex items-center gap-3" style="background:#f0fdfa;border:1px solid #99f6e4">
|
|
<div style="width:36px;height:36px;border-radius:10px;background:#14b8a6;display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
|
<svg style="width:19px;height:19px;color:#fff" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129" /></svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="text-sm font-semibold text-teal-800">后台配置多语言同步</div>
|
|
<div class="text-[11px] text-teal-600 mt-0.5">应用文案、通知 / 邮件模板、公告等后台配置内容,按目标语种自动翻译,保证多语言版本一致。</div>
|
|
</div>
|
|
<span class="badge badge-sm text-white" style="background:#14b8a6;border:none">默认 {{ tActiveProvider.name }}</span>
|
|
</div>
|
|
|
|
<!-- 厂商选择卡片 -->
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span class="text-xs font-semibold text-slate-500">选择默认翻译服务 <span class="text-slate-400 font-normal">(后台多语言同步走此服务)</span></span>
|
|
</div>
|
|
<div class="grid grid-cols-4 gap-3 mb-4">
|
|
<div
|
|
v-for="p in tProviders"
|
|
:key="p.id"
|
|
class="provider-card"
|
|
:class="{ active: tCurProvider === p.id, 'is-default': p.id === tActiveId }"
|
|
@click="tCurProvider = p.id"
|
|
>
|
|
<div class="flex items-center justify-between">
|
|
<div class="provider-logo" :style="{ background: p.color }">{{ p.short }}</div>
|
|
<span v-if="p.enable" class="text-[10px] text-emerald-500 font-semibold">已配置</span>
|
|
<span v-else class="text-[10px] text-slate-300 font-semibold">未配置</span>
|
|
</div>
|
|
<div class="text-sm font-semibold text-slate-700 mt-2">{{ p.name }}</div>
|
|
<div class="text-[11px] text-slate-400 mb-2.5">{{ p.tag }}</div>
|
|
<label class="default-pick" :class="{ on: p.id === tActiveId }" @click.stop="setTDefaultById(p.id)">
|
|
<input type="radio" class="radio radio-xs radio-primary" :checked="p.id === tActiveId" @click.stop="setTDefaultById(p.id)" />
|
|
<span>{{ p.id === tActiveId ? '默认使用' : '设为默认' }}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 选中厂商表单 -->
|
|
<div class="bg-white rounded-2xl border border-slate-100 shadow-sm p-6">
|
|
<div class="flex items-center justify-between mb-5">
|
|
<div class="flex items-center gap-3">
|
|
<div class="provider-logo" :style="{ background: tEditProvider.color, width:'44px', height:'44px', fontSize:'13px' }">{{ tEditProvider.short }}</div>
|
|
<div>
|
|
<h3 class="text-base font-semibold text-slate-800">{{ tEditProvider.name }}</h3>
|
|
<p class="text-xs text-slate-400 mt-0.5">{{ tEditProvider.tag }} · 机器翻译</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<label class="flex items-center gap-2 mr-1">
|
|
<span class="text-xs text-slate-500">启用</span>
|
|
<input v-model="tEditProvider.enable" type="checkbox" class="toggle toggle-primary toggle-sm" />
|
|
</label>
|
|
<button class="btn btn-outline btn-sm" @click="testTranslate">测试连接</button>
|
|
<button v-if="tCurProvider !== tActiveId" class="btn btn-ghost btn-sm text-indigo-600" @click="setTDefaultById(tCurProvider)">设为默认</button>
|
|
<button class="btn btn-primary btn-sm text-white" @click="save">保存</button>
|
|
</div>
|
|
</div>
|
|
<div class="divider text-xs text-slate-400 my-3">连接配置</div>
|
|
<div class="grid grid-cols-2 gap-x-5 gap-y-4">
|
|
<div v-for="f in tEditProvider.fields" :key="f.key" class="form-control" :class="{ 'col-span-2': f.wide }">
|
|
<label class="label py-1">
|
|
<span class="label-text text-sm">
|
|
{{ f.label }}<span v-if="f.required" class="text-rose-500 ml-0.5">*</span>
|
|
<span v-if="f.encrypted" class="lock-tag ml-1.5">🔒 加密存储</span>
|
|
</span>
|
|
</label>
|
|
<div class="relative">
|
|
<input
|
|
v-model="f.value"
|
|
:type="f.encrypted && !f._show ? 'password' : 'text'"
|
|
class="input input-bordered input-sm w-full"
|
|
:class="{ 'font-mono': f.mono, 'pr-14': f.encrypted }"
|
|
:placeholder="f.placeholder"
|
|
/>
|
|
<button v-if="f.encrypted" type="button" class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-400 hover:text-slate-700" @click="f._show = !f._show">{{ f._show ? '隐藏' : '显示' }}</button>
|
|
</div>
|
|
<span v-if="f.hint" class="text-[11px] text-slate-400 mt-1">{{ f.hint }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, computed } from 'vue'
|
|
import { useToast } from '~/composables/useToast'
|
|
|
|
definePageMeta({ title: '系统配置' })
|
|
|
|
const { success, info } = useToast() as any
|
|
|
|
interface Field {
|
|
key: string; label: string; value: string
|
|
placeholder?: string; hint?: string
|
|
required?: boolean; encrypted?: boolean; mono?: boolean; wide?: boolean
|
|
_show?: boolean
|
|
}
|
|
interface Provider {
|
|
id: string; name: string; short: string; tag: string; color: string
|
|
enable: boolean; fields: Field[]
|
|
}
|
|
|
|
const categories = [
|
|
{ id: 'storage', label: '对象存储', desc: '固件 / 头像 / 文件', color: '#6366f1', icon: 'M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8' },
|
|
{ id: 'mail', label: '邮件服务', desc: '生产通知 / 厂家发信', color: '#f59e0b', icon: 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z' },
|
|
{ id: 'translate', label: '翻译服务', desc: '后台文案多语言同步', color: '#14b8a6', icon: 'M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129' },
|
|
]
|
|
|
|
const curCat = ref('storage')
|
|
|
|
// 邮件服务(生产环节给厂家发送通知邮件)
|
|
const mail = reactive({
|
|
enable: true,
|
|
host: 'smtp.exmail.qq.com',
|
|
port: '465',
|
|
secure: 'SSL',
|
|
user: 'noreply@yunyan.com',
|
|
pass: '',
|
|
_showPass: false,
|
|
fromName: '云岩科技',
|
|
cc: 'ops@yunyan.com',
|
|
testTo: '',
|
|
})
|
|
const secureOptions = ['SSL', 'STARTTLS', '不加密']
|
|
function testMail() {
|
|
if (!mail.testTo) { info('请先填测试收件邮箱'); return }
|
|
info(`正在向 ${mail.testTo} 发送测试邮件…(演示)`)
|
|
setTimeout(() => success(`测试邮件已发送至 ${mail.testTo}`), 600)
|
|
}
|
|
function svcEnabled(id: string) {
|
|
if (id === 'storage') return true
|
|
if (id === 'mail') return mail.enable
|
|
if (id === 'translate') return tActiveProvider.value?.enable ?? false
|
|
return false
|
|
}
|
|
|
|
// 翻译服务(后台配置内容多语言同步)
|
|
const tProviders = reactive<Provider[]>([
|
|
{
|
|
id: 'volc', name: '火山引擎翻译', short: '火山', tag: 'Volcengine MT', color: '#1664ff', enable: true,
|
|
fields: [
|
|
{ key: 'region', label: 'Region', value: 'cn-beijing', mono: true, required: true },
|
|
{ key: 'accessKey', label: 'AccessKey', value: 'AKLTZjJmODdk****', mono: true, encrypted: true, required: true },
|
|
{ key: 'secretKey', label: 'SecretKey', value: '', mono: true, encrypted: true, required: true, hint: '留空保留原密文' },
|
|
],
|
|
},
|
|
{
|
|
id: 'aliyun', name: '阿里云翻译', short: '阿里', tag: 'Aliyun MT', color: '#ff6a00', enable: true,
|
|
fields: [
|
|
{ key: 'region', label: 'Region', value: 'cn-hangzhou', mono: true, required: true },
|
|
{ key: 'accessKeyId', label: 'AccessKey ID', value: 'LTAI5txxxxxxxx', mono: true, encrypted: true, required: true },
|
|
{ key: 'accessKeySecret', label: 'AccessKey Secret', value: '', mono: true, encrypted: true, required: true, hint: '留空保留原密文' },
|
|
{ key: 'scene', label: '翻译场景', value: 'general', mono: true, hint: 'general / social / ecommerce / tech' },
|
|
],
|
|
},
|
|
{
|
|
id: 'google', name: 'Google 翻译', short: 'G', tag: 'Cloud Translation', color: '#ea4335', enable: false,
|
|
fields: [
|
|
{ key: 'project', label: 'Project ID', value: '', mono: true, required: true },
|
|
{ key: 'apiKey', label: 'API Key', value: '', mono: true, encrypted: true, required: true },
|
|
],
|
|
},
|
|
{
|
|
id: 'deepl', name: 'DeepL', short: 'DL', tag: 'DeepL API', color: '#0f2b46', enable: false,
|
|
fields: [
|
|
{ key: 'authKey', label: 'Auth Key', value: '', mono: true, encrypted: true, required: true },
|
|
{ key: 'plan', label: '套餐', value: 'free', mono: true, hint: 'free / pro' },
|
|
],
|
|
},
|
|
])
|
|
const tActiveId = ref('volc')
|
|
const tCurProvider = ref('volc')
|
|
const tEditProvider = computed(() => tProviders.find((p) => p.id === tCurProvider.value)!)
|
|
const tActiveProvider = computed(() => tProviders.find((p) => p.id === tActiveId.value)!)
|
|
|
|
function setTDefaultById(id: string) {
|
|
const p = tProviders.find((x) => x.id === id)
|
|
if (!p) return
|
|
tActiveId.value = id
|
|
tCurProvider.value = id
|
|
p.enable = true
|
|
success(`已将「${p.name}」设为默认翻译服务,后台多语言同步将走该服务`)
|
|
}
|
|
function testTranslate() {
|
|
info(`正在测试 ${tEditProvider.value.name} …(演示)`)
|
|
setTimeout(() => success(`${tEditProvider.value.name} 连接正常,可调用翻译`), 600)
|
|
}
|
|
|
|
// 对象存储厂商
|
|
const providers = reactive<Provider[]>([
|
|
{
|
|
id: 'aliyun', name: '阿里云 OSS', short: 'OSS', tag: 'Aliyun', color: '#ff6a00', enable: true,
|
|
fields: [
|
|
{ key: 'region', label: 'Region', value: 'oss-cn-shanghai', placeholder: 'oss-cn-shanghai', mono: true, required: true },
|
|
{ key: 'endpoint', label: 'Endpoint', value: 'oss-cn-shanghai.aliyuncs.com', mono: true, required: true },
|
|
{ key: 'bucket', label: 'Bucket', value: 'yunyan-firmware', mono: true, required: true },
|
|
{ key: 'accessKeyId', label: 'AccessKey ID', value: 'LTAI5txxxxxxxx', mono: true, encrypted: true, required: true },
|
|
{ key: 'accessKeySecret', label: 'AccessKey Secret', value: '', mono: true, encrypted: true, required: true, hint: '留空保留原密文' },
|
|
{ key: 'domain', label: '自定义域名 (CDN)', value: 'https://cdn.yunyan.com', wide: true, hint: '可选,配置后文件 URL 走该域名' },
|
|
],
|
|
},
|
|
{
|
|
id: 'tencent', name: '腾讯云 COS', short: 'COS', tag: 'Tencent', color: '#00a4ff', enable: true,
|
|
fields: [
|
|
{ key: 'region', label: 'Region', value: 'ap-guangzhou', mono: true, required: true },
|
|
{ key: 'appid', label: 'AppId', value: '1253517901', mono: true, required: true },
|
|
{ key: 'bucket', label: 'Bucket', value: 'yunyan-1253517901', mono: true, required: true },
|
|
{ key: 'secretId', label: 'SecretId', value: 'AKIDxxxxxxxx', mono: true, encrypted: true, required: true },
|
|
{ key: 'secretKey', label: 'SecretKey', value: '', mono: true, encrypted: true, required: true, hint: '留空保留原密文' },
|
|
{ key: 'domain', label: '自定义域名 (CDN)', value: '', wide: true, hint: '可选' },
|
|
],
|
|
},
|
|
{
|
|
id: 'qiniu', name: '七牛云 Kodo', short: 'KODO', tag: 'Qiniu', color: '#00aaff', enable: false,
|
|
fields: [
|
|
{ key: 'zone', label: '存储区域', value: '', mono: true, required: true },
|
|
{ key: 'bucket', label: 'Bucket', value: '', mono: true, required: true },
|
|
{ key: 'accessKey', label: 'AccessKey', value: '', mono: true, encrypted: true, required: true },
|
|
{ key: 'secretKey', label: 'SecretKey', value: '', mono: true, encrypted: true, required: true },
|
|
{ key: 'domain', label: '加速域名', value: '', wide: true },
|
|
],
|
|
},
|
|
{
|
|
id: 'minio', name: 'MinIO', short: 'S3', tag: 'S3 兼容', color: '#c72e49', enable: false,
|
|
fields: [
|
|
{ key: 'endpoint', label: 'Endpoint', value: '', mono: true, required: true, placeholder: 'minio.内网:9000' },
|
|
{ key: 'bucket', label: 'Bucket', value: '', mono: true, required: true },
|
|
{ key: 'accessKey', label: 'AccessKey', value: '', mono: true, encrypted: true, required: true },
|
|
{ key: 'secretKey', label: 'SecretKey', value: '', mono: true, encrypted: true, required: true },
|
|
{ key: 'useSSL', label: '使用 HTTPS', value: 'false', mono: true },
|
|
],
|
|
},
|
|
])
|
|
|
|
const activeId = ref('aliyun') // 当前固件/默认存储
|
|
const curProvider = ref('aliyun')
|
|
const editProvider = computed(() => providers.find((p) => p.id === curProvider.value)!)
|
|
const activeProvider = computed(() => providers.find((p) => p.id === activeId.value)!)
|
|
|
|
function setDefault() {
|
|
setDefaultById(curProvider.value)
|
|
}
|
|
function setDefaultById(id: string) {
|
|
const p = providers.find((x) => x.id === id)
|
|
if (!p) return
|
|
activeId.value = id
|
|
curProvider.value = id
|
|
p.enable = true
|
|
success(`已将「${p.name}」设为默认对象存储,固件 / 头像等文件上传将指向该存储桶`)
|
|
}
|
|
function testConn() {
|
|
info ? info(`正在测试 ${editProvider.value.name} 连接…(演示)`) : success('连接测试通过(演示)')
|
|
setTimeout(() => success(`${editProvider.value.name} 连接正常,Bucket 可写`), 600)
|
|
}
|
|
function save() {
|
|
success('配置已保存(演示,未落库)')
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.cat-item {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 10px 12px; border-radius: 11px;
|
|
border: 1px solid #eef2f7; background: #fff; cursor: pointer; transition: all .15s;
|
|
}
|
|
.cat-item:hover { border-color: #c7d2fe; background: #f5f3ff; }
|
|
.cat-item.active { border-color: #6366f1; background: linear-gradient(135deg,#eef2ff,#f5f3ff); box-shadow: 0 4px 12px rgba(99,102,241,.12); }
|
|
.cat-icon { width: 32px; height: 32px; border-radius: 9px; display: flex; align-items: center; justify-content: center; color: #fff; flex-shrink: 0; }
|
|
.dot-on { width: 7px; height: 7px; border-radius: 50%; background: #10b981; box-shadow: 0 0 0 3px rgba(16,185,129,.15); flex-shrink: 0; }
|
|
|
|
.provider-card {
|
|
padding: 12px 14px; border-radius: 13px; border: 1.5px solid #eef2f7; background: #fff; cursor: pointer; transition: all .15s;
|
|
}
|
|
.provider-card:hover { border-color: #c7d2fe; }
|
|
.provider-card.active { border-color: #6366f1; background: linear-gradient(135deg,#eef2ff,#faf5ff); box-shadow: 0 4px 14px rgba(99,102,241,.12); }
|
|
.provider-card.is-default { box-shadow: 0 0 0 1.5px #10b981 inset; }
|
|
.provider-logo { width: 34px; height: 34px; border-radius: 9px; display: flex; align-items: center; justify-content: center; color: #fff; font-weight: 800; font-size: 12px; letter-spacing: -.02em; }
|
|
.default-badge { font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 6px; background: #dcfce7; color: #15803d; }
|
|
.default-pick { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 600; color: #94a3b8; cursor: pointer; padding: 3px 8px; border-radius: 7px; border: 1px solid #eef2f7; transition: all .15s; }
|
|
.default-pick:hover { border-color: #c7d2fe; color: #6366f1; }
|
|
.default-pick.on { color: #15803d; background: #dcfce7; border-color: #bbf7d0; }
|
|
|
|
.lock-tag { font-size: 10px; font-weight: 600; color: #dc2626; background: #fef2f2; padding: 1px 6px; border-radius: 5px; }
|
|
.rule-box { background: #f8fafc; border: 1px solid #eef2f7; border-radius: 11px; padding: 10px 14px; }
|
|
</style>
|
|
|