fix some extention bug
This commit is contained in:
+369
-88
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { ArrowRightOutlined, PlusOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons-vue'
|
||||
@@ -7,7 +7,14 @@ 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 {
|
||||
addIranDaysToIso,
|
||||
addIranPlanExpiryIso,
|
||||
formatIranDate,
|
||||
iranDateInputToIso,
|
||||
iranDaysBetween,
|
||||
todayIranDateInput,
|
||||
} from '@/utils/datetime'
|
||||
import { formatMoney } from '@/utils/money'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
@@ -48,6 +55,7 @@ const pluginStoreEnabled = computed(() => tenant.value?.pluginStoreEnabled === t
|
||||
const selectedId = ref<string | null>(null)
|
||||
const showCreate = ref(false)
|
||||
const showDetail = ref(false)
|
||||
const detailTab = ref<'info' | 'credit' | 'renew'>('info')
|
||||
|
||||
const tenant = ref<{
|
||||
id: string
|
||||
@@ -68,6 +76,7 @@ const form = reactive({
|
||||
description: '',
|
||||
priceRials: 0 as number,
|
||||
chargedAt: null as string | null,
|
||||
expiresAt: null as string | null,
|
||||
planMonths: 1 as number,
|
||||
alertDaysBefore: 3,
|
||||
isActive: true,
|
||||
@@ -89,11 +98,14 @@ const createForm = reactive({
|
||||
|
||||
const renewForm = reactive({
|
||||
chargedAt: iranDateInputToIso(todayIranDateInput()) as string | null,
|
||||
expiresAt: addIranPlanExpiryIso(iranDateInputToIso(todayIranDateInput()) || '', 1),
|
||||
planMonths: 1,
|
||||
priceRials: 0 as number,
|
||||
note: '',
|
||||
})
|
||||
|
||||
const skipPeriodSync = ref(false)
|
||||
|
||||
const tenantId = computed(() => String(route.params.tenantId || ''))
|
||||
|
||||
const selected = computed(() => plugins.value.find((p) => p.id === selectedId.value) || null)
|
||||
@@ -133,12 +145,18 @@ function toneClass(level?: ExpiryLevel) {
|
||||
}
|
||||
|
||||
function fillForm(plugin: PluginItem) {
|
||||
skipPeriodSync.value = true
|
||||
Object.assign(form, {
|
||||
name: plugin.name || '',
|
||||
specs: plugin.specs || '',
|
||||
description: plugin.description || '',
|
||||
priceRials: plugin.priceRials ?? 0,
|
||||
chargedAt: plugin.chargedAt || null,
|
||||
expiresAt:
|
||||
plugin.expiresAt ||
|
||||
(plugin.chargedAt && plugin.planMonths
|
||||
? addIranPlanExpiryIso(plugin.chargedAt, plugin.planMonths)
|
||||
: null),
|
||||
planMonths: plugin.planMonths || 1,
|
||||
alertDaysBefore: plugin.alertDaysBefore ?? 3,
|
||||
isActive: plugin.isActive !== false,
|
||||
@@ -148,9 +166,118 @@ function fillForm(plugin: PluginItem) {
|
||||
renewForm.planMonths = plugin.planMonths || 1
|
||||
renewForm.priceRials = plugin.priceRials ?? 0
|
||||
renewForm.chargedAt = iranDateInputToIso(todayIranDateInput())
|
||||
renewForm.expiresAt = addIranPlanExpiryIso(renewForm.chargedAt || '', renewForm.planMonths)
|
||||
renewForm.note = ''
|
||||
void nextTick(() => {
|
||||
skipPeriodSync.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function shiftExpiryWithCharge(
|
||||
target: { chargedAt: string | null; expiresAt: string | null; planMonths: number },
|
||||
next: string | null,
|
||||
prev: string | null | undefined,
|
||||
) {
|
||||
if (!next) return
|
||||
if (target.expiresAt && prev) {
|
||||
const delta = iranDaysBetween(prev, next) ?? 0
|
||||
target.expiresAt = addIranDaysToIso(target.expiresAt, delta)
|
||||
return
|
||||
}
|
||||
if (target.planMonths) {
|
||||
target.expiresAt = addIranPlanExpiryIso(next, target.planMonths)
|
||||
}
|
||||
}
|
||||
|
||||
const durationDays = computed({
|
||||
get() {
|
||||
if (!form.chargedAt || !form.expiresAt) return 0
|
||||
return iranDaysBetween(form.chargedAt, form.expiresAt) ?? 0
|
||||
},
|
||||
set(days: number | null) {
|
||||
if (!form.chargedAt) return
|
||||
form.expiresAt = addIranDaysToIso(form.chargedAt, Math.max(0, Number(days) || 0))
|
||||
},
|
||||
})
|
||||
|
||||
const renewDurationDays = computed({
|
||||
get() {
|
||||
if (!renewForm.chargedAt || !renewForm.expiresAt) return 0
|
||||
return iranDaysBetween(renewForm.chargedAt, renewForm.expiresAt) ?? 0
|
||||
},
|
||||
set(days: number | null) {
|
||||
if (!renewForm.chargedAt) return
|
||||
renewForm.expiresAt = addIranDaysToIso(renewForm.chargedAt, Math.max(0, Number(days) || 0))
|
||||
},
|
||||
})
|
||||
|
||||
const liveDaysLeft = computed(() =>
|
||||
form.expiresAt ? iranDaysBetween(new Date(), form.expiresAt) : null,
|
||||
)
|
||||
|
||||
function expiryCaption(daysLeft: number | null) {
|
||||
if (daysLeft == null) return 'بدون تاریخ اعتبار'
|
||||
if (daysLeft < 0) return `${Math.abs(daysLeft)} روز گذشته از انقضا`
|
||||
if (daysLeft === 0) return 'امروز منقضی میشود'
|
||||
return `${daysLeft} روز مانده`
|
||||
}
|
||||
|
||||
function pluginDaysLeft(plugin: PluginItem) {
|
||||
return plugin.expiresAt ? iranDaysBetween(new Date(), plugin.expiresAt) : null
|
||||
}
|
||||
|
||||
function pluginExpiryLabel(plugin: PluginItem) {
|
||||
return expiryCaption(pluginDaysLeft(plugin))
|
||||
}
|
||||
|
||||
function pluginExpiryTone(plugin: PluginItem): ExpiryLevel {
|
||||
const days = pluginDaysLeft(plugin)
|
||||
const threshold = plugin.alertDaysBefore ?? 3
|
||||
if (days == null) return 'unknown'
|
||||
if (days <= threshold) return 'critical'
|
||||
if (days <= threshold + 4) return 'warning'
|
||||
return 'healthy'
|
||||
}
|
||||
|
||||
const liveExpiryLabel = computed(() => expiryCaption(liveDaysLeft.value))
|
||||
const liveExpiryLevel = computed((): ExpiryLevel => {
|
||||
const days = liveDaysLeft.value
|
||||
const threshold = form.alertDaysBefore ?? 3
|
||||
if (days == null) return 'unknown'
|
||||
if (days <= threshold) return 'critical'
|
||||
if (days <= threshold + 4) return 'warning'
|
||||
return 'healthy'
|
||||
})
|
||||
|
||||
watch(
|
||||
() => form.chargedAt,
|
||||
(next, prev) => {
|
||||
if (skipPeriodSync.value) return
|
||||
shiftExpiryWithCharge(form, next, prev)
|
||||
},
|
||||
)
|
||||
watch(
|
||||
() => form.planMonths,
|
||||
(months) => {
|
||||
if (skipPeriodSync.value || !form.chargedAt || !months) return
|
||||
form.expiresAt = addIranPlanExpiryIso(form.chargedAt, months)
|
||||
},
|
||||
)
|
||||
watch(
|
||||
() => renewForm.chargedAt,
|
||||
(next, prev) => {
|
||||
if (skipPeriodSync.value) return
|
||||
shiftExpiryWithCharge(renewForm, next, prev)
|
||||
},
|
||||
)
|
||||
watch(
|
||||
() => renewForm.planMonths,
|
||||
(months) => {
|
||||
if (skipPeriodSync.value || !renewForm.chargedAt || !months) return
|
||||
renewForm.expiresAt = addIranPlanExpiryIso(renewForm.chargedAt, months)
|
||||
},
|
||||
)
|
||||
|
||||
function selectPlugin(id: string) {
|
||||
selectedId.value = id
|
||||
const plugin = plugins.value.find((p) => p.id === id)
|
||||
@@ -159,11 +286,13 @@ function selectPlugin(id: string) {
|
||||
|
||||
function openPlugin(id: string) {
|
||||
selectPlugin(id)
|
||||
detailTab.value = 'info'
|
||||
showDetail.value = true
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
showDetail.value = false
|
||||
detailTab.value = 'info'
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
@@ -246,6 +375,7 @@ async function savePlugin() {
|
||||
description: form.description || null,
|
||||
priceRials: Number(form.priceRials) || 0,
|
||||
chargedAt: form.chargedAt,
|
||||
expiresAt: form.expiresAt,
|
||||
planMonths: Number(form.planMonths),
|
||||
alertDaysBefore: Number(form.alertDaysBefore) || 3,
|
||||
isActive: form.isActive,
|
||||
@@ -274,6 +404,7 @@ async function renewPlugin() {
|
||||
try {
|
||||
await api.post(`/plugins/items/${selectedId.value}/renew`, {
|
||||
chargedAt: renewForm.chargedAt,
|
||||
expiresAt: renewForm.expiresAt,
|
||||
planMonths: Number(renewForm.planMonths),
|
||||
priceRials: Number(renewForm.priceRials),
|
||||
note: renewForm.note || undefined,
|
||||
@@ -394,11 +525,11 @@ onMounted(loadDetail)
|
||||
:key="p.id"
|
||||
type="button"
|
||||
class="plugin-card"
|
||||
:class="toneClass(p.summary?.expiryLevel)"
|
||||
:class="toneClass(pluginExpiryTone(p))"
|
||||
@click="openPlugin(p.id)"
|
||||
>
|
||||
<strong>{{ p.name }}</strong>
|
||||
<span>{{ p.summary?.expiryLabel }}</span>
|
||||
<span>{{ pluginExpiryLabel(p) }}</span>
|
||||
</button>
|
||||
<div v-if="!filteredActivePlugins.length" class="empty-hint">
|
||||
{{ pluginQuery.trim() ? 'افزونهای با این جستجو نیست.' : 'افزونهٔ فعالی نیست — با «افزونه جدید» ثبت کنید.' }}
|
||||
@@ -416,11 +547,11 @@ onMounted(loadDetail)
|
||||
:key="p.id"
|
||||
type="button"
|
||||
class="plugin-card inactive"
|
||||
:class="toneClass(p.summary?.expiryLevel)"
|
||||
:class="toneClass(pluginExpiryTone(p))"
|
||||
@click="openPlugin(p.id)"
|
||||
>
|
||||
<strong>{{ p.name }}</strong>
|
||||
<span>{{ p.summary?.expiryLabel }}</span>
|
||||
<span>{{ pluginExpiryLabel(p) }}</span>
|
||||
<small>غیرفعال</small>
|
||||
</button>
|
||||
</div>
|
||||
@@ -431,23 +562,51 @@ onMounted(loadDetail)
|
||||
<a-modal
|
||||
v-model:open="showDetail"
|
||||
:title="selected?.name || 'جزئیات افزونه'"
|
||||
width="40rem"
|
||||
wrap-class-name="sa-modal-wrap"
|
||||
width="48rem"
|
||||
wrap-class-name="sa-modal-wrap plugin-detail-modal"
|
||||
:footer="null"
|
||||
@cancel="closeDetail"
|
||||
>
|
||||
<div v-if="selected" class="detail">
|
||||
<div class="status-banner" :class="toneClass(selected.summary?.expiryLevel)">
|
||||
<strong>{{ selected.name }} — {{ selected.summary?.expiryLabel }}</strong>
|
||||
<span v-if="selected.summary?.expiresAt">
|
||||
انقضا: {{ formatIranDate(selected.summary.expiresAt) }}
|
||||
<div class="status-banner" :class="toneClass(liveExpiryLevel)">
|
||||
<strong>{{ selected.name }} — {{ liveExpiryLabel }}</strong>
|
||||
<span v-if="form.expiresAt">
|
||||
انقضا: {{ formatIranDate(form.expiresAt) }}
|
||||
<template v-if="durationDays"> · {{ durationDays }} روز از شارژ</template>
|
||||
<template v-if="selected.summary?.planLabel"> · {{ selected.summary.planLabel }}</template>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<section class="form-section">
|
||||
<div class="detail-tabs" role="tablist">
|
||||
<a-button
|
||||
:type="detailTab === 'info' ? 'primary' : 'default'"
|
||||
ghost
|
||||
@click="detailTab = 'info'"
|
||||
>
|
||||
مشخصات
|
||||
</a-button>
|
||||
<a-button
|
||||
:type="detailTab === 'credit' ? 'primary' : 'default'"
|
||||
ghost
|
||||
@click="detailTab = 'credit'"
|
||||
>
|
||||
اعتبار
|
||||
</a-button>
|
||||
<a-button
|
||||
:type="detailTab === 'renew' ? 'primary' : 'default'"
|
||||
ghost
|
||||
@click="detailTab = 'renew'"
|
||||
>
|
||||
تمدید
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<section v-show="detailTab === 'info'" class="detail-pane">
|
||||
<div class="section-head">
|
||||
<h3>مشخصات افزونه</h3>
|
||||
<div>
|
||||
<h3>مشخصات افزونه</h3>
|
||||
<p class="field-hint">نام، وضعیت فعال و کلید فروشگاه.</p>
|
||||
</div>
|
||||
<a-button v-if="canWrite" danger type="text" @click="confirmDelete">
|
||||
<DeleteOutlined /> حذف
|
||||
</a-button>
|
||||
@@ -456,41 +615,80 @@ onMounted(loadDetail)
|
||||
<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-row :gutter="[12, 0]">
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="مشخصات">
|
||||
<a-textarea v-model:value="form.specs" :rows="2" placeholder="جزئیات فنی یا یادداشت داخلی…" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="توضیحات">
|
||||
<a-textarea v-model:value="form.description" :rows="2" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="[12, 8]">
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="فعال">
|
||||
<a-switch v-model:checked="form.isActive" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="isKhanegi" :xs="24" :sm="12">
|
||||
<a-form-item label="قطع خودکار بعد از انقضا">
|
||||
<a-switch v-model:checked="form.autoCutOnExpiry" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item v-if="isKhanegi" label="کلید تنظیمات (فروشگاه)">
|
||||
<a-select
|
||||
v-model:value="form.settingKey"
|
||||
allow-clear
|
||||
placeholder="دستی — بدون فروشگاه"
|
||||
:options="catalogKeys.map((c) => ({ value: c.settingKey, label: `${c.titleFa} (${c.settingKey})` }))"
|
||||
/>
|
||||
</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>
|
||||
<template v-if="isKhanegi">
|
||||
<a-form-item label="کلید تنظیمات (فروشگاه)">
|
||||
<a-select
|
||||
v-model:value="form.settingKey"
|
||||
allow-clear
|
||||
placeholder="دستی — بدون فروشگاه"
|
||||
:options="catalogKeys.map((c) => ({ value: c.settingKey, label: `${c.titleFa} (${c.settingKey})` }))"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="قطع خودکار بعد از انقضا">
|
||||
<a-switch v-model:checked="form.autoCutOnExpiry" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
<a-button
|
||||
v-if="canWrite"
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
:loading="saving"
|
||||
@click="savePlugin"
|
||||
>
|
||||
ذخیره مشخصات
|
||||
</a-button>
|
||||
</section>
|
||||
|
||||
<section class="form-section">
|
||||
<h3>اعتبار و قیمت (وضعیت فعلی)</h3>
|
||||
<section v-show="detailTab === 'credit'" class="detail-pane">
|
||||
<h3>اعتبار فعلی</h3>
|
||||
<p class="field-hint">
|
||||
تاریخ شارژ را عقب/جلو کنید تا انقضا با همان فاصله حرکت کند. تعداد روز را کموزیاد کنید تا فقط انقضا جابهجا شود.
|
||||
</p>
|
||||
<a-form layout="vertical">
|
||||
<a-row :gutter="12">
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-row :gutter="[12, 0]">
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="تاریخ شارژ (شمسی)">
|
||||
<JalaliDatePicker v-model="form.chargedAt" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="تاریخ انقضا (شمسی)">
|
||||
<JalaliDatePicker v-model="form.expiresAt" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="فاصله تا انقضا (روز)">
|
||||
<a-input-number
|
||||
v-model:value="durationDays"
|
||||
:min="0"
|
||||
:max="4000"
|
||||
size="large"
|
||||
class="full-field"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="مدت شارژ">
|
||||
<a-select
|
||||
v-model:value="form.planMonths"
|
||||
@@ -499,44 +697,66 @@ onMounted(loadDetail)
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="قیمت دوره (تومان)">
|
||||
<MoneyInput v-model:value="form.priceRials" placeholder="مثلاً 150,000" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="هشدار SMS (روز مانده)">
|
||||
<a-input-number
|
||||
v-model:value="form.alertDaysBefore"
|
||||
:min="1"
|
||||
:max="30"
|
||||
size="large"
|
||||
class="full-field"
|
||||
/>
|
||||
<span class="inline-hint">پیشفرض ۳ روز — روزانه تا تمدید</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<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>
|
||||
<a-button
|
||||
v-if="canWrite"
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
:loading="saving"
|
||||
@click="savePlugin"
|
||||
>
|
||||
ذخیره اعتبار
|
||||
</a-button>
|
||||
</section>
|
||||
|
||||
<div v-if="canWrite" class="sticky-actions settings-save">
|
||||
<a-button type="primary" size="large" block :loading="saving" @click="savePlugin">
|
||||
ذخیره تنظیمات
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<section v-if="canWrite" class="form-section renew-section">
|
||||
<section v-show="detailTab === 'renew'" class="detail-pane">
|
||||
<h3>شارژ / تمدید جدید</h3>
|
||||
<p class="field-hint">
|
||||
با ثبت شارژ، تاریخ انقضا تمدید میشود و هشدارهای قبلی قطع میگردد تا دوباره نزدیک پایان برسد.
|
||||
دورهٔ جدید ثبت میشود؛ انقضا عوض میشود و هشدارهای قبلی تا نزدیک پایان قطع میگردد.
|
||||
</p>
|
||||
<a-form layout="vertical">
|
||||
<a-row :gutter="12">
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form v-if="canWrite" layout="vertical">
|
||||
<a-row :gutter="[12, 0]">
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="تاریخ شروع دوره (شمسی)">
|
||||
<JalaliDatePicker v-model="renewForm.chargedAt" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="تاریخ انقضا (شمسی)">
|
||||
<JalaliDatePicker v-model="renewForm.expiresAt" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="فاصله تا انقضا (روز)">
|
||||
<a-input-number
|
||||
v-model:value="renewDurationDays"
|
||||
:min="0"
|
||||
:max="4000"
|
||||
size="large"
|
||||
class="full-field"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="مدت">
|
||||
<a-select
|
||||
v-model:value="renewForm.planMonths"
|
||||
@@ -545,23 +765,23 @@ onMounted(loadDetail)
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="مبلغ (تومان)">
|
||||
<MoneyInput v-model:value="renewForm.priceRials" placeholder="مثلاً 150,000" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12">
|
||||
<a-form-item label="یادداشت">
|
||||
<a-input v-model:value="renewForm.note" size="large" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="یادداشت">
|
||||
<a-input v-model:value="renewForm.note" />
|
||||
</a-form-item>
|
||||
<a-button type="primary" size="large" :loading="renewing" @click="renewPlugin">
|
||||
<a-button type="primary" size="large" block :loading="renewing" @click="renewPlugin">
|
||||
ثبت شارژ / تمدید
|
||||
</a-button>
|
||||
</a-form>
|
||||
</section>
|
||||
|
||||
<section class="form-section">
|
||||
<h3>تاریخچه شارژ</h3>
|
||||
<h3 class="history-head">تاریخچه شارژ</h3>
|
||||
<div v-if="selected.charges?.length" class="charge-list">
|
||||
<article v-for="item in selected.charges" :key="item.id" class="charge-item">
|
||||
<div>
|
||||
@@ -724,12 +944,81 @@ onMounted(loadDetail)
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.status-banner {
|
||||
border-radius: 18px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 14px;
|
||||
.detail-tabs {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
padding: 4px;
|
||||
margin-bottom: 12px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.detail-tabs :deep(.ant-btn) {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-radius: 10px;
|
||||
padding-inline: 6px;
|
||||
font-weight: 700;
|
||||
font-size: 0.82rem;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.detail-tabs :deep(.ant-btn-primary) {
|
||||
background: #fff;
|
||||
color: #0f766e;
|
||||
box-shadow: 0 1px 4px rgba(45, 55, 72, 0.08);
|
||||
}
|
||||
|
||||
.detail-pane h3 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 0.98rem;
|
||||
color: #17324d;
|
||||
}
|
||||
|
||||
.detail-pane .section-head {
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.detail-pane .section-head {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-pane .section-head .field-hint {
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.full-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detail-pane :deep(.ant-input-number),
|
||||
.detail-pane :deep(.ant-select),
|
||||
.detail-pane :deep(.jalali-date-field) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.history-head {
|
||||
margin: 18px 0 8px;
|
||||
}
|
||||
|
||||
.status-banner {
|
||||
border-radius: 14px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 12px;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.status-banner strong,
|
||||
.status-banner span {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.status-banner.healthy { background: #dff8ea; color: #0f7a45; }
|
||||
@@ -746,23 +1035,15 @@ onMounted(loadDetail)
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
margin: 0 0 8px;
|
||||
margin: 0 0 10px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.settings-save {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.renew-section {
|
||||
margin-top: 28px;
|
||||
padding-top: 22px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.inline-hint {
|
||||
margin-right: 10px;
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user