feat(sms): 新增节点短信提醒(审核未通过/被租用需辅助/上架未成交)
This commit is contained in:
@@ -335,6 +335,8 @@ export interface DistributionSiteSMSConfig {
|
||||
endpoint: string
|
||||
sign_name: string
|
||||
login_template_code: string
|
||||
/** 站点自有的节点短信模板 CODE(key 为节点标识),未配置的节点回退主站模板。 */
|
||||
business_templates: Record<string, string>
|
||||
}
|
||||
|
||||
export interface UpdateDistributionSiteSMSConfigPayload {
|
||||
@@ -345,6 +347,8 @@ export interface UpdateDistributionSiteSMSConfigPayload {
|
||||
endpoint?: string
|
||||
sign_name?: string
|
||||
login_template_code?: string
|
||||
/** 整体覆盖站点节点模板映射,传空对象即清空并回退主站模板。 */
|
||||
business_templates?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface TestDistributionSiteSMSConfigPayload {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 节点短信模板配置:与后端 internal/modules/sms/nodes.go 登记一致。
|
||||
* 阿里云要求模板提前报备审核,因此这里只维护模板 CODE,文案由平台模板决定。
|
||||
*/
|
||||
export type SmsNodeCode =
|
||||
| 'listing_review_rejected'
|
||||
| 'order_handoff_assist'
|
||||
| 'listing_stale_price_suggestion'
|
||||
|
||||
export interface SmsNodeMeta {
|
||||
code: SmsNodeCode
|
||||
name: string
|
||||
recipient: string
|
||||
description: string
|
||||
/** 报备模板的参考 CODE,仅作提示,真实值以配置为准。 */
|
||||
templateCode: string
|
||||
/** 报备模板的参考文案,仅供识别节点,实际文案由平台模板决定。 */
|
||||
sample: string
|
||||
supportsStaleDays: boolean
|
||||
}
|
||||
|
||||
export const smsNodeMetas: SmsNodeMeta[] = [
|
||||
{
|
||||
code: 'listing_review_rejected',
|
||||
name: '账号审核未通过',
|
||||
recipient: '号主',
|
||||
description: '号主上架的账号被后台审核拒绝时,提醒登录网站修改后重新上架。',
|
||||
templateCode: 'SMS_512520993',
|
||||
sample: '板板,您在我们商行上架的账号审核未通过,请登录网站进行修改重新上架',
|
||||
supportsStaleDays: false,
|
||||
},
|
||||
{
|
||||
code: 'order_handoff_assist',
|
||||
name: '账号被租用需辅助登录',
|
||||
recipient: '号主',
|
||||
description: '订单支付成功进入待交接后,提醒号主及时辅助登录,避免订单被强制结束。',
|
||||
templateCode: 'SMS_512540945',
|
||||
sample:
|
||||
'板板,您的订单已有人下单,下单方需要辅助登录,请及时留意平台信息!长时间未辅助平台会强制结束订单',
|
||||
supportsStaleDays: false,
|
||||
},
|
||||
{
|
||||
code: 'listing_stale_price_suggestion',
|
||||
name: '上架多日未成交降价建议',
|
||||
recipient: '号主',
|
||||
description: '账号上架满指定天数仍未被租用,建议号主适当调整比例,加快出租。',
|
||||
templateCode: 'SMS_512620985',
|
||||
sample:
|
||||
'板板,您的三角洲账号上架三天还未成功出售,建议您比例适当让一点,比例越高,出租越快、匹配越顺。具体调多少按自己心理价位定',
|
||||
supportsStaleDays: true,
|
||||
},
|
||||
]
|
||||
|
||||
export interface SmsNodeConfig {
|
||||
enabled: boolean
|
||||
template_code: string
|
||||
/** 仅「上架多日未成交降价建议」使用:上架满多少天仍未成交才提醒。 */
|
||||
stale_days?: number
|
||||
}
|
||||
|
||||
export interface SmsBusinessTemplatesConfig {
|
||||
enabled: boolean
|
||||
nodes: Record<string, SmsNodeConfig>
|
||||
}
|
||||
|
||||
export const defaultSmsBusinessTemplatesConfig: SmsBusinessTemplatesConfig = {
|
||||
// 与后端默认值一致:总开关关闭,避免未配置模板就产生短信费用。
|
||||
enabled: false,
|
||||
nodes: {
|
||||
listing_review_rejected: { enabled: true, template_code: '' },
|
||||
order_handoff_assist: { enabled: true, template_code: '' },
|
||||
listing_stale_price_suggestion: { enabled: true, template_code: '', stale_days: 3 },
|
||||
},
|
||||
}
|
||||
|
||||
export function parseSmsBusinessTemplatesConfig(raw: string): SmsBusinessTemplatesConfig {
|
||||
const parsed = parseRawConfig(raw)
|
||||
const nodes: Record<string, SmsNodeConfig> = {}
|
||||
for (const meta of smsNodeMetas) {
|
||||
const fallback = defaultSmsBusinessTemplatesConfig.nodes[meta.code]
|
||||
const incoming = parsed.nodes?.[meta.code]
|
||||
const node: SmsNodeConfig = {
|
||||
enabled:
|
||||
typeof incoming?.enabled === 'boolean' ? incoming.enabled : (fallback?.enabled ?? true),
|
||||
template_code:
|
||||
typeof incoming?.template_code === 'string' ? incoming.template_code.trim() : '',
|
||||
}
|
||||
if (meta.supportsStaleDays) {
|
||||
node.stale_days = positiveInteger(incoming?.stale_days, fallback?.stale_days ?? 3)
|
||||
}
|
||||
nodes[meta.code] = node
|
||||
}
|
||||
return {
|
||||
enabled: typeof parsed.enabled === 'boolean' ? parsed.enabled : false,
|
||||
nodes,
|
||||
}
|
||||
}
|
||||
|
||||
export function cloneSmsBusinessTemplatesConfig(
|
||||
config: SmsBusinessTemplatesConfig
|
||||
): SmsBusinessTemplatesConfig {
|
||||
return JSON.parse(JSON.stringify(config)) as SmsBusinessTemplatesConfig
|
||||
}
|
||||
|
||||
function parseRawConfig(raw: string): Partial<SmsBusinessTemplatesConfig> {
|
||||
try {
|
||||
return JSON.parse(raw) as Partial<SmsBusinessTemplatesConfig>
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function positiveInteger(value: unknown, fallback: number) {
|
||||
const number = Number(value)
|
||||
return Number.isInteger(number) && number > 0 ? number : fallback
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import {
|
||||
cloneSmsBusinessTemplatesConfig,
|
||||
defaultSmsBusinessTemplatesConfig,
|
||||
parseSmsBusinessTemplatesConfig,
|
||||
smsNodeMetas,
|
||||
type SmsBusinessTemplatesConfig,
|
||||
} from '@/features/admin/api/smsNodeTemplates'
|
||||
import { updateSystemConfig, type SystemConfig } from '@/features/admin/api/systemConfigs'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
config: SystemConfig
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: boolean): void
|
||||
(event: 'saved'): void
|
||||
}>()
|
||||
|
||||
const submitting = ref(false)
|
||||
const draft = ref<SmsBusinessTemplatesConfig>(
|
||||
cloneSmsBusinessTemplatesConfig(defaultSmsBusinessTemplatesConfig)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.config.value] as const,
|
||||
([visible]) => {
|
||||
if (visible) draft.value = parseSmsBusinessTemplatesConfig(props.config.value)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function resetDefaults() {
|
||||
draft.value = cloneSmsBusinessTemplatesConfig(defaultSmsBusinessTemplatesConfig)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
submitting.value = true
|
||||
try {
|
||||
await updateSystemConfig(props.config.key, {
|
||||
value: JSON.stringify(draft.value),
|
||||
description: '节点短信模板配置(总开关、各业务节点模板 CODE)',
|
||||
})
|
||||
ElMessage.success('节点短信配置已更新')
|
||||
emit('saved')
|
||||
emit('update:modelValue', false)
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '节点短信配置保存失败'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
title="节点短信配置"
|
||||
width="860px"
|
||||
destroy-on-close
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
>
|
||||
<div class="sms-node-form">
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="只填阿里云已报备的模板 CODE,短信文案由平台模板决定;总开关关闭或模板留空时,对应节点不会发送短信。"
|
||||
/>
|
||||
<div class="sms-switch-row">
|
||||
<span>启用节点短信</span>
|
||||
<el-switch v-model="draft.enabled" />
|
||||
</div>
|
||||
|
||||
<el-table :data="smsNodeMetas" border>
|
||||
<el-table-column prop="name" label="节点" width="190">
|
||||
<template #default="{ row }">
|
||||
<div class="node-name">{{ row.name }}</div>
|
||||
<div class="node-recipient">收件人:{{ row.recipient }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="说明" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<div class="node-desc">{{ row.description }}</div>
|
||||
<div class="node-sample">参考文案:{{ row.sample }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="draft.nodes[row.code]!.enabled" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="模板 CODE" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-model="draft.nodes[row.code]!.template_code"
|
||||
:placeholder="`例如:${row.templateCode}`"
|
||||
/>
|
||||
<el-input-number
|
||||
v-if="row.supportsStaleDays"
|
||||
v-model="draft.nodes[row.code]!.stale_days"
|
||||
class="stale-days"
|
||||
:min="1"
|
||||
:max="30"
|
||||
:step="1"
|
||||
step-strictly
|
||||
/>
|
||||
<span v-if="row.supportsStaleDays" class="stale-unit">天后仍未成交才提醒</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button :disabled="submitting" @click="resetDefaults">恢复默认</el-button>
|
||||
<el-button :disabled="submitting" @click="emit('update:modelValue', false)">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSave">保存配置</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sms-node-form {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.sms-switch-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 40px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
color: #111827;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.node-name {
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.node-recipient {
|
||||
margin-top: 2px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.node-desc {
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.node-sample {
|
||||
margin-top: 4px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.stale-days {
|
||||
margin-top: 8px;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.stale-unit {
|
||||
margin-left: 8px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.sms-node-form :deep(.el-table) {
|
||||
min-width: 760px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from '@/features/admin/api/adminDistribution'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
import { smsNodeMetas } from '@/features/admin/api/smsNodeTemplates'
|
||||
import {
|
||||
distributionAdminBaseUrl,
|
||||
distributionAdminLoginUrl,
|
||||
@@ -143,6 +144,7 @@ const smsForm = reactive({
|
||||
masked_secret: '',
|
||||
has_secret: false,
|
||||
endpoint: '',
|
||||
business_templates: {} as Record<string, string>,
|
||||
})
|
||||
|
||||
function handleOpenSettlement(row: DistributionSite) {
|
||||
@@ -208,6 +210,10 @@ async function handleOpenSMS(row: DistributionSite) {
|
||||
smsForm.masked_secret = cfg.masked_secret || ''
|
||||
smsForm.has_secret = cfg.has_secret
|
||||
smsForm.endpoint = cfg.endpoint || ''
|
||||
// 逐节点初始化,未配置的节点留空表示回退主站模板。
|
||||
smsForm.business_templates = Object.fromEntries(
|
||||
smsNodeMetas.map(meta => [meta.code, cfg.business_templates?.[meta.code] || ''])
|
||||
)
|
||||
} catch (err) {
|
||||
ElMessage.error(readError(err, '读取分站短信配置失败'))
|
||||
} finally {
|
||||
@@ -215,6 +221,15 @@ async function handleOpenSMS(row: DistributionSite) {
|
||||
}
|
||||
}
|
||||
|
||||
function trimmedBusinessTemplates() {
|
||||
const out: Record<string, string> = {}
|
||||
for (const meta of smsNodeMetas) {
|
||||
const code = (smsForm.business_templates[meta.code] || '').trim()
|
||||
if (code) out[meta.code] = code
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
async function handleSMSSubmit() {
|
||||
if (smsForm.enabled) {
|
||||
if (!smsForm.sign_name.trim()) {
|
||||
@@ -244,6 +259,7 @@ async function handleSMSSubmit() {
|
||||
access_key_id: string
|
||||
access_key_secret?: string
|
||||
endpoint?: string
|
||||
business_templates: Record<string, string>
|
||||
} = {
|
||||
enabled: smsForm.enabled,
|
||||
provider: smsForm.provider || 'aliyun',
|
||||
@@ -251,6 +267,7 @@ async function handleSMSSubmit() {
|
||||
login_template_code: smsForm.login_template_code.trim(),
|
||||
access_key_id: smsForm.access_key_id.trim(),
|
||||
endpoint: smsForm.endpoint.trim(),
|
||||
business_templates: trimmedBusinessTemplates(),
|
||||
}
|
||||
if (smsSecretInput.value.trim()) {
|
||||
payload.access_key_secret = smsSecretInput.value.trim()
|
||||
@@ -1082,6 +1099,14 @@ onMounted(() => {
|
||||
<el-form-item label="API Endpoint">
|
||||
<el-input v-model="smsForm.endpoint" placeholder="默认 dysmsapi.aliyuncs.com" />
|
||||
</el-form-item>
|
||||
<el-divider content-position="left">节点短信模板(可选)</el-divider>
|
||||
<el-form-item v-for="meta in smsNodeMetas" :key="meta.code" :label="meta.name">
|
||||
<el-input
|
||||
v-model="smsForm.business_templates[meta.code]"
|
||||
:placeholder="`留空则用主站模板,例如:${meta.templateCode}`"
|
||||
/>
|
||||
<div class="sms-node-hint">{{ meta.description }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="连通性测试">
|
||||
<div style="display: flex; gap: 8px; width: 100%">
|
||||
<el-input v-model="smsTestPhone" placeholder="输入接收测试短信的手机号" />
|
||||
@@ -1344,4 +1369,12 @@ onMounted(() => {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.sms-node-hint {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,6 +19,10 @@ import {
|
||||
import type { OrderAgreements } from '@/features/orders/api/orders'
|
||||
import { fetchSystemConfigs, type SystemConfig } from '@/features/admin/api/systemConfigs'
|
||||
import { parseRenterGrowthConfig } from '@/features/admin/api/renterGrowth'
|
||||
import {
|
||||
parseSmsBusinessTemplatesConfig,
|
||||
smsNodeMetas,
|
||||
} from '@/features/admin/api/smsNodeTemplates'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
import { safeParseJSON } from '@/shared/utils/json'
|
||||
import { formatSystemConfigSelectValue } from '@/shared/utils/systemConfigOptions'
|
||||
@@ -40,6 +44,7 @@ import OrderAgreementsDialog from '../components/OrderAgreementsDialog.vue'
|
||||
import PostRentalNoticeDialog from '../components/PostRentalNoticeDialog.vue'
|
||||
import GeneralConfigDialog from '../components/GeneralConfigDialog.vue'
|
||||
import RenterGrowthConfigDialog from '../components/RenterGrowthConfigDialog.vue'
|
||||
import SmsNodeTemplatesDialog from '../components/SmsNodeTemplatesDialog.vue'
|
||||
import AutoWelcomeConfig from '../components/AutoWelcomeConfig.vue'
|
||||
import {
|
||||
fetchAutoWelcomeMessage,
|
||||
@@ -86,6 +91,7 @@ const agreementsVisible = ref(false)
|
||||
const postRentalNoticeVisible = ref(false)
|
||||
const generalVisible = ref(false)
|
||||
const renterGrowthVisible = ref(false)
|
||||
const smsNodeTemplatesVisible = ref(false)
|
||||
|
||||
const publishConfig = computed(
|
||||
() => configs.value.find(item => item.key === 'listing.publish_options') || null
|
||||
@@ -99,6 +105,9 @@ const listingPublishCooldownConfig = computed(
|
||||
const renterGrowthConfig = computed(
|
||||
() => configs.value.find(item => item.key === 'renter.growth_level_rules') || null
|
||||
)
|
||||
const smsNodeTemplatesConfig = computed(
|
||||
() => configs.value.find(item => item.key === 'sms.business_templates') || null
|
||||
)
|
||||
const homeAnnouncementsConfig = computed(
|
||||
() => configs.value.find(item => item.key === 'mobile.home_announcements') || null
|
||||
)
|
||||
@@ -167,7 +176,8 @@ const regularConfigs = computed(() =>
|
||||
item.key !== 'integration.paddle_ocr_token' &&
|
||||
item.key !== 'integration.paddle_ocr_job_url' &&
|
||||
item.key !== 'integration.paddle_ocr_model' &&
|
||||
item.key !== 'renter.growth_level_rules'
|
||||
item.key !== 'renter.growth_level_rules' &&
|
||||
item.key !== 'sms.business_templates'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -227,6 +237,24 @@ const renterGrowthStats = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const smsNodeStats = computed(() => {
|
||||
const config = parseSmsBusinessTemplatesConfig(smsNodeTemplatesConfig.value?.value || '')
|
||||
const configured = smsNodeMetas.filter(
|
||||
meta => config.nodes[meta.code]?.template_code && config.nodes[meta.code]?.enabled
|
||||
)
|
||||
return {
|
||||
enabled: config.enabled,
|
||||
configuredCount: configured.length,
|
||||
totalCount: smsNodeMetas.length,
|
||||
nodes: smsNodeMetas,
|
||||
statusOf: (code: string) => {
|
||||
const node = config.nodes[code]
|
||||
if (!node?.template_code) return '未配置'
|
||||
return node.enabled ? '已配置' : '已停用'
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const agreementStats = computed(() => {
|
||||
const agreements = parseOrderAgreements(orderAgreementsConfig.value?.value || '')
|
||||
return {
|
||||
@@ -295,6 +323,8 @@ function openEdit(row: SystemConfig) {
|
||||
postRentalNoticeVisible.value = true
|
||||
} else if (row.key === 'renter.growth_level_rules') {
|
||||
renterGrowthVisible.value = true
|
||||
} else if (row.key === 'sms.business_templates') {
|
||||
smsNodeTemplatesVisible.value = true
|
||||
} else if (row.key === 'chat.renter_retention_days_after_order_end') {
|
||||
generalVisible.value = true
|
||||
} else {
|
||||
@@ -592,6 +622,36 @@ function formatConfigValue(row: SystemConfig) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="smsNodeTemplatesConfig" class="publish-config-panel">
|
||||
<div class="publish-config-main">
|
||||
<div>
|
||||
<p class="eyebrow">Node SMS</p>
|
||||
<h2>节点短信</h2>
|
||||
<span
|
||||
>审核未通过、被租用需辅助登录、上架多日未成交等节点给号主发短信;只填阿里云已报备的模板
|
||||
CODE,文案由平台模板决定。分站可在「分销站点 → 短信配置」里配置自己的模板。</span
|
||||
>
|
||||
</div>
|
||||
<el-button type="primary" @click="openEdit(smsNodeTemplatesConfig)">
|
||||
编辑节点短信
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="publish-stat-grid home-stat-grid">
|
||||
<div class="publish-stat">
|
||||
<strong>{{ smsNodeStats.enabled ? '已启用' : '已停用' }}</strong>
|
||||
<span>总开关</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>{{ smsNodeStats.configuredCount }} / {{ smsNodeStats.totalCount }}</strong>
|
||||
<span>已配置节点</span>
|
||||
</div>
|
||||
<div v-for="meta in smsNodeStats.nodes" :key="meta.code" class="publish-stat">
|
||||
<strong>{{ smsNodeStats.statusOf(meta.code) }}</strong>
|
||||
<span>{{ meta.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="
|
||||
homeAnnouncementsConfig || homeBannersConfig || awRecycleConfig || cooperationFeedbackConfig
|
||||
@@ -956,6 +1016,13 @@ function formatConfigValue(row: SystemConfig) {
|
||||
@saved="loadConfigs"
|
||||
/>
|
||||
|
||||
<SmsNodeTemplatesDialog
|
||||
v-if="smsNodeTemplatesConfig"
|
||||
v-model="smsNodeTemplatesVisible"
|
||||
:config="smsNodeTemplatesConfig"
|
||||
@saved="loadConfigs"
|
||||
/>
|
||||
|
||||
<GeneralConfigDialog
|
||||
v-if="currentEditingConfig"
|
||||
v-model="generalVisible"
|
||||
|
||||
Reference in New Issue
Block a user