17 changed files with 1008 additions and 50 deletions
@ -1 +1 @@ |
|||
{"id":"dev","timestamp":1782613791473} |
|||
{"id":"dev","timestamp":1782719244117} |
|||
@ -1 +1 @@ |
|||
{"id":"dev","timestamp":1782613791473,"prerendered":[]} |
|||
{"id":"dev","timestamp":1782719244117,"prerendered":[]} |
|||
@ -0,0 +1,9 @@ |
|||
{ |
|||
"pid": 9287, |
|||
"startedAt": 1782719243662, |
|||
"command": "dev", |
|||
"cwd": "/Users/liwei/work/go/yunyan/yunyan-sas/apps/admin", |
|||
"port": 3000, |
|||
"hostname": "::1", |
|||
"url": "http://[::1]:3000" |
|||
} |
|||
@ -0,0 +1,256 @@ |
|||
<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。由「应用注册」列表点击「服务配置」进入,配置随应用隔离。</span> |
|||
</div> |
|||
|
|||
<!-- 返回 + 标题 --> |
|||
<div class="flex items-center gap-2 mb-4"> |
|||
<button class="btn btn-ghost btn-sm" @click="goBack">← 返回应用列表</button> |
|||
<span class="text-slate-300">/</span> |
|||
<h2 class="text-lg font-semibold text-slate-800">应用服务配置</h2> |
|||
</div> |
|||
|
|||
<!-- 应用信息条 --> |
|||
<div class="rounded-xl px-5 py-4 mb-5 flex items-center gap-4" style="background:linear-gradient(135deg,#eef2ff,#faf5ff);border:1px solid #e0e7ff"> |
|||
<div class="app-avatar">{{ appInitial }}</div> |
|||
<div class="flex-1 min-w-0"> |
|||
<div class="flex items-center gap-2"> |
|||
<span class="text-base font-bold text-slate-800">{{ appName }}</span> |
|||
<span class="badge badge-sm" style="background:#e0e7ff;color:#4338ca;border:none">#{{ appId }}</span> |
|||
<span v-if="appRegion" class="badge badge-ghost badge-sm">{{ appRegion }}</span> |
|||
</div> |
|||
<div class="text-[11px] text-slate-500 mt-0.5">正在配置该应用的第三方登录、支付、推送与基础信息;仅对本应用生效,与其他应用隔离。</div> |
|||
</div> |
|||
<span class="badge badge-success badge-sm text-white">SaaS 独立配置</span> |
|||
</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="c.count" class="cnt-badge">{{ c.count }}</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 右侧详情 --> |
|||
<div class="flex-1 min-w-0 flex flex-col gap-3"> |
|||
<!-- ===== 登录认证 ===== --> |
|||
<template v-if="curCat === 'auth'"> |
|||
<div v-for="p in authProviders" :key="p.id" class="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden"> |
|||
<div class="flex items-center gap-3 px-5 py-4 cursor-pointer" @click="toggleOpen(p)"> |
|||
<div class="provider-logo" :style="{ background: p.color }">{{ p.short }}</div> |
|||
<div class="flex-1 min-w-0"> |
|||
<div class="flex items-center gap-2"> |
|||
<span class="text-sm font-semibold text-slate-800">{{ p.name }}</span> |
|||
<span class="badge badge-xs" :class="p.enable ? 'badge-success text-white' : 'badge-ghost'">{{ p.enable ? '已启用' : '未启用' }}</span> |
|||
</div> |
|||
<div class="text-[11px] text-slate-400 mt-0.5">{{ p.desc }}</div> |
|||
</div> |
|||
<input v-model="p.enable" type="checkbox" class="toggle toggle-primary toggle-sm" @click.stop /> |
|||
<svg class="w-4 h-4 text-slate-300 transition-transform" :class="{ 'rotate-180': p._open }" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /></svg> |
|||
</div> |
|||
<div v-show="p._open" class="px-5 pb-5 pt-1 border-t border-slate-50"> |
|||
<p v-if="p.note" class="text-[11px] text-slate-500 bg-slate-50 rounded-lg px-3 py-2 mb-3">{{ p.note }}</p> |
|||
<div class="grid grid-cols-2 gap-x-5 gap-y-3"> |
|||
<div v-for="f in p.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" :readonly="f.readonly" /> |
|||
<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="flex justify-end mt-4"> |
|||
<button class="btn btn-primary btn-sm text-white" @click="save">保存</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<!-- ===== 支付配置 ===== --> |
|||
<template v-else-if="curCat === 'pay'"> |
|||
<div v-for="p in payProviders" :key="p.id" class="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden"> |
|||
<div class="flex items-center gap-3 px-5 py-4 cursor-pointer" @click="toggleOpen(p)"> |
|||
<div class="provider-logo" :style="{ background: p.color }">{{ p.short }}</div> |
|||
<div class="flex-1 min-w-0"> |
|||
<div class="flex items-center gap-2"> |
|||
<span class="text-sm font-semibold text-slate-800">{{ p.name }}</span> |
|||
<span class="badge badge-xs" :class="p.enable ? 'badge-success text-white' : 'badge-ghost'">{{ p.enable ? '已启用' : '未启用' }}</span> |
|||
</div> |
|||
<div class="text-[11px] text-slate-400 mt-0.5">{{ p.desc }}</div> |
|||
</div> |
|||
<input v-model="p.enable" type="checkbox" class="toggle toggle-primary toggle-sm" @click.stop /> |
|||
<svg class="w-4 h-4 text-slate-300 transition-transform" :class="{ 'rotate-180': p._open }" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /></svg> |
|||
</div> |
|||
<div v-show="p._open" class="px-5 pb-5 pt-1 border-t border-slate-50"> |
|||
<div class="grid grid-cols-2 gap-x-5 gap-y-3"> |
|||
<div v-for="f in p.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.encrypted" class="lock-tag ml-1.5">🔒</span></span></label> |
|||
<input v-model="f.value" :type="f.encrypted ? 'password' : 'text'" class="input input-bordered input-sm w-full" :class="{ 'font-mono': f.mono }" :placeholder="f.placeholder" /> |
|||
</div> |
|||
</div> |
|||
<div class="flex justify-end mt-4"><button class="btn btn-primary btn-sm text-white" @click="save">保存</button></div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<!-- ===== 消息推送 ===== --> |
|||
<template v-else-if="curCat === 'push'"> |
|||
<div class="bg-white rounded-2xl border border-slate-100 shadow-sm p-6"> |
|||
<h3 class="text-base font-semibold text-slate-800 mb-1">消息推送</h3> |
|||
<p class="text-xs text-slate-400 mb-4">推送通道(极光 / FCM / APNs)在「系统配置」统一接入,此处为各应用的开关与包名映射。</p> |
|||
<div class="grid grid-cols-2 gap-x-5 gap-y-3"> |
|||
<div class="form-control"><label class="label py-1"><span class="label-text text-sm">Android 包名</span></label><input v-model="push.androidPkg" class="input input-bordered input-sm font-mono" /></div> |
|||
<div class="form-control"><label class="label py-1"><span class="label-text text-sm">iOS Bundle ID</span></label><input v-model="push.iosBundle" class="input input-bordered input-sm font-mono" /></div> |
|||
<div class="form-control"><label class="label py-1"><span class="label-text text-sm">极光 AppKey(应用级)</span></label><input v-model="push.jpushKey" class="input input-bordered input-sm font-mono" /></div> |
|||
<div class="form-control"><label class="label py-1"><span class="label-text text-sm">推送总开关</span></label><input v-model="push.enable" type="checkbox" class="toggle toggle-primary mt-2" /></div> |
|||
</div> |
|||
<div class="flex justify-end mt-5"><button class="btn btn-primary btn-sm text-white" @click="save">保存</button></div> |
|||
</div> |
|||
</template> |
|||
|
|||
<!-- ===== 基础信息 ===== --> |
|||
<template v-else-if="curCat === 'basic'"> |
|||
<div class="bg-white rounded-2xl border border-slate-100 shadow-sm p-6"> |
|||
<h3 class="text-base font-semibold text-slate-800 mb-4">应用基础信息</h3> |
|||
<div class="grid grid-cols-2 gap-x-5 gap-y-3"> |
|||
<div class="form-control"><label class="label py-1"><span class="label-text text-sm">应用名称</span></label><input v-model="basic.name" class="input input-bordered input-sm" /></div> |
|||
<div class="form-control"><label class="label py-1"><span class="label-text text-sm">应用简称</span></label><input v-model="basic.shortName" class="input input-bordered input-sm" /></div> |
|||
<div class="form-control col-span-2"><label class="label py-1"><span class="label-text text-sm">应用 Logo URL</span></label><input v-model="basic.logo" class="input input-bordered input-sm font-mono" /></div> |
|||
<div class="form-control"><label class="label py-1"><span class="label-text text-sm">用户协议链接</span></label><input v-model="basic.terms" class="input input-bordered input-sm" /></div> |
|||
<div class="form-control"><label class="label py-1"><span class="label-text text-sm">隐私政策链接</span></label><input v-model="basic.privacy" class="input input-bordered input-sm" /></div> |
|||
<div class="form-control"><label class="label py-1"><span class="label-text text-sm">客服邮箱</span></label><input v-model="basic.support" class="input input-bordered input-sm" /></div> |
|||
<div class="form-control"><label class="label py-1"><span class="label-text text-sm">默认语言</span></label><input v-model="basic.lang" class="input input-bordered input-sm" /></div> |
|||
</div> |
|||
<div class="flex justify-end mt-5"><button class="btn btn-primary btn-sm text-white" @click="save">保存</button></div> |
|||
</div> |
|||
</template> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, reactive, computed } from 'vue' |
|||
import { useToast } from '~/composables/useToast' |
|||
|
|||
definePageMeta({ title: '应用服务配置' }) |
|||
|
|||
const { success } = useToast() |
|||
const route = useRoute() |
|||
const router = useRouter() |
|||
|
|||
interface Field { key: string; label: string; value: string; placeholder?: string; hint?: string; required?: boolean; encrypted?: boolean; mono?: boolean; wide?: boolean; readonly?: boolean; _show?: boolean } |
|||
interface ProviderCard { id: string; name: string; short: string; desc: string; color: string; enable: boolean; note?: string; fields: Field[]; _open?: boolean } |
|||
|
|||
// 应用上下文(由「应用注册」列表跳转携带 query) |
|||
const appId = computed(() => Number(route.query.id) || 0) |
|||
const appName = computed(() => (route.query.name as string) || '未指定应用') |
|||
const appRegion = computed(() => (route.query.region as string) || '') |
|||
const appInitial = computed(() => (appName.value || 'A').charAt(0).toUpperCase()) |
|||
function goBack() { router.push('/apps') } |
|||
|
|||
const categories = computed(() => [ |
|||
{ id: 'auth', label: '登录认证', desc: '微信 / Apple / Google', color: '#6366f1', count: authProviders.filter((p) => p.enable).length, icon: 'M12 11c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM12 11v6m0-6V5m6 6a3 3 0 11-6 0 3 3 0 016 0z' }, |
|||
{ id: 'pay', label: '支付配置', desc: '微信支付 / 支付宝', color: '#10b981', count: payProviders.filter((p) => p.enable).length, icon: 'M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z' }, |
|||
{ id: 'push', label: '消息推送', desc: '极光 / FCM / APNs', color: '#ec4899', count: 0, icon: 'M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9' }, |
|||
{ id: 'basic', label: '基础信息', desc: '名称 / Logo / 协议', color: '#f59e0b', count: 0, icon: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' }, |
|||
]) |
|||
const curCat = ref('auth') |
|||
|
|||
// 登录认证(重点:微信登录) |
|||
const authProviders = reactive<ProviderCard[]>([ |
|||
{ |
|||
id: 'wechat', name: '微信登录', short: '微', desc: '微信开放平台 App / 公众号授权登录', color: '#07c160', enable: true, _open: true, |
|||
note: '微信开放平台 → 创建移动应用,审核通过后获取 AppID 与 AppSecret。App 登录用「移动应用」,公众号网页授权用「公众号」的 AppID。', |
|||
fields: [ |
|||
{ key: 'appid', label: 'AppID', value: 'wx9f8e7d6c5b4a3210', mono: true, required: true }, |
|||
{ key: 'secret', label: 'AppSecret', value: '', mono: true, encrypted: true, required: true, hint: '留空保留原密文' }, |
|||
{ key: 'mch', label: '关联商户号', value: '', mono: true, hint: '可选,用于微信支付打通' }, |
|||
{ key: 'scope', label: '授权 scope', value: 'snsapi_userinfo', mono: true }, |
|||
], |
|||
}, |
|||
{ |
|||
id: 'apple', name: 'Apple 登录', short: '', desc: 'Sign in with Apple(iOS 上架必备)', color: '#111827', enable: true, _open: false, |
|||
fields: [ |
|||
{ key: 'clientId', label: 'Client ID (Bundle)', value: 'com.yunyan.voitrans', mono: true, required: true }, |
|||
{ key: 'teamId', label: 'Team ID', value: 'A1B2C3D4E5', mono: true, required: true }, |
|||
{ key: 'keyId', label: 'Key ID', value: 'XYZ1234567', mono: true, required: true }, |
|||
{ key: 'privateKey', label: 'AuthKey (.p8)', value: '', encrypted: true, wide: true, required: true, placeholder: '粘贴 .p8 私钥内容' }, |
|||
], |
|||
}, |
|||
{ |
|||
id: 'google', name: 'Google 登录', short: 'G', desc: 'Google OAuth 2.0(海外应用)', color: '#ea4335', enable: false, _open: false, |
|||
fields: [ |
|||
{ key: 'clientId', label: 'Client ID', value: '', mono: true, wide: true, required: true, placeholder: 'xxx.apps.googleusercontent.com' }, |
|||
{ key: 'clientSecret', label: 'Client Secret', value: '', mono: true, encrypted: true, required: true }, |
|||
], |
|||
}, |
|||
{ |
|||
id: 'phone', name: '手机短信登录', short: '号', desc: '手机号 + 验证码登录', color: '#0ea5e9', enable: true, _open: false, |
|||
note: '短信通道复用「系统配置 → 短信服务」,此处仅控制本应用是否开放手机号登录及验证码有效期。', |
|||
fields: [ |
|||
{ key: 'channel', label: '短信通道', value: '系统配置 · 腾讯云 SMS', readonly: true, wide: true }, |
|||
{ key: 'expire', label: '验证码有效期(秒)', value: '300', mono: true }, |
|||
], |
|||
}, |
|||
]) |
|||
|
|||
// 支付配置 |
|||
const payProviders = reactive<ProviderCard[]>([ |
|||
{ |
|||
id: 'wxpay', name: '微信支付', short: '微', desc: 'App / JSAPI 支付', color: '#07c160', enable: true, _open: false, |
|||
fields: [ |
|||
{ key: 'mchid', label: '商户号 MchId', value: '1612345678', mono: true }, |
|||
{ key: 'appid', label: '关联 AppID', value: 'wx9f8e7d6c5b4a3210', mono: true }, |
|||
{ key: 'apiv3', label: 'APIv3 密钥', value: '', mono: true, encrypted: true }, |
|||
{ key: 'serialNo', label: '证书序列号', value: '', mono: true, wide: true }, |
|||
], |
|||
}, |
|||
{ |
|||
id: 'alipay', name: '支付宝', short: '支', desc: 'App 支付', color: '#1677ff', enable: false, _open: false, |
|||
fields: [ |
|||
{ key: 'appid', label: 'AppID', value: '', mono: true }, |
|||
{ key: 'privateKey', label: '应用私钥', value: '', encrypted: true, wide: true }, |
|||
{ key: 'publicKey', label: '支付宝公钥', value: '', wide: true }, |
|||
], |
|||
}, |
|||
]) |
|||
|
|||
const push = reactive({ androidPkg: 'com.yunyan.voitrans', iosBundle: 'com.yunyan.voitrans', jpushKey: 'a1b2c3d4e5f6', enable: true }) |
|||
const basic = reactive({ name: 'Voitrans 同声传译', shortName: 'Voitrans', logo: 'https://cdn.yunyan.com/app/voitrans/logo.png', terms: 'https://yunyan.com/terms', privacy: 'https://yunyan.com/privacy', support: 'support@yunyan.com', lang: 'zh-CN' }) |
|||
|
|||
function toggleOpen(p: ProviderCard) { p._open = !p._open } |
|||
function save() { success(`「${appName.value}」的配置已保存(演示,未落库)`) } |
|||
</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; } |
|||
.cnt-badge { font-size: 11px; font-weight: 700; color: #4338ca; background: #e0e7ff; min-width: 20px; height: 20px; padding: 0 6px; border-radius: 10px; display: flex; align-items: center; justify-content: center; } |
|||
.provider-logo { width: 38px; height: 38px; border-radius: 10px; display: flex; align-items: center; justify-content: center; color: #fff; font-weight: 800; font-size: 14px; flex-shrink: 0; } |
|||
.lock-tag { font-size: 10px; font-weight: 600; color: #dc2626; background: #fef2f2; padding: 1px 6px; border-radius: 5px; } |
|||
.app-avatar { width: 48px; height: 48px; border-radius: 13px; background: linear-gradient(135deg,#4F46E5,#7C3AED); color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 20px; flex-shrink: 0; box-shadow: 0 6px 18px rgba(79,70,229,.3); } |
|||
</style> |
|||
@ -0,0 +1,514 @@ |
|||
<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> |
|||
Binary file not shown.
Loading…
Reference in new issue