add reygiri page
This commit is contained in:
Vendored
-4
@@ -10,7 +10,6 @@ export {}
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
_index: typeof import('./src/core/components/_sample/_index.vue')['default']
|
||||
AAlert: typeof import('ant-design-vue/es')['Alert']
|
||||
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
||||
ABadge: typeof import('ant-design-vue/es')['Badge']
|
||||
AButton: typeof import('ant-design-vue/es')['Button']
|
||||
@@ -43,7 +42,6 @@ declare module '@vue/runtime-core' {
|
||||
APagination: typeof import('ant-design-vue/es')['Pagination']
|
||||
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
||||
APopover: typeof import('ant-design-vue/es')['Popover']
|
||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
||||
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||
ARow: typeof import('ant-design-vue/es')['Row']
|
||||
@@ -55,11 +53,9 @@ declare module '@vue/runtime-core' {
|
||||
ATable: typeof import('ant-design-vue/es')['Table']
|
||||
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
|
||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||
BaseButton: typeof import('./src/core/components/BaseButton/index.ts')['default']
|
||||
BaseCard: typeof import('./src/core/components/BaseCard/index.ts')['default']
|
||||
BaseCollapse: typeof import('./src/components/BaseCollapse.vue')['default']
|
||||
|
||||
+8
-1
@@ -24,4 +24,11 @@ export const updateBannerConst = async (obj: Const) =>
|
||||
await useAxios.put<Const>(`/admin/const/${obj.id}`, obj)
|
||||
|
||||
export const getHeaderBannerConst = async () =>
|
||||
await useAxios.get<Const>('/api/const/key/header_banner')
|
||||
await useAxios.get<Const>('/api/const/key/header_banner')
|
||||
|
||||
/** GET /api/const/key/reygiri_sites */
|
||||
export const getReygiriSitesConst = async () =>
|
||||
await useAxios.get<Const | Array<Const>>('/api/const/key/reygiri_sites')
|
||||
|
||||
export const updateReygiriSitesConst = async (obj: Const) =>
|
||||
await useAxios.put<Const>(`/admin/const/${obj.id}`, obj)
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface CaptureSite {
|
||||
id: string
|
||||
name: string
|
||||
url: string
|
||||
}
|
||||
@@ -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>
|
||||
@@ -54,6 +54,17 @@ const Setting: RouteRecordRaw = {
|
||||
order: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'captureSitesMain',
|
||||
path: 'capture-sites',
|
||||
component: () => import('@/pages/menuPages/captureSites/index.vue'),
|
||||
meta: {
|
||||
permissionsKey: 'pricing',
|
||||
icon: 'vuesax-linear:global',
|
||||
title: 'سایتهای ریگیری',
|
||||
order: 2.5
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'advancedConfiguration',
|
||||
path: 'advanced-configuration',
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { Const } from '@/model/const'
|
||||
import type { CaptureSite } from '@/model/captureSite'
|
||||
|
||||
export const REYGIri_SITES_KEY = 'reygiri_sites'
|
||||
|
||||
export function normalizeReygiriSitesConst(response: Const | Const[] | null | undefined): {
|
||||
constId: number | null
|
||||
sites: CaptureSite[]
|
||||
} {
|
||||
const record = Array.isArray(response) ? response[0] : response
|
||||
if (!record) {
|
||||
return { constId: null, sites: [] }
|
||||
}
|
||||
|
||||
return {
|
||||
constId: record.id ?? record.const_id ?? null,
|
||||
sites: parseReygiriSitesValue(record.value),
|
||||
}
|
||||
}
|
||||
|
||||
export function parseReygiriSitesValue(value: string | null | undefined): CaptureSite[] {
|
||||
if (!value) return []
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
if (!Array.isArray(parsed)) return []
|
||||
|
||||
return parsed
|
||||
.map((item, index) => ({
|
||||
id: String(item?.id ?? `site-${index + 1}`),
|
||||
name: String(item?.name ?? item?.title ?? item?.title_fa ?? item?.caption ?? '').trim(),
|
||||
url: String(item?.url ?? item?.link ?? item?.value ?? '').trim(),
|
||||
}))
|
||||
.filter((item) => item.name && item.url)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeReygiriSites(sites: CaptureSite[]): string {
|
||||
return JSON.stringify(
|
||||
sites.map(({ id, name, url }) => ({
|
||||
id,
|
||||
name,
|
||||
url,
|
||||
}))
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user