feat(admin): colleague accounting codes and reliable banner saves
Add per-provider Montzar account codes on the colleague transactions page, rename the menu label, and auto-persist banner uploads with https file URLs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { reactive, ref } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { loadAppBannersConst, saveAppBannersConst } from '@/api/appBanners'
|
||||
import { emptyAppBanners, type AppBannersValue } from '@/model/appBanners'
|
||||
import { emptyAppBanners, type AppBannersValue, type ThemeImagePair } from '@/model/appBanners'
|
||||
import { toAbsoluteFileUrl } from '@/utils/file'
|
||||
import type { IBenefits } from '@/model/banner'
|
||||
|
||||
const state = reactive<AppBannersValue>(emptyAppBanners())
|
||||
const constId = ref<number | null>(null)
|
||||
@@ -9,6 +11,38 @@ const loaded = ref(false)
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
let loadPromise: Promise<void> | null = null
|
||||
let saveTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let saveChain: Promise<boolean> = Promise.resolve(true)
|
||||
|
||||
function httpsUrl(raw: string | null | undefined): string | null {
|
||||
if (typeof raw !== 'string' || !raw.trim()) return null
|
||||
return toAbsoluteFileUrl(raw.trim()) ?? raw.trim()
|
||||
}
|
||||
|
||||
function httpsPair(pair: ThemeImagePair): ThemeImagePair {
|
||||
return { light: httpsUrl(pair.light), dark: httpsUrl(pair.dark) }
|
||||
}
|
||||
|
||||
function httpsSlider(slides: IBenefits[]): IBenefits[] {
|
||||
return slides.map((slide) => ({
|
||||
...slide,
|
||||
icon: httpsUrl(slide.icon),
|
||||
}))
|
||||
}
|
||||
|
||||
function snapshotForSave(): AppBannersValue {
|
||||
return {
|
||||
slider: httpsSlider([...state.slider]),
|
||||
homeAsset: httpsPair(state.homeAsset),
|
||||
piggyCreate: httpsPair(state.piggyCreate),
|
||||
productsHero1: httpsPair(state.productsHero1),
|
||||
productsHero2: httpsPair(state.productsHero2),
|
||||
productsHero3: httpsPair(state.productsHero3),
|
||||
referral: httpsPair(state.referral),
|
||||
share: httpsPair(state.share),
|
||||
invite: httpsPair(state.invite),
|
||||
}
|
||||
}
|
||||
|
||||
async function load(force = false) {
|
||||
if (!force && loaded.value) return
|
||||
@@ -36,17 +70,7 @@ async function load(force = false) {
|
||||
async function save(successText = 'بنرهای اپ ذخیره شد') {
|
||||
saving.value = true
|
||||
try {
|
||||
const nextId = await saveAppBannersConst(constId.value, {
|
||||
slider: [...state.slider],
|
||||
homeAsset: { ...state.homeAsset },
|
||||
piggyCreate: { ...state.piggyCreate },
|
||||
productsHero1: { ...state.productsHero1 },
|
||||
productsHero2: { ...state.productsHero2 },
|
||||
productsHero3: { ...state.productsHero3 },
|
||||
referral: { ...state.referral },
|
||||
share: { ...state.share },
|
||||
invite: { ...state.invite },
|
||||
})
|
||||
const nextId = await saveAppBannersConst(constId.value, snapshotForSave())
|
||||
constId.value = nextId
|
||||
loaded.value = true
|
||||
message.success(successText)
|
||||
@@ -60,6 +84,15 @@ async function save(successText = 'بنرهای اپ ذخیره شد') {
|
||||
}
|
||||
}
|
||||
|
||||
/** آپلود/حذف را با تأخیر کوتاه در const مینویسد تا رفرش تصویر را برنگرداند. */
|
||||
function saveSoon(successText = 'بنر ذخیره شد') {
|
||||
if (saveTimer) clearTimeout(saveTimer)
|
||||
saveTimer = setTimeout(() => {
|
||||
saveTimer = null
|
||||
saveChain = saveChain.then(() => save(successText))
|
||||
}, 450)
|
||||
}
|
||||
|
||||
export function useAppBannersAdmin() {
|
||||
void load()
|
||||
|
||||
@@ -71,5 +104,6 @@ export function useAppBannersAdmin() {
|
||||
saving,
|
||||
load,
|
||||
save,
|
||||
saveSoon,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="banner-image-upload">
|
||||
<a-upload
|
||||
v-if="!imageUrl"
|
||||
v-if="!displayUrl"
|
||||
class="banner-image-upload__zone"
|
||||
name="file"
|
||||
accept="image/*"
|
||||
@@ -26,7 +26,7 @@
|
||||
</a-upload>
|
||||
|
||||
<div v-else class="banner-image-upload__filled">
|
||||
<img :src="imageUrl" alt="" class="banner-image-upload__img" />
|
||||
<img :src="displayUrl" alt="" class="banner-image-upload__img" />
|
||||
<div class="banner-image-upload__bar">
|
||||
<a-upload
|
||||
name="file"
|
||||
@@ -59,14 +59,14 @@ import { LoadingOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { uploadURL } from '@root/config'
|
||||
import { getToken } from '@/utils'
|
||||
import { normalizeUploadFileResponse } from '@/utils/file'
|
||||
import { normalizeUploadFileResponse, toAbsoluteFileUrl } from '@/utils/file'
|
||||
import {
|
||||
createImageBeforeUpload,
|
||||
createImageCustomRequest
|
||||
} from '@/utils/prepareImageForUpload'
|
||||
import type { UploadProps } from 'ant-design-vue'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
imageUrl?: string | null
|
||||
}>()
|
||||
|
||||
@@ -75,6 +75,8 @@ const emit = defineEmits<{
|
||||
remove: []
|
||||
}>()
|
||||
|
||||
const displayUrl = computed(() => toAbsoluteFileUrl(props.imageUrl) ?? props.imageUrl ?? '')
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const fileHeader = computed(() => ({
|
||||
@@ -99,7 +101,7 @@ const customRequest: UploadProps['customRequest'] = createImageCustomRequest({
|
||||
message.error('خطا در ساخت آدرس تصویر')
|
||||
return
|
||||
}
|
||||
emit('upload', fileResource.full_src)
|
||||
emit('upload', toAbsoluteFileUrl(fileResource.full_src) ?? fileResource.full_src)
|
||||
message.success('تصویر با موفقیت آپلود شد')
|
||||
},
|
||||
onFailed: () => {
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
<div class="profile-banner-card__upload">
|
||||
<BannerImageUpload
|
||||
:image-url="state[slot.key].light"
|
||||
@upload="state[slot.key].light = $event"
|
||||
@remove="state[slot.key].light = null"
|
||||
@upload="onThemeUpload(slot.key, 'light', $event)"
|
||||
@remove="onThemeRemove(slot.key, 'light')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,8 +60,8 @@
|
||||
<div class="profile-banner-card__upload profile-banner-card__upload--dark">
|
||||
<BannerImageUpload
|
||||
:image-url="state[slot.key].dark"
|
||||
@upload="state[slot.key].dark = $event"
|
||||
@remove="state[slot.key].dark = null"
|
||||
@upload="onThemeUpload(slot.key, 'dark', $event)"
|
||||
@remove="onThemeRemove(slot.key, 'dark')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,7 +84,7 @@ import {
|
||||
} from '@/utils/adminHelpCopy'
|
||||
import BannerImageUpload from './BannerImageUpload.vue'
|
||||
|
||||
const { state, saving, save } = useAppBannersAdmin()
|
||||
const { state, saving, save, saveSoon } = useAppBannersAdmin()
|
||||
/** پیشفرض همه کالپسها بسته — فقط هدرها دیده میشوند */
|
||||
const activeKey = ref<(string | number)[]>([])
|
||||
|
||||
@@ -178,6 +178,16 @@ const groups: Array<{
|
||||
async function handleSubmit() {
|
||||
await save('تصاویر تم اپ ذخیره شد')
|
||||
}
|
||||
|
||||
function onThemeUpload(key: AppThemeBannerKey, theme: 'light' | 'dark', url: string) {
|
||||
state[key][theme] = url
|
||||
saveSoon('تصویر بنر ذخیره شد')
|
||||
}
|
||||
|
||||
function onThemeRemove(key: AppThemeBannerKey, theme: 'light' | 'dark') {
|
||||
state[key][theme] = null
|
||||
saveSoon('تصویر بنر حذف شد')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
@@ -83,8 +83,8 @@
|
||||
<div class="banner-card__preview">
|
||||
<BannerImageUpload
|
||||
:image-url="item.icon"
|
||||
@upload="item.icon = $event"
|
||||
@remove="item.icon = null"
|
||||
@upload="onSliderUpload(index, $event)"
|
||||
@remove="onSliderRemove(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,7 +146,7 @@ import {
|
||||
} from '@/utils/adminHelpCopy'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const { state, saving, save, load } = useAppBannersAdmin()
|
||||
const { state, saving, save, load, saveSoon } = useAppBannersAdmin()
|
||||
/** اگر اسلایدری هست، پنل را باز نگه دار تا خالی بهنظر نرسد */
|
||||
const sliderActiveKey = ref<(string | number)[]>(['slider'])
|
||||
const showSettingsProfileBanners = computed(() => hasAppUiConfig())
|
||||
@@ -214,6 +214,21 @@ function addRow() {
|
||||
|
||||
function removeRow(index: number) {
|
||||
state.slider.splice(index, 1)
|
||||
saveSoon('اسلاید حذف شد')
|
||||
}
|
||||
|
||||
function onSliderUpload(index: number, url: string) {
|
||||
const slide = state.slider[index]
|
||||
if (!slide) return
|
||||
slide.icon = url
|
||||
saveSoon('تصویر اسلاید ذخیره شد')
|
||||
}
|
||||
|
||||
function onSliderRemove(index: number) {
|
||||
const slide = state.slider[index]
|
||||
if (!slide) return
|
||||
slide.icon = null
|
||||
saveSoon('تصویر اسلاید حذف شد')
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<section class="colleague-accounts">
|
||||
<header class="colleague-accounts__head">
|
||||
<div>
|
||||
<h3 class="colleague-accounts__title">کد حسابداری معامله با همکار</h3>
|
||||
<p class="colleague-accounts__desc">
|
||||
برای هر خرید/فروشی که نزد همکار ثبت میشود، سند روی این کد حساب در سیستم حسابداری فعال زده میشود.
|
||||
</p>
|
||||
</div>
|
||||
<BaseButton type="primary" :loading="saving" @click="save">
|
||||
ذخیره کدها
|
||||
</BaseButton>
|
||||
</header>
|
||||
|
||||
<div class="colleague-accounts__grid">
|
||||
<label
|
||||
v-for="field in fields"
|
||||
:key="field.key"
|
||||
class="colleague-accounts__field"
|
||||
:class="{ 'colleague-accounts__field--active': field.active }"
|
||||
>
|
||||
<span class="colleague-accounts__label">
|
||||
{{ field.label }}
|
||||
<span v-if="field.active" class="colleague-accounts__badge">فعال</span>
|
||||
</span>
|
||||
<a-input
|
||||
v-model:value="values[field.key]"
|
||||
:placeholder="field.placeholder"
|
||||
allow-clear
|
||||
class="colleague-accounts__input"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { getConfiguration, setConfiguration } from '@/api/configuration'
|
||||
import type { Const } from '@/model/configuration'
|
||||
import {
|
||||
accountingProviderSetting,
|
||||
initWalletSettingsFromApi,
|
||||
type AccountingProvider,
|
||||
} from '@/utils/config'
|
||||
|
||||
const KEYS = {
|
||||
zarin: 'montzar_account_zarin',
|
||||
tahesab: 'montzar_account_tahesab',
|
||||
pargar: 'montzar_account_pargar',
|
||||
} as const
|
||||
|
||||
const values = reactive<Record<string, string>>({
|
||||
[KEYS.zarin]: '',
|
||||
[KEYS.tahesab]: '',
|
||||
[KEYS.pargar]: '',
|
||||
})
|
||||
|
||||
const rowIds = reactive<Record<string, number | null>>({
|
||||
[KEYS.zarin]: null,
|
||||
[KEYS.tahesab]: null,
|
||||
[KEYS.pargar]: null,
|
||||
})
|
||||
|
||||
const saving = ref(false)
|
||||
const loaded = ref(false)
|
||||
|
||||
const fields = computed(() => {
|
||||
const provider = accountingProviderSetting.value as AccountingProvider
|
||||
return [
|
||||
{
|
||||
key: KEYS.zarin,
|
||||
label: 'کد حساب زرین',
|
||||
placeholder: 'کد حساب همکار در زرین',
|
||||
active: provider === 'zarin',
|
||||
},
|
||||
{
|
||||
key: KEYS.tahesab,
|
||||
label: 'کد حساب تهحساب',
|
||||
placeholder: 'کد حساب همکار در تهحساب',
|
||||
active: provider === 'tahesab',
|
||||
},
|
||||
{
|
||||
key: KEYS.pargar,
|
||||
label: 'کد حساب پرگار',
|
||||
placeholder: 'کد حساب همکار در پرگار',
|
||||
active: provider === 'pargar',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
async function load() {
|
||||
await initWalletSettingsFromApi()
|
||||
const rows = await getConfiguration()
|
||||
const list = Array.isArray(rows) ? rows : []
|
||||
for (const key of Object.values(KEYS)) {
|
||||
const row = list.find((item) => item?.key === key)
|
||||
values[key] = row?.value != null ? String(row.value) : ''
|
||||
rowIds[key] = row?.id != null ? Number(row.id) : null
|
||||
}
|
||||
loaded.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
for (const key of Object.values(KEYS)) {
|
||||
await setConfiguration({
|
||||
id: rowIds[key] ?? undefined,
|
||||
key,
|
||||
value: (values[key] ?? '').trim(),
|
||||
title_fa:
|
||||
key === KEYS.zarin
|
||||
? 'کد حساب همکار در زرین'
|
||||
: key === KEYS.tahesab
|
||||
? 'کد حساب همکار در تهحساب'
|
||||
: 'کد حساب همکار در پرگار',
|
||||
is_active: true,
|
||||
can_edit: true,
|
||||
type: 'text',
|
||||
description:
|
||||
'کد حساب طرف حساب همکار؛ سند خرید/فروش پوشششده روی Montzar روی این حساب ثبت میشود.',
|
||||
} satisfies Const)
|
||||
}
|
||||
message.success('کدهای حساب همکار ذخیره شد')
|
||||
await load()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error('ذخیره کد حساب همکار ناموفق بود')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void load()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.colleague-accounts {
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem 1.125rem;
|
||||
border: 1px solid var(--admin-border, rgba(255, 255, 255, 0.12));
|
||||
border-radius: 14px;
|
||||
background: var(--admin-bg-elevated, rgba(28, 28, 34, 0.92));
|
||||
}
|
||||
|
||||
.colleague-accounts__head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.colleague-accounts__title {
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--admin-text, #fff);
|
||||
}
|
||||
|
||||
.colleague-accounts__desc {
|
||||
margin: 0.35rem 0 0;
|
||||
max-width: 42rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.7;
|
||||
color: var(--admin-text-secondary, rgba(255, 255, 255, 0.65));
|
||||
}
|
||||
|
||||
.colleague-accounts__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.875rem;
|
||||
|
||||
@media (min-width: 900px) {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.colleague-accounts__field {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--admin-border, rgba(255, 255, 255, 0.1));
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--admin-bg-muted, #1a1a1f) 80%, transparent);
|
||||
|
||||
&--active {
|
||||
border-color: color-mix(in srgb, #d4af37 55%, transparent);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, #d4af37 18%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.colleague-accounts__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--admin-text, #fff);
|
||||
}
|
||||
|
||||
.colleague-accounts__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(212, 175, 55, 0.18);
|
||||
color: #d4af37;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.colleague-accounts__input {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
||||
<BasicShowContext title="تراکنش همکار">
|
||||
<BasicShowContext title="تراکنش با اپلیکیشن همکار">
|
||||
<div class="min-w-0 w-full max-w-full overflow-x-auto">
|
||||
<BaseTable v-bind="tblProps" @register="submit" />
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div>
|
||||
<BasicMainCard>
|
||||
<div class="flex h-full w-full min-w-0 max-w-full flex-col overflow-hidden">
|
||||
<ColleagueAccountingAccounts />
|
||||
<tbl ref="table" class="min-h-0 flex-1" />
|
||||
</div>
|
||||
</BasicMainCard>
|
||||
@@ -11,6 +12,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import tbl from './components/table.vue'
|
||||
import ColleagueAccountingAccounts from './components/ColleagueAccountingAccounts.vue'
|
||||
|
||||
const table = ref<InstanceType<typeof tbl> | null>(null)
|
||||
</script>
|
||||
|
||||
@@ -325,7 +325,8 @@ const filteredRows = computed(() =>
|
||||
if (!menu && !isSystemMode.value && TRADING_HOURS_SETTING_KEY_SET.has(row.key || '')) {
|
||||
return false
|
||||
}
|
||||
if (isSystemMode.value) return true
|
||||
// پیشرفته فقط can_edit=0؛ قابلویرایشها (مثل دسترسی مشتری) فقط در «تنظیمات»
|
||||
if (isSystemMode.value) return !isEditable(row)
|
||||
return isEditable(row)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -36,7 +36,7 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [
|
||||
title: 'کارمزدها',
|
||||
icon: 'vuesax-linear:dollar-circle',
|
||||
match: (k) =>
|
||||
/^(fee_|buy_fee|sell_fee|up_price|down_price|price_extreme_date)/.test(k) ||
|
||||
/^(fee_|buy_fee|sell_fee)/.test(k) ||
|
||||
k.includes('charge_percentage')
|
||||
},
|
||||
{
|
||||
@@ -49,7 +49,8 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [
|
||||
id: 'price_source',
|
||||
title: 'منبع قیمت و تابلو',
|
||||
icon: 'vuesax-linear:chart-2',
|
||||
match: (k) => /^(price_provider|zaarbox_|main_product$)/.test(k)
|
||||
match: (k) =>
|
||||
/^(chart_use_local_data|zaarbox_price_key|show_gold_ounce_price)$/.test(k)
|
||||
},
|
||||
{
|
||||
id: 'content',
|
||||
@@ -71,21 +72,16 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [
|
||||
title: 'همکار Montzar',
|
||||
icon: 'vuesax-linear:link-2',
|
||||
match: (k) =>
|
||||
(k.startsWith('montzar_') && !['montzar_base_url', 'montzar_closed'].includes(k)) ||
|
||||
k === 'display_montzar'
|
||||
},
|
||||
{
|
||||
id: 'store',
|
||||
title: 'سفارش خارجی',
|
||||
icon: 'vuesax-linear:shop',
|
||||
match: (k) => /^external_order/.test(k)
|
||||
/^(montzar_mobile|montzar_open_buy|montzar_open_sell|montzar_password|montzar_product_id|display_montzar)$/.test(
|
||||
k
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'deposit_methods',
|
||||
title: 'روشهای واریز',
|
||||
icon: 'vuesax-linear:card-send',
|
||||
match: (k) =>
|
||||
k.startsWith('deposit_method_')
|
||||
k === 'deposit_method_card_to_card'
|
||||
|| k === 'deposit_account_holder'
|
||||
|| k === 'deposit_card_number'
|
||||
|| k === 'deposit_account_number'
|
||||
@@ -101,21 +97,6 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [
|
||||
|| k === 'deposit_account_number'
|
||||
|| k === 'deposit_sheba',
|
||||
},
|
||||
{
|
||||
id: 'gateway',
|
||||
title: 'درگاه پرداخت',
|
||||
match: (k) => k === 'deposit_method_gateway',
|
||||
},
|
||||
{
|
||||
id: 'identified',
|
||||
title: 'شناسه واریز',
|
||||
match: (k) => k === 'deposit_method_identified',
|
||||
},
|
||||
{
|
||||
id: 'shetab',
|
||||
title: 'شبکه شتابی',
|
||||
match: (k) => k === 'deposit_method_shetab',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -163,14 +144,15 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [
|
||||
}
|
||||
]
|
||||
|
||||
/** گروههای صفحه دیپلوی — شامل تنظیمات can_edit=0 */
|
||||
/** گروههای صفحه دیپلوی — فقط تنظیمات can_edit=0 (قابلویرایشها در EDITABLE_SETTING_GROUPS) */
|
||||
export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
|
||||
{
|
||||
id: 'customer_features',
|
||||
title: 'دسترسی مشتری به قابلیتها',
|
||||
icon: 'vuesax-linear:people',
|
||||
// فقط فلگهای مالک سوپرادمین — banner/tracking/نمایش محصول در «تنظیمات» عادیاند
|
||||
match: (k) =>
|
||||
/^(enable_automation|enable_conditional_order|enable_product_funds|has_page_price_trigger|has_page_notification|has_page_gift_card|enable_customer_level_profit|banner_enabled|jibit_enabled|customer_direct_sms|has_page_earn_money|group_sms|customer_special_pricing|tracking_systems_enabled|permision__enabled|show_to_users_chat)$/.test(
|
||||
/^(enable_automation|enable_conditional_order|enable_product_funds|has_page_price_trigger|has_page_notification|has_page_gift_card|enable_customer_level_profit|jibit_enabled|customer_direct_sms|has_page_earn_money|group_sms|customer_special_pricing|permision__enabled|show_to_users_chat)$/.test(
|
||||
k
|
||||
)
|
||||
},
|
||||
@@ -201,7 +183,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
|
||||
title: 'منبع قیمت و تابلو',
|
||||
icon: 'vuesax-linear:dollar-circle',
|
||||
match: (k) =>
|
||||
/^(price_provider|zaarbox_|main_product$)/.test(k) ||
|
||||
/^(price_provider|zaarbox_|main_product$|up_price|down_price|price_extreme_date)$/.test(k) ||
|
||||
k.includes('charge_percentage')
|
||||
},
|
||||
{
|
||||
@@ -222,14 +204,28 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
|
||||
icon: 'vuesax-linear:card-send',
|
||||
match: (k) =>
|
||||
k.startsWith('external_payment_')
|
||||
|| k === 'wallet_payment_provider',
|
||||
|| k === 'wallet_payment_provider'
|
||||
|| k === 'deposit_method_gateway'
|
||||
|| k === 'deposit_method_identified'
|
||||
|| k === 'deposit_method_shetab',
|
||||
subsections: [
|
||||
{
|
||||
id: 'gateway',
|
||||
title: 'درگاه فروشگاه / پرداخت آنلاین',
|
||||
match: (k) =>
|
||||
k.startsWith('external_payment_')
|
||||
|| k === 'wallet_payment_provider',
|
||||
|| k === 'wallet_payment_provider'
|
||||
|| k === 'deposit_method_gateway',
|
||||
},
|
||||
{
|
||||
id: 'identified',
|
||||
title: 'شناسه واریز',
|
||||
match: (k) => k === 'deposit_method_identified',
|
||||
},
|
||||
{
|
||||
id: 'shetab',
|
||||
title: 'شبکه شتابی',
|
||||
match: (k) => k === 'deposit_method_shetab',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -175,23 +175,10 @@ export function supportMessengerIdPlaceholder(messenger?: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* کلیدهایی که کد tenant مستقیم مینویسد — can_edit=1 در DB (خارج از سوپرادمین)،
|
||||
* فقط در «تنظیمات پیشرفته» دیده میشوند و از UI قابل ویرایش نیستند.
|
||||
* همتراز SuperAdminProgramSettingsOverlay::RUNTIME_KEYS (+ tracking_systems جدا).
|
||||
* کلیدهایی که کد tenant مستقیم مینویسد — از UI قابل ویرایش نیستند.
|
||||
* همتراز SuperAdminProgramSettingsOverlay::RUNTIME_KEYS.
|
||||
*/
|
||||
export const SETTING_CODE_MANAGED_KEYS = new Set([
|
||||
'montzar_last_product_log',
|
||||
'montzar_order_response',
|
||||
'montzar_live_price',
|
||||
'montzar_mazaneh',
|
||||
'montzar_diff',
|
||||
'montzar_truly_buy_mazaneh',
|
||||
'montzar_truly_sell_mazaneh',
|
||||
'montzar_token',
|
||||
'montzar_closed',
|
||||
'up_price',
|
||||
'down_price',
|
||||
'price_extreme_date',
|
||||
'do_send_liman',
|
||||
])
|
||||
|
||||
@@ -327,6 +314,9 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
||||
gift_for_referral: 'number',
|
||||
referral_bonus_amount: 'text',
|
||||
montzar_product_id: 'number',
|
||||
montzar_account_zarin: 'text',
|
||||
montzar_account_tahesab: 'text',
|
||||
montzar_account_pargar: 'text',
|
||||
balance_report_exclude_customer_id: 'number',
|
||||
jibit_verification: 'switch',
|
||||
register_jibit_verify: 'switch',
|
||||
@@ -775,6 +765,12 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
'رمز عبور حساب اتصال به سرویس همکار قیمت. محرمانه نگه دارید.',
|
||||
montzar_product_id:
|
||||
'محصول مرجع آبشده در API همکار — از لیست محصولات همکار انتخاب کنید.',
|
||||
montzar_account_zarin:
|
||||
'کد حساب طرفحساب همکار در زرین؛ سند خرید/فروش پوشششده روی همکار روی این حساب ثبت میشود.',
|
||||
montzar_account_tahesab:
|
||||
'کد حساب طرفحساب همکار در تهحساب؛ سند خرید/فروش پوشششده روی همکار روی این حساب ثبت میشود.',
|
||||
montzar_account_pargar:
|
||||
'کد حساب طرفحساب همکار در پرگار؛ سند خرید/فروش پوشششده روی همکار روی این حساب ثبت میشود.',
|
||||
montzar_base_url:
|
||||
'آدرس پایه سرور سرویس همکار (بدون اسلش انتهایی).',
|
||||
zaarbox_api_url:
|
||||
@@ -1081,6 +1077,9 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
|
||||
main_product: 'محصول مرجع قیمت',
|
||||
montzar_open_buy: 'خرید از همکار',
|
||||
montzar_open_sell: 'فروش از همکار',
|
||||
montzar_account_zarin: 'کد حساب همکار در زرین',
|
||||
montzar_account_tahesab: 'کد حساب همکار در تهحساب',
|
||||
montzar_account_pargar: 'کد حساب همکار در پرگار',
|
||||
external_order_api_enabled: 'API سفارش خارجی',
|
||||
payping_fin_sync_enabled: 'همگامسازی با حسابداری مالی',
|
||||
accounting_provider: 'سیستم حسابداری',
|
||||
@@ -1131,12 +1130,21 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
|
||||
change_mesghal: 'وزن مثقال (گرم)'
|
||||
}
|
||||
|
||||
/** غیرقابل ویرایش در پنل — همتراز deploy_settings_const.sql */
|
||||
/** غیرقابل ویرایش در پنل عادی (can_edit=0) — در تنظیمات پیشرفته قابل ویرایش (ادمین دیپلوی) */
|
||||
export const SETTING_LOCKED_KEYS = new Set([
|
||||
'admin_url', 'apk_download_link', 'app_url',
|
||||
'bale_bot_token', 'banner_enabled',
|
||||
'chart_id', 'chart_use_local_data',
|
||||
'external_order_api_enabled',
|
||||
'chart_id',
|
||||
'up_price', 'down_price', 'price_extreme_date',
|
||||
'main_product', 'price_provider', 'zaarbox_api_url',
|
||||
'montzar_base_url', 'montzar_diff', 'montzar_closed', 'montzar_order_response',
|
||||
'montzar_token', 'montzar_live_price', 'montzar_last_product_log',
|
||||
'montzar_truly_buy_mazaneh', 'montzar_truly_sell_mazaneh', 'montzar_mazaneh',
|
||||
'node_client_id', 'node_shared_secret', 'external_order_api_enabled',
|
||||
'deposit_method_gateway', 'deposit_method_identified', 'deposit_method_shetab',
|
||||
'payping_verify_url', 'payping_start_url', 'payping_strict_failed_withdraw',
|
||||
'payping_fin_token', 'payping_round_toman', 'payping_gateway_items',
|
||||
'payping_gateway_name', 'payping_fin_customer_id', 'payping_fin_url',
|
||||
'external_payment_enabled',
|
||||
'external_payment_base_url',
|
||||
'external_payment_client_id',
|
||||
@@ -1162,15 +1170,11 @@ export const SETTING_LOCKED_KEYS = new Set([
|
||||
'is_store',
|
||||
'sms_panel',
|
||||
'logo_login', 'logo_register', 'title_pwa', 'title_fav',
|
||||
'main_product', 'main_wallet_products', 'metal_type', 'main_metal',
|
||||
'main_wallet_products', 'metal_type', 'main_metal',
|
||||
'montify_name',
|
||||
'montzar_base_url',
|
||||
'node_client_id', 'node_shared_secret',
|
||||
'payping_fin_customer_id', 'payping_fin_sync_enabled', 'payping_fin_token', 'payping_fin_url',
|
||||
'payping_gateway_items', 'payping_gateway_name',
|
||||
'payping_round_toman', 'payping_start_url', 'payping_strict_failed_withdraw', 'payping_verify_url',
|
||||
'payping_fin_sync_enabled',
|
||||
'permision__enabled',
|
||||
'price_provider', 'profit_gold_api_enabled',
|
||||
'profit_gold_api_enabled',
|
||||
'send_to_tahesab', 'send_to_pargar', 'accounting_provider',
|
||||
'sse_path',
|
||||
'tahesab_bank_account', 'tahesab_db', 'tahesab_host', 'tahesab_port', 'tahesab_token',
|
||||
@@ -1183,15 +1187,15 @@ export const SETTING_LOCKED_KEYS = new Set([
|
||||
'price_channel_bale_to', 'price_channel_telegram_to', 'price_channel_rubika_to',
|
||||
'unit',
|
||||
'wss_enabled',
|
||||
'zaarbox_api_url',
|
||||
'show_to_users_chat', 'chat_api', 'chat_api_key', 'chat_upload_url', 'chat_download_url'
|
||||
])
|
||||
|
||||
/** قابل ویرایش در پنل — همتراز deploy_settings_const.sql */
|
||||
/** قابل ویرایش در پنل عادی — همتراز deploy_settings_const.sql */
|
||||
export const SETTING_EDITABLE_KEYS = new Set([
|
||||
'name_fa',
|
||||
'auto_active_customer', 'balance_report_exclude_customer_id',
|
||||
'buy_fee', 'charge_notif', 'close_price',
|
||||
'chart_use_local_data',
|
||||
'require_zero_main_gold_before_other_buy',
|
||||
'sms_notif_order_admin', 'sms_notif_order_customer',
|
||||
'sms_notif_withdraw_admin', 'sms_notif_withdraw_otp', 'sms_notif_login_otp',
|
||||
@@ -1201,7 +1205,7 @@ export const SETTING_EDITABLE_KEYS = new Set([
|
||||
'sms_notif_price_trigger', 'sms_notif_wallet_alert', 'sms_notif_support_admin',
|
||||
'deposit_sms_support_number', 'deposit_support_message_text',
|
||||
'deposit_account_holder', 'deposit_card_number', 'deposit_account_number', 'deposit_sheba',
|
||||
'deposit_method_card_to_card', 'deposit_method_gateway', 'deposit_method_identified', 'deposit_method_shetab',
|
||||
'deposit_method_card_to_card',
|
||||
'endDay',
|
||||
'fee_free', 'fee_service_charge_percentage',
|
||||
'gift_for_referral', 'gift_for_referrer',
|
||||
@@ -1211,9 +1215,7 @@ export const SETTING_EDITABLE_KEYS = new Set([
|
||||
'min_first_buy', 'min_withdraw', 'min_deposit', 'minimum_sell',
|
||||
'montzar_mobile', 'montzar_open_buy', 'montzar_open_sell',
|
||||
'montzar_password', 'montzar_product_id',
|
||||
'montzar_closed', 'montzar_diff', 'montzar_last_product_log', 'montzar_live_price', 'montzar_mazaneh',
|
||||
'montzar_order_response', 'montzar_token',
|
||||
'montzar_truly_buy_mazaneh', 'montzar_truly_sell_mazaneh',
|
||||
'montzar_account_zarin', 'montzar_account_tahesab', 'montzar_account_pargar',
|
||||
'open_register',
|
||||
'referral_id', 'referral_limit', 'referral_gift_on_first_order', 'referral_gift_buy_from_montzar',
|
||||
'register_jibit_verify', 'register_referral_bonus_message', 'register_referral_sms_to_referrer',
|
||||
@@ -1222,10 +1224,8 @@ export const SETTING_EDITABLE_KEYS = new Set([
|
||||
'products_site_link_enabled', 'products_site_link_url', 'products_site_link_text',
|
||||
'startDay',
|
||||
'support_messengers', 'support_messenger_text',
|
||||
'up_price', 'down_price', 'price_extreme_date', 'do_send_liman',
|
||||
'do_send_liman',
|
||||
'has_chat',
|
||||
'deposit_support_message_text', 'deposit_account_holder', 'deposit_card_number', 'deposit_account_number', 'deposit_sheba',
|
||||
'deposit_method_card_to_card', 'deposit_method_gateway', 'deposit_method_identified', 'deposit_method_shetab',
|
||||
'text_about', 'text_rule', 'about_software', 'enamad_url', 'enamad_logo',
|
||||
'zaarbox_price_key',
|
||||
'price_channel_enabled', 'price_channel_messengers', 'price_channel_diff',
|
||||
|
||||
@@ -121,7 +121,7 @@ const HesabKetab: RouteRecordRaw = {
|
||||
component: () => import('@/pages/menuPages/colleagueOrders/index.vue'),
|
||||
meta: {
|
||||
icon: 'vuesax-linear:people',
|
||||
title: 'تراکنش همکار',
|
||||
title: 'تراکنش با اپلیکیشن همکار',
|
||||
order: 6.8
|
||||
},
|
||||
beforeEnter() {
|
||||
|
||||
+12
-6
@@ -57,12 +57,12 @@ export function getFileBaseUrl(): string {
|
||||
getConfigApp('file_base')
|
||||
|
||||
if (typeof fromConfig === 'string' && fromConfig.trim()) {
|
||||
return fromConfig.replace(/\/$/, '')
|
||||
return preferHttpsFileUrl(fromConfig.replace(/\/$/, ''))
|
||||
}
|
||||
|
||||
const storageBase = baseURLStorage || baseURL
|
||||
if (storageBase) {
|
||||
return storageBase.replace(/\/$/, '')
|
||||
return preferHttpsFileUrl(storageBase.replace(/\/$/, ''))
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -72,6 +72,12 @@ export function getFileBaseUrl(): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
/** صفحهٔ ادمین https است؛ URLهای http پشت CDN در پیشنمایش بلاک میشوند. */
|
||||
export function preferHttpsFileUrl(url: string): string {
|
||||
if (/localhost|127\.0\.0\.1/i.test(url)) return url
|
||||
return url.replace(/^http:\/\//i, 'https://')
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns API paths like "storage/1405/02/27/file.jpg" into a full URL.
|
||||
*/
|
||||
@@ -81,14 +87,14 @@ export function toAbsoluteFileUrl(path?: string | null): string | undefined {
|
||||
const trimmed = path.trim()
|
||||
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
return trimmed
|
||||
return preferHttpsFileUrl(trimmed)
|
||||
}
|
||||
|
||||
const storageMatch = trimmed.match(/(?:^|\/)storage\/(.+)$/i)
|
||||
if (storageMatch) {
|
||||
const base = getFileBaseUrl()
|
||||
if (base) {
|
||||
return `${base}/storage/${storageMatch[1]}`
|
||||
return preferHttpsFileUrl(`${base}/storage/${storageMatch[1]}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,10 +106,10 @@ export function toAbsoluteFileUrl(path?: string | null): string | undefined {
|
||||
if (!base) return undefined
|
||||
|
||||
if (trimmed.startsWith('/')) {
|
||||
return `${base}${trimmed}`
|
||||
return preferHttpsFileUrl(`${base}${trimmed}`)
|
||||
}
|
||||
|
||||
return `${base}/${trimmed.replace(/^\//, '')}`
|
||||
return preferHttpsFileUrl(`${base}/${trimmed.replace(/^\//, '')}`)
|
||||
}
|
||||
|
||||
function isBareFileName(value: string): boolean {
|
||||
|
||||
Reference in New Issue
Block a user