|
|
@@ -0,0 +1,539 @@
|
|
|
+<template>
|
|
|
+ <div class="lepao-proxy-page">
|
|
|
+ <Breadcrumb :items="['网站管理', '乐跑出口代理']" />
|
|
|
+
|
|
|
+ <a-spin :loading="pageLoading">
|
|
|
+ <a-card class="hero-card" :bordered="false">
|
|
|
+ <div class="hero-inner">
|
|
|
+ <div class="hero-title">
|
|
|
+ <span class="t1">乐跑出站代理</span>
|
|
|
+ <a-tag color="arcoblue" size="small">青果 · 通道提取</a-tag>
|
|
|
+ </div>
|
|
|
+ <div class="hero-desc">
|
|
|
+ 对学校 HTTPS 接口经 HTTP 代理出口;密钥在服务端 <code>qgChannelProxy</code>。
|
|
|
+ <a-link href="https://www.qg.net/doc/1850.html" target="_blank" rel="noopener">资源地区 API</a-link>
|
|
|
+ ·
|
|
|
+ <a-link href="https://www.qg.net/doc/1846.html" target="_blank" rel="noopener">提取 IP</a-link>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-row :gutter="16" style="margin-top: 16px">
|
|
|
+ <a-col :span="24">
|
|
|
+ <a-card title="当前状态" :bordered="false" class="panel">
|
|
|
+ <a-descriptions :column="2" bordered size="small">
|
|
|
+ <a-descriptions-item label="extractKey">
|
|
|
+ <a-tag :color="status?.extract_key_configured ? 'green' : 'orangered'">
|
|
|
+ {{ status?.extract_key_configured ? '已配置' : '未配置' }}
|
|
|
+ </a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="代理账密">
|
|
|
+ <a-tag :color="status?.proxy_auth_configured ? 'cyan' : 'gray'">
|
|
|
+ {{ status?.proxy_auth_configured ? '已配置' : '可选(白名单可不填)' }}
|
|
|
+ </a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="代理节点 server">
|
|
|
+ <span class="mono">{{ status?.current_proxy?.server ?? '—' }}</span>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="节点 IP 属地">
|
|
|
+ <a-tag color="purple">{{ status?.current_proxy?.node_region ?? '—' }}</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="出口 IP proxy_ip">
|
|
|
+ <span class="mono">{{ status?.current_proxy?.proxy_ip ?? '—' }}</span>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="出口 IP 属地">
|
|
|
+ <a-tag color="gold">{{ status?.current_proxy?.proxy_ip_region ?? '—' }}</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="deadline" :span="2">
|
|
|
+ {{ status?.current_proxy?.deadline ?? '—' }}
|
|
|
+ <a-tag
|
|
|
+ v-if="status?.current_proxy"
|
|
|
+ size="small"
|
|
|
+ :color="status?.current_proxy?.stale ? 'orangered' : 'green'"
|
|
|
+ style="margin-left: 8px"
|
|
|
+ >
|
|
|
+ {{ status?.current_proxy?.stale ? '临近/已过期' : '有效期内' }}
|
|
|
+ </a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ </a-descriptions>
|
|
|
+ </a-card>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+
|
|
|
+ <a-row :gutter="16" style="margin-top: 16px">
|
|
|
+ <a-col :xs="24" :lg="16">
|
|
|
+ <a-card title="筛选与路由" :bordered="false" class="panel">
|
|
|
+ <a-form :model="form" layout="vertical">
|
|
|
+ <a-form-item label="启用青果出站">
|
|
|
+ <a-switch v-model="form.proxy_enabled" />
|
|
|
+ </a-form-item>
|
|
|
+
|
|
|
+ <a-form-item label="从青果可选资源勾选地区(支持搜索)">
|
|
|
+ <a-space direction="vertical" fill style="width: 100%">
|
|
|
+ <a-space wrap>
|
|
|
+ <a-button size="mini" type="outline" @click="loadResources" :loading="resLoading">
|
|
|
+ 刷新资源列表
|
|
|
+ </a-button>
|
|
|
+ <a-checkbox v-model="onlyAvailableRes">仅显示「可提取」</a-checkbox>
|
|
|
+ </a-space>
|
|
|
+ <a-select
|
|
|
+ v-model="selectedAreaCodes"
|
|
|
+ multiple
|
|
|
+ allow-search
|
|
|
+ allow-clear
|
|
|
+ :loading="resLoading"
|
|
|
+ :options="filteredResourceOptions"
|
|
|
+ :placeholder="resourcePlaceholder"
|
|
|
+ :filter-option="filterResourceOption"
|
|
|
+ style="width: 100%"
|
|
|
+ />
|
|
|
+ <div class="hint-muted">
|
|
|
+ 将写入后端 area 字段(逗号分隔,与青果调试工具一致)。也可在下方微调编码。
|
|
|
+ </div>
|
|
|
+ <a-input
|
|
|
+ v-model="form.area"
|
|
|
+ allow-clear
|
|
|
+ placeholder="area 编码,逗号分隔(与上方选择同步,失焦后与多选对齐)"
|
|
|
+ @blur="syncSelectedFromArea"
|
|
|
+ />
|
|
|
+ </a-space>
|
|
|
+ </a-form-item>
|
|
|
+
|
|
|
+ <a-form-item label="排除地区 area_ex">
|
|
|
+ <a-input v-model="form.area_ex" allow-clear placeholder="可选,逗号分隔" />
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="运营商 isp">
|
|
|
+ <a-select v-model="form.isp" allow-clear placeholder="不筛选">
|
|
|
+ <a-option :value="undefined">不筛选</a-option>
|
|
|
+ <a-option :value="1">电信</a-option>
|
|
|
+ <a-option :value="2">移动</a-option>
|
|
|
+ <a-option :value="3">联通</a-option>
|
|
|
+ </a-select>
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="distinct 去重提取">
|
|
|
+ <a-switch v-model="form.distinct_extract" />
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="保存时清空 Redis 当前 IP">
|
|
|
+ <a-checkbox v-model="form.invalidate_cache">清空(下次强制重新提取)</a-checkbox>
|
|
|
+ </a-form-item>
|
|
|
+ <a-space>
|
|
|
+ <a-button type="primary" @click="saveConfig" :loading="saving">保存配置</a-button>
|
|
|
+ <a-button @click="loadStatus">刷新状态</a-button>
|
|
|
+ </a-space>
|
|
|
+ </a-form>
|
|
|
+ </a-card>
|
|
|
+ </a-col>
|
|
|
+ <a-col :xs="24" :lg="8">
|
|
|
+ <a-card title="说明" :bordered="false" class="panel side-tips">
|
|
|
+ <ul class="tips-list">
|
|
|
+ <li>资源列表来自青果「查询资源地区」接口,请以控制台实际可用为准。</li>
|
|
|
+ <li>日志表中「出口 IP」为青果 proxy_ip;「出口属地」仅据此 IP 解析(ip2region)。</li>
|
|
|
+ <li>单通道时请避免频繁作废 IP;后端已对 /get 与 POST 做多轮退让重试。</li>
|
|
|
+ </ul>
|
|
|
+ </a-card>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+
|
|
|
+ <a-card title="出站与切换摘要" class="panel log-card" style="margin-top: 16px" :bordered="false">
|
|
|
+ <template #extra>
|
|
|
+ <a-space>
|
|
|
+ <a-button
|
|
|
+ type="primary"
|
|
|
+ status="danger"
|
|
|
+ :disabled="!selectedLogKeys.length"
|
|
|
+ :loading="logDeleting"
|
|
|
+ @click="deleteSelectedLogs"
|
|
|
+ >
|
|
|
+ 删除所选({{ selectedLogKeys.length }})
|
|
|
+ </a-button>
|
|
|
+ <a-button type="outline" status="danger" :loading="logDeleting" @click="confirmDeleteAllLogs">
|
|
|
+ 清空全部
|
|
|
+ </a-button>
|
|
|
+ </a-space>
|
|
|
+ </template>
|
|
|
+ <a-table
|
|
|
+ v-model:selected-keys="selectedLogKeys"
|
|
|
+ row-key="id"
|
|
|
+ :row-selection="{ type: 'checkbox', showCheckedAll: true }"
|
|
|
+ :data="logData"
|
|
|
+ :loading="logLoading"
|
|
|
+ :bordered="false"
|
|
|
+ :pagination="{
|
|
|
+ showPageSize: true,
|
|
|
+ showJumper: true,
|
|
|
+ showTotal: true,
|
|
|
+ pageSize: pagination.pagesize,
|
|
|
+ current: pagination.current,
|
|
|
+ total: pagination.total
|
|
|
+ }"
|
|
|
+ @page-change="handlePageChange"
|
|
|
+ @page-size-change="handlePageSizeChange"
|
|
|
+ >
|
|
|
+ <template #columns>
|
|
|
+ <a-table-column title="时间" :width="178">
|
|
|
+ <template #cell="{ record }">{{ strTime(record.created_at) }}</template>
|
|
|
+ </a-table-column>
|
|
|
+ <a-table-column title="类型" :width="120">
|
|
|
+ <template #cell="{ record }">
|
|
|
+ <a-tag size="small" :color="record.event_color">{{ record.event_label }}</a-tag>
|
|
|
+ </template>
|
|
|
+ </a-table-column>
|
|
|
+ <a-table-column title="节点 server" data-index="server" :width="160" ellipsis tooltip />
|
|
|
+ <a-table-column title="出口 IP" :width="130">
|
|
|
+ <template #cell="{ record }">
|
|
|
+ <span class="mono">{{ record.egress_ip ?? '—' }}</span>
|
|
|
+ </template>
|
|
|
+ </a-table-column>
|
|
|
+ <a-table-column title="出口属地" :width="200">
|
|
|
+ <template #cell="{ record }">
|
|
|
+ <span v-if="!record.egress_ip" class="hint-muted">—</span>
|
|
|
+ <a-tag v-else color="cyan" size="small">{{ shortenRegion(record.egress_region || '未知') }}</a-tag>
|
|
|
+ </template>
|
|
|
+ </a-table-column>
|
|
|
+ <a-table-column title="摘要">
|
|
|
+ <template #cell="{ record }">
|
|
|
+ <div class="summary-text">{{ record.summary }}</div>
|
|
|
+ </template>
|
|
|
+ </a-table-column>
|
|
|
+ <a-table-column title="操作" fixed="right" :width="88">
|
|
|
+ <template #cell="{ record }">
|
|
|
+ <a-popconfirm content="删除该条日志?" @ok="deleteOneLog(record.id)">
|
|
|
+ <a-button type="text" size="mini" status="danger">删除</a-button>
|
|
|
+ </a-popconfirm>
|
|
|
+ </template>
|
|
|
+ </a-table-column>
|
|
|
+ </template>
|
|
|
+ </a-table>
|
|
|
+ </a-card>
|
|
|
+ </a-spin>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { reactive, ref, computed, watch, onMounted } from 'vue'
|
|
|
+import { Notification, Modal } from '@arco-design/web-vue'
|
|
|
+import {
|
|
|
+ getAdminLepaoProxyStatus,
|
|
|
+ postAdminLepaoProxyConfig,
|
|
|
+ getAdminLepaoProxyLogs,
|
|
|
+ postAdminLepaoProxyLogsDelete,
|
|
|
+ getAdminLepaoProxyResources
|
|
|
+} from '@/api/lepao'
|
|
|
+
|
|
|
+const pageLoading = ref(false)
|
|
|
+const logLoading = ref(false)
|
|
|
+const logDeleting = ref(false)
|
|
|
+const selectedLogKeys = ref([])
|
|
|
+const saving = ref(false)
|
|
|
+const status = ref(null)
|
|
|
+const logData = ref([])
|
|
|
+const resourcesRaw = ref([])
|
|
|
+const resLoading = ref(false)
|
|
|
+const onlyAvailableRes = ref(true)
|
|
|
+const selectedAreaCodes = ref([])
|
|
|
+
|
|
|
+const pagination = reactive({
|
|
|
+ total: 0,
|
|
|
+ current: 1,
|
|
|
+ pagesize: 20
|
|
|
+})
|
|
|
+
|
|
|
+const form = reactive({
|
|
|
+ proxy_enabled: false,
|
|
|
+ area: '',
|
|
|
+ area_ex: '',
|
|
|
+ isp: undefined,
|
|
|
+ distinct_extract: true,
|
|
|
+ invalidate_cache: false
|
|
|
+})
|
|
|
+
|
|
|
+watch(
|
|
|
+ selectedAreaCodes,
|
|
|
+ (v) => {
|
|
|
+ form.area = Array.isArray(v) && v.length ? v.join(',') : ''
|
|
|
+ },
|
|
|
+ { deep: true }
|
|
|
+)
|
|
|
+
|
|
|
+const filteredResourceOptions = computed(() => {
|
|
|
+ const rows = resourcesRaw.value || []
|
|
|
+ let list = onlyAvailableRes.value ? rows.filter((r) => r.available === true) : [...rows]
|
|
|
+ return list.map((r) => {
|
|
|
+ const code = String(r.area_code ?? '')
|
|
|
+ const isp = r.isp ?? ''
|
|
|
+ const ok = r.available ? '可提取' : '暂不可用'
|
|
|
+ return {
|
|
|
+ label: `[${code}] ${r.area ?? ''} · ${isp} · ${ok}`,
|
|
|
+ value: code
|
|
|
+ }
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+const resourcePlaceholder = computed(() =>
|
|
|
+ resourcesRaw.value.length ? '搜索城市 / 运营商 / 编码…' : '请先点击「刷新资源列表」'
|
|
|
+)
|
|
|
+
|
|
|
+function filterResourceOption(input, option) {
|
|
|
+ return String(option?.label ?? '')
|
|
|
+ .toLowerCase()
|
|
|
+ .includes(String(input || '').toLowerCase())
|
|
|
+}
|
|
|
+
|
|
|
+function shortenRegion(r) {
|
|
|
+ if (!r || r === '未知') return '未知'
|
|
|
+ const parts = String(r).split(' · ')
|
|
|
+ return parts.slice(-2).join(' · ') || r
|
|
|
+}
|
|
|
+
|
|
|
+const strTime = (t) =>
|
|
|
+ t == null
|
|
|
+ ? '—'
|
|
|
+ : new Date(Number(t)).toLocaleString('zh-CN', {
|
|
|
+ year: 'numeric',
|
|
|
+ month: '2-digit',
|
|
|
+ day: '2-digit',
|
|
|
+ hour: '2-digit',
|
|
|
+ minute: '2-digit',
|
|
|
+ second: '2-digit'
|
|
|
+ })
|
|
|
+
|
|
|
+const syncSelectedFromArea = () => {
|
|
|
+ selectedAreaCodes.value = form.area
|
|
|
+ ? String(form.area)
|
|
|
+ .split(',')
|
|
|
+ .map((s) => s.trim())
|
|
|
+ .filter(Boolean)
|
|
|
+ .map(String)
|
|
|
+ : []
|
|
|
+}
|
|
|
+
|
|
|
+const applyStatus = (data) => {
|
|
|
+ status.value = data
|
|
|
+ form.proxy_enabled = !!data?.proxy_enabled
|
|
|
+ form.area = data?.area ?? ''
|
|
|
+ form.area_ex = data?.area_ex ?? ''
|
|
|
+ form.isp = data?.isp === null || data?.isp === '' ? undefined : Number(data.isp)
|
|
|
+ form.distinct_extract = data?.distinct_extract !== false
|
|
|
+ syncSelectedFromArea()
|
|
|
+}
|
|
|
+
|
|
|
+const loadStatus = async () => {
|
|
|
+ pageLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await getAdminLepaoProxyStatus({})
|
|
|
+ if (!res || res.code !== 0) {
|
|
|
+ Notification.error({ title: '读取状态失败', content: res?.msg ?? '请稍后再试' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ applyStatus(res.data)
|
|
|
+ } finally {
|
|
|
+ pageLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const loadResources = async () => {
|
|
|
+ resLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await getAdminLepaoProxyResources({})
|
|
|
+ if (!res || res.code !== 0) {
|
|
|
+ Notification.error({ title: '资源列表获取失败', content: res?.msg ?? '请检查 extractKey 与网络' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ resourcesRaw.value = res.data || []
|
|
|
+ Notification.success({ title: '资源列表已更新', content: `共 ${resourcesRaw.value.length} 条` })
|
|
|
+ } finally {
|
|
|
+ resLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const loadLogs = async () => {
|
|
|
+ logLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await getAdminLepaoProxyLogs({
|
|
|
+ current: pagination.current,
|
|
|
+ pagesize: pagination.pagesize
|
|
|
+ })
|
|
|
+ if (!res || res.code !== 0) {
|
|
|
+ Notification.error({ title: '读取日志失败', content: res?.msg ?? '请稍后再试' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ logData.value = res.data || []
|
|
|
+ pagination.total = res.pagination?.total || 0
|
|
|
+ selectedLogKeys.value = []
|
|
|
+ } finally {
|
|
|
+ logLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const deleteOneLog = async (id) => {
|
|
|
+ logDeleting.value = true
|
|
|
+ try {
|
|
|
+ const res = await postAdminLepaoProxyLogsDelete({ ids: [id] })
|
|
|
+ if (!res || res.code !== 0) {
|
|
|
+ Notification.error({ title: '删除失败', content: res?.msg ?? '请稍后再试' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ Notification.success({ title: '已删除', content: '' })
|
|
|
+ await loadLogs()
|
|
|
+ } finally {
|
|
|
+ logDeleting.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const deleteSelectedLogs = async () => {
|
|
|
+ if (!selectedLogKeys.value.length) return
|
|
|
+ logDeleting.value = true
|
|
|
+ try {
|
|
|
+ const res = await postAdminLepaoProxyLogsDelete({ ids: selectedLogKeys.value.map(Number) })
|
|
|
+ if (!res || res.code !== 0) {
|
|
|
+ Notification.error({ title: '删除失败', content: res?.msg ?? '请稍后再试' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ Notification.success({ title: `已删除 ${res.data?.deleted ?? selectedLogKeys.value.length} 条`, content: '' })
|
|
|
+ pagination.current = 1
|
|
|
+ await loadLogs()
|
|
|
+ } finally {
|
|
|
+ logDeleting.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const confirmDeleteAllLogs = () => {
|
|
|
+ Modal.confirm({
|
|
|
+ title: '清空全部代理日志',
|
|
|
+ content: '将删除表中全部 lepao_proxy_log 记录,不可恢复。',
|
|
|
+ okText: '确认清空',
|
|
|
+ modalStyle: { maxWidth: '420px' },
|
|
|
+ okButtonProps: { status: 'danger' },
|
|
|
+ onOk: async () => {
|
|
|
+ logDeleting.value = true
|
|
|
+ try {
|
|
|
+ const res = await postAdminLepaoProxyLogsDelete({ purge_all: 1 })
|
|
|
+ if (!res || res.code !== 0) {
|
|
|
+ Notification.error({ title: '清空失败', content: res?.msg ?? '请稍后再试' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ Notification.success({ title: '已清空日志', content: '' })
|
|
|
+ pagination.current = 1
|
|
|
+ await loadLogs()
|
|
|
+ } finally {
|
|
|
+ logDeleting.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const saveConfig = async () => {
|
|
|
+ saving.value = true
|
|
|
+ try {
|
|
|
+ const res = await postAdminLepaoProxyConfig({
|
|
|
+ proxy_enabled: form.proxy_enabled ? 1 : 0,
|
|
|
+ area: form.area,
|
|
|
+ area_ex: form.area_ex,
|
|
|
+ isp: form.isp === undefined ? '' : form.isp,
|
|
|
+ distinct_extract: form.distinct_extract ? 1 : 0,
|
|
|
+ invalidate_cache: form.invalidate_cache ? 1 : 0
|
|
|
+ })
|
|
|
+ if (!res || res.code !== 0) {
|
|
|
+ Notification.error({ title: '保存失败', content: res?.msg ?? '请稍后再试' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ Notification.success({ title: '已保存', content: '' })
|
|
|
+ form.invalidate_cache = false
|
|
|
+ await loadStatus()
|
|
|
+ await loadLogs()
|
|
|
+ } finally {
|
|
|
+ saving.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handlePageChange = (page) => {
|
|
|
+ pagination.current = page
|
|
|
+ loadLogs()
|
|
|
+}
|
|
|
+
|
|
|
+const handlePageSizeChange = (size) => {
|
|
|
+ pagination.pagesize = size
|
|
|
+ pagination.current = 1
|
|
|
+ loadLogs()
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ await loadStatus()
|
|
|
+ await loadLogs()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="less">
|
|
|
+.lepao-proxy-page {
|
|
|
+ padding: 16px 20px 32px;
|
|
|
+ max-width: 1280px;
|
|
|
+ margin: 0 auto;
|
|
|
+}
|
|
|
+
|
|
|
+.hero-card {
|
|
|
+ background: linear-gradient(135deg, var(--color-bg-2), var(--color-fill-2));
|
|
|
+}
|
|
|
+
|
|
|
+.hero-inner {
|
|
|
+ padding: 4px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.hero-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+
|
|
|
+ .t1 {
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--color-text-1);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.hero-desc {
|
|
|
+ margin-top: 10px;
|
|
|
+ color: var(--color-text-2);
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.6;
|
|
|
+
|
|
|
+ code {
|
|
|
+ padding: 0 6px;
|
|
|
+ border-radius: 4px;
|
|
|
+ background: var(--color-fill-3);
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.panel {
|
|
|
+ border-radius: 12px;
|
|
|
+
|
|
|
+ &.log-card {
|
|
|
+ box-shadow: 0 1px 4px rgb(0 0 0 / 4%);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.mono {
|
|
|
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.side-tips .tips-list {
|
|
|
+ margin: 0;
|
|
|
+ padding-left: 18px;
|
|
|
+ color: var(--color-text-2);
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.85;
|
|
|
+}
|
|
|
+
|
|
|
+.summary-text {
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.65;
|
|
|
+ color: var(--color-text-2);
|
|
|
+}
|
|
|
+
|
|
|
+.hint-muted {
|
|
|
+ color: var(--color-text-3);
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+</style>
|