add reygiri page

This commit is contained in:
2026-07-12 17:51:48 +02:00
parent b8bdf8228c
commit b2e59464a4
6 changed files with 410 additions and 5 deletions
+338
View File
@@ -0,0 +1,338 @@
<template>
<div class="capture-sites-page">
<BasicShowContext title="لیست سایت‌های ری‌گیری">
<template #extra>
<BaseButton type="primary" size="small" :disabled="loading" @click="openAdd">
<span>افزودن سایت</span>
<BaseIcon icon="vuesax-linear:add" size="16" />
</BaseButton>
</template>
<a-spin :spinning="loading">
<a-empty
v-if="!sites.length"
description="هنوز سایتی ثبت نشده"
class="capture-sites-page__empty"
/>
<ul v-else class="capture-sites-list">
<li v-for="(site, index) in sites" :key="site.id" class="capture-sites-list__item">
<div class="capture-sites-list__name">{{ site.name }}</div>
<a
class="capture-sites-list__url"
:href="resolveHref(site.url)"
target="_blank"
rel="noopener noreferrer"
>
{{ site.url }}
</a>
<div class="capture-sites-list__actions">
<a-tooltip placement="bottom">
<template #title>ویرایش</template>
<Icon
icon="vuesax-linear:edit"
color="#4572E9"
class="cursor-pointer"
@click="openEdit(index)"
/>
</a-tooltip>
<a-tooltip placement="bottom">
<template #title>حذف</template>
<Icon
icon="vuesax-linear:trash"
color="#C53030"
class="cursor-pointer"
@click="confirmDelete(index)"
/>
</a-tooltip>
</div>
</li>
</ul>
</a-spin>
</BasicShowContext>
<BasicModal
v-model:visible="editorVisible"
:title="editorTitle"
width="28rem"
ok-text="ثبت"
cancel-text="انصراف"
:confirm-loading="saving"
@ok="submitForm"
@close="closeEditor"
>
<a-form ref="formRef" :model="form" layout="vertical" autocomplete="off">
<a-form-item label="نام سایت" name="name" :rules="inputRules('نام سایت')">
<BasicInput v-model:value="form.name" placeHolder="مثال: سایت ریگیری طلا" />
</a-form-item>
<a-form-item label="لینک سایت" name="url" :rules="urlRules">
<BasicInput v-model:value="form.url" placeHolder="https://example.com" class="capture-sites-page__url-input" />
</a-form-item>
</a-form>
</BasicModal>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { Modal, message } from 'ant-design-vue'
import { createVNode } from 'vue'
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
import type { FormInstance } from 'ant-design-vue'
import Icon from '@core/components/BaseIcon/src/Icon.vue'
import BasicShowContext from '@/components/BasicShowContext.vue'
import BasicModal from '@/components/BasicModal.vue'
import type { CaptureSite } from '@/model/captureSite'
import {
createAddConst,
getReygiriSitesConst,
updateReygiriSitesConst,
} from '@/api/consts'
import { inputRules } from '@/utils/inputRules'
import {
REYGIri_SITES_KEY,
normalizeReygiriSitesConst,
serializeReygiriSites,
} from '@/utils/reygiriSites'
const sites = ref<CaptureSite[]>([])
const loading = ref(false)
const saving = ref(false)
const editorVisible = ref(false)
const editIndex = ref<number | null>(null)
const constId = ref<number | null>(null)
const formRef = ref<FormInstance>()
const form = ref({
name: '',
url: '',
})
const editorTitle = computed(() =>
editIndex.value == null ? 'افزودن سایت' : 'ویرایش سایت'
)
const urlRules = [
{ required: true, message: 'پر کردن لینک سایت الزامی است' },
{
validator(_rule: unknown, value: string) {
const trimmed = (value ?? '').trim()
if (!trimmed) {
return Promise.reject('پر کردن لینک سایت الزامی است')
}
if (/\s/.test(trimmed)) {
return Promise.reject('لینک سایت نباید شامل فاصله باشد')
}
return Promise.resolve()
},
trigger: ['change', 'blur'],
},
]
function resolveHref(url: string): string {
const trimmed = url.trim()
if (!trimmed) return '#'
if (/^https?:\/\//i.test(trimmed)) return trimmed
return `https://${trimmed.replace(/^\/+/, '')}`
}
function createSiteId() {
return `site-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
}
function resetForm() {
form.value = { name: '', url: '' }
formRef.value?.clearValidate()
}
function openAdd() {
editIndex.value = null
resetForm()
editorVisible.value = true
}
function openEdit(index: number) {
const site = sites.value[index]
if (!site) return
editIndex.value = index
form.value = {
name: site.name,
url: site.url,
}
editorVisible.value = true
}
function closeEditor() {
editorVisible.value = false
editIndex.value = null
resetForm()
}
async function persistSites() {
const payload = {
key: REYGIri_SITES_KEY,
value: serializeReygiriSites(sites.value),
is_active: true,
description: 'لیست سایت‌های ری‌گیری',
}
if (constId.value) {
await updateReygiriSitesConst({ id: constId.value, ...payload })
return
}
const created = await createAddConst(payload)
constId.value = created.id ?? created.const_id ?? null
}
async function submitForm() {
try {
await formRef.value?.validate()
saving.value = true
const nextSite: CaptureSite = {
id: editIndex.value == null ? createSiteId() : sites.value[editIndex.value].id,
name: form.value.name.trim(),
url: form.value.url.trim(),
}
if (editIndex.value == null) {
sites.value.push(nextSite)
} else {
sites.value[editIndex.value] = nextSite
}
await persistSites()
message.success(editIndex.value == null ? 'سایت با موفقیت افزوده شد' : 'سایت با موفقیت ویرایش شد')
closeEditor()
} catch (error) {
console.error(error)
} finally {
saving.value = false
}
}
function confirmDelete(index: number) {
const site = sites.value[index]
if (!site) return
Modal.confirm({
title: `آیا از حذف «${site.name}» اطمینان دارید؟`,
icon: createVNode(ExclamationCircleOutlined),
okText: 'حذف',
okType: 'danger',
centered: true,
cancelText: 'انصراف',
async onOk() {
sites.value.splice(index, 1)
saving.value = true
try {
await persistSites()
message.success('سایت با موفقیت حذف شد')
} catch (error) {
console.error(error)
message.error('خطا در حذف سایت')
} finally {
saving.value = false
}
},
})
}
async function loadSites() {
loading.value = true
try {
const res = await getReygiriSitesConst()
const normalized = normalizeReygiriSitesConst(res)
constId.value = normalized.constId
sites.value = normalized.sites
} catch (error) {
console.error('خطا در دریافت سایت‌های ری‌گیری:', error)
message.error('خطا در دریافت لیست سایت‌های ری‌گیری')
sites.value = []
} finally {
loading.value = false
}
}
onMounted(() => {
void loadSites()
})
</script>
<style scoped lang="less">
.capture-sites-page {
display: flex;
width: 100%;
min-width: 0;
height: 100%;
min-height: 0;
}
.capture-sites-page__empty {
margin-top: 3rem;
}
:deep(.capture-sites-page__url-input .ant-input) {
direction: ltr;
text-align: left;
}
.capture-sites-list {
margin: 0;
padding: 0;
list-style: none;
border: 1px solid var(--admin-border, #e5e7eb);
border-radius: 12px;
overflow: hidden;
background: var(--admin-bg-elevated, #fff);
}
.capture-sites-list__item {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr) auto;
align-items: center;
gap: 1rem;
padding: 0.9rem 1rem;
border-bottom: 1px solid var(--admin-border, #e5e7eb);
&:last-child {
border-bottom: none;
}
@media (max-width: 768px) {
grid-template-columns: 1fr;
gap: 0.75rem;
}
}
.capture-sites-list__name {
font-size: 0.9375rem;
font-weight: 600;
color: var(--admin-text, #111827);
line-height: 1.6;
}
.capture-sites-list__url {
justify-self: start;
direction: ltr;
text-align: left;
font-size: 0.875rem;
color: #4572e9;
word-break: break-all;
&:hover {
text-decoration: underline;
}
}
.capture-sites-list__actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.75rem;
}
</style>