669 lines
21 KiB
Vue
669 lines
21 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { message, Modal } from 'ant-design-vue'
|
||
import { ArrowRightOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||
import api from '@/api/http'
|
||
import JalaliDatePicker from '@/components/JalaliDatePicker.vue'
|
||
import MoneyInput from '@/components/MoneyInput.vue'
|
||
import TenantPanelShortcuts from '@/components/TenantPanelShortcuts.vue'
|
||
import { formatIranDate, todayIranDateInput, iranDateInputToIso } from '@/utils/datetime'
|
||
import { formatMoney } from '@/utils/money'
|
||
|
||
type ExpiryLevel = 'healthy' | 'warning' | 'critical' | 'unknown' | 'none'
|
||
|
||
type SupportItem = {
|
||
id: string
|
||
name: string
|
||
specs: string | null
|
||
description: string | null
|
||
priceRials: number | null
|
||
isFree: boolean
|
||
chargedAt: string | null
|
||
planMonths: number | null
|
||
expiresAt: string | null
|
||
alertDaysBefore: number
|
||
isActive: boolean
|
||
summary: {
|
||
expiryLevel: ExpiryLevel
|
||
expiryLabel: string
|
||
planLabel: string | null
|
||
expiresAt: string | null
|
||
}
|
||
charges: any[]
|
||
}
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const loading = ref(false)
|
||
const saving = ref(false)
|
||
const renewing = ref(false)
|
||
const creating = ref(false)
|
||
const selectedId = ref<string | null>(null)
|
||
const showCreate = ref(false)
|
||
|
||
const tenant = ref<{
|
||
id: string
|
||
product: 'hamkar' | 'khanegi'
|
||
slug: string
|
||
nameFa: string
|
||
domain: string
|
||
} | null>(null)
|
||
const supports = ref<SupportItem[]>([])
|
||
const planOptions = ref<{ months: number; label: string }[]>([])
|
||
|
||
const form = reactive({
|
||
name: '',
|
||
specs: '',
|
||
description: '',
|
||
priceRials: 0 as number,
|
||
isFree: false,
|
||
chargedAt: null as string | null,
|
||
planMonths: 1 as number,
|
||
alertDaysBefore: 3,
|
||
isActive: true,
|
||
})
|
||
|
||
const createForm = reactive({
|
||
name: '',
|
||
specs: '',
|
||
description: '',
|
||
priceRials: 0 as number,
|
||
isFree: false,
|
||
chargedAt: iranDateInputToIso(todayIranDateInput()) as string | null,
|
||
planMonths: 1,
|
||
alertDaysBefore: 3,
|
||
})
|
||
|
||
const renewForm = reactive({
|
||
chargedAt: iranDateInputToIso(todayIranDateInput()) as string | null,
|
||
planMonths: 1,
|
||
priceRials: 0 as number,
|
||
isFree: false,
|
||
note: '',
|
||
})
|
||
|
||
const tenantId = computed(() => String(route.params.tenantId || ''))
|
||
|
||
const selected = computed(() => supports.value.find((p) => p.id === selectedId.value) || null)
|
||
|
||
const planSelectOptions = computed(() =>
|
||
(planOptions.value.length
|
||
? planOptions.value
|
||
: [
|
||
{ months: 1, label: 'یکماهه' },
|
||
{ months: 3, label: 'سهماهه' },
|
||
{ months: 6, label: 'ششماهه' },
|
||
{ months: 12, label: 'یکساله' },
|
||
]
|
||
).map((p) => ({ value: p.months, label: p.label })),
|
||
)
|
||
|
||
function toneClass(level?: ExpiryLevel) {
|
||
return level && level !== 'none' ? level : 'unknown'
|
||
}
|
||
|
||
function fillForm(item: SupportItem) {
|
||
Object.assign(form, {
|
||
name: item.name || '',
|
||
specs: item.specs || '',
|
||
description: item.description || '',
|
||
priceRials: item.isFree ? 0 : (item.priceRials ?? 0),
|
||
isFree: Boolean(item.isFree),
|
||
chargedAt: item.chargedAt || null,
|
||
planMonths: item.planMonths || 1,
|
||
alertDaysBefore: item.alertDaysBefore ?? 3,
|
||
isActive: item.isActive !== false,
|
||
})
|
||
renewForm.planMonths = item.planMonths || 1
|
||
renewForm.isFree = Boolean(item.isFree)
|
||
renewForm.priceRials = item.isFree ? 0 : (item.priceRials ?? 0)
|
||
renewForm.chargedAt = iranDateInputToIso(todayIranDateInput())
|
||
renewForm.note = ''
|
||
}
|
||
|
||
function selectSupport(id: string) {
|
||
selectedId.value = id
|
||
const item = supports.value.find((p) => p.id === id)
|
||
if (item) fillForm(item)
|
||
}
|
||
|
||
async function loadDetail() {
|
||
if (!tenantId.value) return
|
||
loading.value = true
|
||
try {
|
||
const { data } = await api.get(`/support/tenants/${tenantId.value}`)
|
||
tenant.value = data.tenant
|
||
supports.value = data.supports || []
|
||
planOptions.value = data.planOptions || []
|
||
|
||
const keep =
|
||
selectedId.value && supports.value.some((p) => p.id === selectedId.value)
|
||
? selectedId.value
|
||
: supports.value[0]?.id || null
|
||
selectedId.value = keep
|
||
if (keep) {
|
||
const item = supports.value.find((p) => p.id === keep)
|
||
if (item) fillForm(item)
|
||
}
|
||
} catch (e: any) {
|
||
message.error(e?.response?.data?.message || 'خطا در دریافت پشتیبانیها')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function createSupport() {
|
||
if (!createForm.name.trim()) {
|
||
message.warning('نام پشتیبانی لازم است')
|
||
return
|
||
}
|
||
creating.value = true
|
||
try {
|
||
const isFree = Boolean(createForm.isFree)
|
||
const { data } = await api.post(`/support/tenants/${tenantId.value}`, {
|
||
name: createForm.name.trim(),
|
||
specs: createForm.specs || undefined,
|
||
description: createForm.description || undefined,
|
||
isFree,
|
||
priceRials: isFree ? 0 : Number(createForm.priceRials) || 0,
|
||
chargedAt: createForm.chargedAt || undefined,
|
||
planMonths: Number(createForm.planMonths),
|
||
alertDaysBefore: Number(createForm.alertDaysBefore) || 3,
|
||
})
|
||
message.success('پشتیبانی اضافه شد')
|
||
showCreate.value = false
|
||
createForm.name = ''
|
||
createForm.specs = ''
|
||
createForm.description = ''
|
||
createForm.isFree = false
|
||
createForm.priceRials = 0
|
||
createForm.chargedAt = iranDateInputToIso(todayIranDateInput())
|
||
createForm.planMonths = 1
|
||
createForm.alertDaysBefore = 3
|
||
await loadDetail()
|
||
if (data?.support?.id) selectSupport(data.support.id)
|
||
} catch (e: any) {
|
||
const msg = e?.response?.data?.message
|
||
message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'ثبت ناموفق')
|
||
} finally {
|
||
creating.value = false
|
||
}
|
||
}
|
||
|
||
async function saveSupport() {
|
||
if (!selectedId.value) return
|
||
if (!form.name.trim()) {
|
||
message.warning('نام پشتیبانی لازم است')
|
||
return
|
||
}
|
||
saving.value = true
|
||
try {
|
||
const isFree = Boolean(form.isFree)
|
||
await api.patch(`/support/items/${selectedId.value}`, {
|
||
name: form.name.trim(),
|
||
specs: form.specs || null,
|
||
description: form.description || null,
|
||
isFree,
|
||
priceRials: isFree ? 0 : Number(form.priceRials) || 0,
|
||
chargedAt: form.chargedAt,
|
||
planMonths: Number(form.planMonths),
|
||
alertDaysBefore: Number(form.alertDaysBefore) || 3,
|
||
isActive: form.isActive,
|
||
})
|
||
message.success('ذخیره شد')
|
||
await loadDetail()
|
||
} catch (e: any) {
|
||
const msg = e?.response?.data?.message
|
||
message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'ذخیره ناموفق')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
async function renewSupport() {
|
||
if (!selectedId.value) return
|
||
if (!renewForm.chargedAt) {
|
||
message.warning('تاریخ شارژ لازم است')
|
||
return
|
||
}
|
||
renewing.value = true
|
||
try {
|
||
const isFree = Boolean(renewForm.isFree)
|
||
await api.post(`/support/items/${selectedId.value}/renew`, {
|
||
chargedAt: renewForm.chargedAt,
|
||
planMonths: Number(renewForm.planMonths),
|
||
isFree,
|
||
priceRials: isFree ? 0 : Number(renewForm.priceRials),
|
||
note: renewForm.note || undefined,
|
||
})
|
||
message.success('پشتیبانی شارژ/تمدید شد')
|
||
renewForm.note = ''
|
||
await loadDetail()
|
||
} catch (e: any) {
|
||
const msg = e?.response?.data?.message
|
||
message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'شارژ ناموفق')
|
||
} finally {
|
||
renewing.value = false
|
||
}
|
||
}
|
||
|
||
function confirmDelete() {
|
||
if (!selected.value) return
|
||
Modal.confirm({
|
||
title: 'حذف پشتیبانی',
|
||
content: `پشتیبانی «${selected.value.name}» و تاریخچه شارژ آن حذف شود؟`,
|
||
okText: 'حذف',
|
||
okType: 'danger',
|
||
cancelText: 'انصراف',
|
||
onOk: async () => {
|
||
try {
|
||
await api.delete(`/support/items/${selected.value!.id}`)
|
||
message.success('حذف شد')
|
||
selectedId.value = null
|
||
await loadDetail()
|
||
} catch (e: any) {
|
||
message.error(e?.response?.data?.message || 'حذف ناموفق')
|
||
}
|
||
},
|
||
})
|
||
}
|
||
|
||
watch(
|
||
() => form.isFree,
|
||
(free) => {
|
||
if (free) form.priceRials = 0
|
||
},
|
||
)
|
||
|
||
watch(
|
||
() => createForm.isFree,
|
||
(free) => {
|
||
if (free) createForm.priceRials = 0
|
||
},
|
||
)
|
||
|
||
watch(
|
||
() => renewForm.isFree,
|
||
(free) => {
|
||
if (free) renewForm.priceRials = 0
|
||
},
|
||
)
|
||
|
||
watch(() => route.params.tenantId, loadDetail)
|
||
onMounted(loadDetail)
|
||
</script>
|
||
|
||
<template>
|
||
<div class="page-shell">
|
||
<div style="display: flex; align-items: flex-start; gap: 8px; margin-bottom: 16px">
|
||
<a-button type="text" @click="router.push({ name: 'support-panel' })">
|
||
<ArrowRightOutlined />
|
||
</a-button>
|
||
<div style="flex: 1">
|
||
<h1 class="page-title">{{ tenant?.nameFa || 'پشتیبانیهای نسخه' }}</h1>
|
||
<p class="page-sub">
|
||
{{ tenant?.product === 'hamkar' ? 'همکار' : 'خانگی' }} · {{ tenant?.domain }}
|
||
</p>
|
||
</div>
|
||
<a-button type="primary" @click="showCreate = true">
|
||
<PlusOutlined /> پشتیبانی جدید
|
||
</a-button>
|
||
</div>
|
||
|
||
<TenantPanelShortcuts :tenant-id="tenantId" current="support" />
|
||
|
||
<a-spin :spinning="loading">
|
||
<div class="layout">
|
||
<aside class="support-list">
|
||
<div class="list-head">پشتیبانیها ({{ supports.length }})</div>
|
||
<button
|
||
v-for="p in supports"
|
||
:key="p.id"
|
||
type="button"
|
||
class="support-card"
|
||
:class="[toneClass(p.summary?.expiryLevel), { active: p.id === selectedId, inactive: !p.isActive }]"
|
||
@click="selectSupport(p.id)"
|
||
>
|
||
<strong>
|
||
{{ p.name }}
|
||
<a-tag v-if="p.isFree" color="blue" class="free-tag">رایگان</a-tag>
|
||
</strong>
|
||
<span>{{ p.summary?.expiryLabel }}</span>
|
||
<small v-if="!p.isActive">غیرفعال</small>
|
||
</button>
|
||
<div v-if="!supports.length" class="empty-hint">
|
||
هنوز پشتیبانیای ثبت نشده — با «پشتیبانی جدید» شروع کنید.
|
||
</div>
|
||
</aside>
|
||
|
||
<div v-if="selected" class="detail">
|
||
<div class="status-banner" :class="toneClass(selected.summary?.expiryLevel)">
|
||
<strong>
|
||
{{ selected.name }} — {{ selected.summary?.expiryLabel }}
|
||
<a-tag v-if="selected.isFree" color="blue" class="free-tag">رایگان</a-tag>
|
||
</strong>
|
||
<span v-if="selected.summary?.expiresAt">
|
||
انقضا: {{ formatIranDate(selected.summary.expiresAt) }}
|
||
<template v-if="selected.summary?.planLabel"> · {{ selected.summary.planLabel }}</template>
|
||
</span>
|
||
</div>
|
||
|
||
<section class="form-section">
|
||
<div class="section-head">
|
||
<h3>مشخصات پشتیبانی</h3>
|
||
<a-button danger type="text" @click="confirmDelete">
|
||
<DeleteOutlined /> حذف
|
||
</a-button>
|
||
</div>
|
||
<a-form layout="vertical">
|
||
<a-form-item label="نام پشتیبانی" required>
|
||
<a-input v-model:value="form.name" size="large" placeholder="مثلاً پشتیبانی پایه / ویژه" />
|
||
</a-form-item>
|
||
<a-form-item label="مشخصات">
|
||
<a-textarea v-model:value="form.specs" :rows="2" placeholder="جزئیات فنی یا یادداشت داخلی…" />
|
||
</a-form-item>
|
||
<a-form-item label="توضیحات">
|
||
<a-textarea v-model:value="form.description" :rows="2" />
|
||
</a-form-item>
|
||
<a-form-item label="فعال">
|
||
<a-switch v-model:checked="form.isActive" />
|
||
</a-form-item>
|
||
</a-form>
|
||
</section>
|
||
|
||
<section class="form-section">
|
||
<h3>اعتبار و قیمت (وضعیت فعلی)</h3>
|
||
<a-form layout="vertical">
|
||
<a-row :gutter="12">
|
||
<a-col :xs="24" :md="8">
|
||
<a-form-item label="تاریخ شارژ (شمسی)">
|
||
<JalaliDatePicker v-model="form.chargedAt" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :xs="24" :md="8">
|
||
<a-form-item label="مدت شارژ">
|
||
<a-select
|
||
v-model:value="form.planMonths"
|
||
size="large"
|
||
:options="planSelectOptions"
|
||
/>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :xs="24" :md="8">
|
||
<a-form-item label="دوره رایگان">
|
||
<a-switch v-model:checked="form.isFree" />
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-form-item v-if="!form.isFree" label="قیمت دوره (تومان)">
|
||
<MoneyInput v-model:value="form.priceRials" placeholder="مثلاً 150,000" />
|
||
</a-form-item>
|
||
<p v-else class="field-hint">
|
||
دوره رایگان فعال است — مبلغ صفر میشود. مدت ۱ / ۳ / ۶ / ۱۲ ماه میتواند رایگان باشد.
|
||
</p>
|
||
<a-form-item label="شروع هشدار SMS (روز مانده به انقضا)">
|
||
<a-input-number
|
||
v-model:value="form.alertDaysBefore"
|
||
:min="1"
|
||
:max="30"
|
||
size="large"
|
||
style="width: 160px"
|
||
/>
|
||
<span class="inline-hint">پیشفرض ۳ روز — روزانه یک پیام تا تمدید</span>
|
||
</a-form-item>
|
||
</a-form>
|
||
</section>
|
||
|
||
<div class="sticky-actions settings-save">
|
||
<a-button type="primary" size="large" block :loading="saving" @click="saveSupport">
|
||
ذخیره تنظیمات
|
||
</a-button>
|
||
</div>
|
||
|
||
<section class="form-section renew-section">
|
||
<h3>شارژ / تمدید جدید</h3>
|
||
<p class="field-hint">
|
||
با ثبت شارژ، تاریخ انقضا تمدید میشود و هشدارهای قبلی قطع میگردد تا دوباره نزدیک پایان برسد.
|
||
</p>
|
||
<a-form layout="vertical">
|
||
<a-row :gutter="12">
|
||
<a-col :xs="24" :md="8">
|
||
<a-form-item label="تاریخ شروع دوره (شمسی)">
|
||
<JalaliDatePicker v-model="renewForm.chargedAt" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :xs="24" :md="8">
|
||
<a-form-item label="مدت">
|
||
<a-select
|
||
v-model:value="renewForm.planMonths"
|
||
size="large"
|
||
:options="planSelectOptions"
|
||
/>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :xs="24" :md="8">
|
||
<a-form-item label="دوره رایگان">
|
||
<a-switch v-model:checked="renewForm.isFree" />
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-form-item v-if="!renewForm.isFree" label="مبلغ (تومان)">
|
||
<MoneyInput v-model:value="renewForm.priceRials" placeholder="مثلاً 150,000" />
|
||
</a-form-item>
|
||
<p v-else class="field-hint">
|
||
دوره رایگان — مبلغ صفر. مدت ۱ / ۳ / ۶ / ۱۲ ماه میتواند رایگان باشد.
|
||
</p>
|
||
<a-form-item label="یادداشت">
|
||
<a-input v-model:value="renewForm.note" />
|
||
</a-form-item>
|
||
<a-button type="primary" size="large" :loading="renewing" @click="renewSupport">
|
||
ثبت شارژ / تمدید
|
||
</a-button>
|
||
</a-form>
|
||
</section>
|
||
|
||
<section class="form-section">
|
||
<h3>تاریخچه شارژ</h3>
|
||
<div v-if="selected.charges?.length" class="charge-list">
|
||
<article v-for="item in selected.charges" :key="item.id" class="charge-item">
|
||
<div>
|
||
<strong v-if="item.isFree">رایگان</strong>
|
||
<strong v-else>{{ formatMoney(item.priceRials) }}</strong>
|
||
<a-tag v-if="item.isFree" color="blue" class="free-tag">رایگان</a-tag>
|
||
<span> · {{ item.planMonths }} ماهه</span>
|
||
</div>
|
||
<div class="charge-meta">
|
||
از {{ formatIranDate(item.chargedAt) }} تا {{ formatIranDate(item.expiresAt) }}
|
||
<template v-if="item.note"> · {{ item.note }}</template>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
<div v-else class="hint-box">هنوز شارژی ثبت نشده است.</div>
|
||
</section>
|
||
</div>
|
||
|
||
<div v-else class="detail empty-detail">
|
||
پشتیبانیای انتخاب نشده است.
|
||
</div>
|
||
</div>
|
||
</a-spin>
|
||
|
||
<a-modal
|
||
v-model:open="showCreate"
|
||
title="افزودن پشتیبانی"
|
||
ok-text="ثبت"
|
||
cancel-text="انصراف"
|
||
:confirm-loading="creating"
|
||
@ok="createSupport"
|
||
>
|
||
<a-form layout="vertical" style="margin-top: 12px">
|
||
<a-form-item label="نام پشتیبانی" required>
|
||
<a-input v-model:value="createForm.name" size="large" placeholder="نام پشتیبانی" />
|
||
</a-form-item>
|
||
<a-form-item label="مشخصات">
|
||
<a-textarea v-model:value="createForm.specs" :rows="2" />
|
||
</a-form-item>
|
||
<a-row :gutter="12">
|
||
<a-col :span="12">
|
||
<a-form-item label="تاریخ شارژ">
|
||
<JalaliDatePicker v-model="createForm.chargedAt" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item label="مدت">
|
||
<a-select
|
||
v-model:value="createForm.planMonths"
|
||
size="large"
|
||
:options="planSelectOptions"
|
||
/>
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-form-item label="دوره رایگان">
|
||
<a-switch v-model:checked="createForm.isFree" />
|
||
</a-form-item>
|
||
<a-form-item v-if="!createForm.isFree" label="قیمت دوره (تومان)">
|
||
<MoneyInput v-model:value="createForm.priceRials" placeholder="مثلاً 150,000" />
|
||
</a-form-item>
|
||
<p v-else class="field-hint">
|
||
دوره رایگان — مبلغ صفر. مدت ۱ / ۳ / ۶ / ۱۲ ماه میتواند رایگان باشد.
|
||
</p>
|
||
</a-form>
|
||
</a-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.layout {
|
||
display: grid;
|
||
grid-template-columns: minmax(200px, 260px) 1fr;
|
||
gap: 16px;
|
||
align-items: start;
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.layout {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
.support-list {
|
||
border-radius: 18px;
|
||
background: #fff;
|
||
border: 1px solid #e8eef5;
|
||
padding: 12px;
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.list-head {
|
||
font-weight: 800;
|
||
margin-bottom: 4px;
|
||
color: #334155;
|
||
}
|
||
|
||
.support-card {
|
||
appearance: none;
|
||
border: 2px solid transparent;
|
||
border-radius: 14px;
|
||
padding: 12px;
|
||
text-align: right;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
display: grid;
|
||
gap: 2px;
|
||
color: #17324d;
|
||
}
|
||
|
||
.support-card.active {
|
||
border-color: #3b6fd4;
|
||
}
|
||
|
||
.support-card.inactive {
|
||
opacity: 0.65;
|
||
}
|
||
|
||
.support-card.healthy { background: linear-gradient(135deg, #dff8ea, #c3efd7); }
|
||
.support-card.warning { background: linear-gradient(135deg, #fff4c7, #ffe39a); }
|
||
.support-card.critical { background: linear-gradient(135deg, #ffd8d8, #ffb9b9); }
|
||
.support-card.unknown { background: linear-gradient(135deg, #eef2f7, #dde5ef); }
|
||
|
||
.free-tag {
|
||
margin-inline-start: 6px;
|
||
font-size: 11px;
|
||
line-height: 18px;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.empty-hint,
|
||
.empty-detail,
|
||
.hint-box {
|
||
color: #94a3b8;
|
||
padding: 14px;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.detail {
|
||
min-width: 0;
|
||
}
|
||
|
||
.status-banner {
|
||
border-radius: 18px;
|
||
padding: 14px 16px;
|
||
margin-bottom: 14px;
|
||
display: grid;
|
||
gap: 4px;
|
||
}
|
||
|
||
.status-banner.healthy { background: #dff8ea; color: #0f7a45; }
|
||
.status-banner.warning { background: #fff4c7; color: #9a6b00; }
|
||
.status-banner.critical { background: #ffd8d8; color: #b42318; }
|
||
.status-banner.unknown { background: #eef2f7; color: #64748b; }
|
||
|
||
.section-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.field-hint {
|
||
margin: 0 0 8px;
|
||
color: #64748b;
|
||
font-size: 13px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.settings-save {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.renew-section {
|
||
margin-top: 28px;
|
||
padding-top: 22px;
|
||
}
|
||
|
||
.inline-hint {
|
||
margin-right: 10px;
|
||
color: #94a3b8;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.charge-list {
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.charge-item {
|
||
border-radius: 14px;
|
||
background: #f8fafc;
|
||
padding: 12px 14px;
|
||
}
|
||
|
||
.charge-meta {
|
||
margin-top: 4px;
|
||
color: #64748b;
|
||
font-size: 0.85rem;
|
||
}
|
||
</style>
|