This commit is contained in:
@@ -23,6 +23,7 @@ export { OrderService } from './order'
|
||||
export { AssetService } from './asset'
|
||||
export { AgentRechargeService } from './agentRecharge'
|
||||
export { PaymentSettingsService } from './paymentSettings'
|
||||
export { SystemConfigService } from './systemConfig'
|
||||
export { ExchangeService } from './exchange'
|
||||
export { RefundService } from './refund'
|
||||
export { DataCleanupService } from './dataCleanup'
|
||||
|
||||
28
src/api/modules/systemConfig.ts
Normal file
28
src/api/modules/systemConfig.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { BaseService } from '../BaseService'
|
||||
import type { BaseResponse } from '@/types/api'
|
||||
import type {
|
||||
SystemConfigItem,
|
||||
SystemConfigPageResult,
|
||||
SystemConfigQueryParams,
|
||||
UpdateSystemConfigRequest
|
||||
} from '@/types/api/systemConfig'
|
||||
|
||||
const SYSTEM_CONFIG_BASE_URL = '/api/admin/system-configs'
|
||||
|
||||
export class SystemConfigService extends BaseService {
|
||||
static getSystemConfigs(
|
||||
params?: SystemConfigQueryParams
|
||||
): Promise<BaseResponse<SystemConfigPageResult>> {
|
||||
return this.get<BaseResponse<SystemConfigPageResult>>(SYSTEM_CONFIG_BASE_URL, params)
|
||||
}
|
||||
|
||||
static updateSystemConfig(
|
||||
key: string,
|
||||
data: UpdateSystemConfigRequest
|
||||
): Promise<BaseResponse<SystemConfigItem>> {
|
||||
return this.put<BaseResponse<SystemConfigItem>>(
|
||||
`${SYSTEM_CONFIG_BASE_URL}/${encodeURIComponent(key)}`,
|
||||
data
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -764,6 +764,17 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER']
|
||||
}
|
||||
},
|
||||
// 系统配置
|
||||
{
|
||||
path: 'system-configs',
|
||||
name: 'SystemConfigs',
|
||||
component: RoutesAlias.SystemConfigs,
|
||||
meta: {
|
||||
title: '系统配置',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -92,6 +92,7 @@ export enum RoutesAlias {
|
||||
PaymentSettings = '/settings/payment-settings', // 支付设置
|
||||
PaymentSettingsDetail = '/settings/payment-settings/detail', // 支付设置详情
|
||||
OperationPasswordSettings = '/settings/operation-password', // 操作密码设置
|
||||
SystemConfigs = '/settings/system-configs', // 系统配置
|
||||
|
||||
// 轮询管理
|
||||
DataCleanup = '/polling-management/data-cleanup', // 数据清理
|
||||
|
||||
@@ -78,6 +78,9 @@ export * from './agentRecharge'
|
||||
// 支付设置相关
|
||||
export * from './paymentSettings'
|
||||
|
||||
// 系统配置相关
|
||||
export * from './systemConfig'
|
||||
|
||||
// 退款管理相关
|
||||
export * from './refund'
|
||||
|
||||
|
||||
@@ -336,8 +336,8 @@ export interface CreateShopSeriesGrantRequest {
|
||||
commission_tiers?: CommissionTier[] // 梯度配置列表,梯度模式时必填
|
||||
enable_force_recharge?: boolean // 是否启用强充
|
||||
force_recharge_amount?: number // 强充金额(分)
|
||||
packages?: GrantPackageItem[] // 套餐列表
|
||||
expiry_base_override: PackageAllocationExpiryBaseOverride | null
|
||||
packages: GrantPackageItem[] // 初始套餐列表,1~100项
|
||||
expiry_base_override?: PackageAllocationExpiryBaseOverride | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,7 +354,7 @@ export interface UpdateShopSeriesGrantRequest {
|
||||
* 管理套餐请求中的套餐项
|
||||
*/
|
||||
export interface GrantPackageItem {
|
||||
package_id?: number // 套餐ID
|
||||
package_id: number // 套餐ID
|
||||
cost_price?: number // 成本价(分)
|
||||
expiry_base_override?: PackageAllocationExpiryBaseOverride | null
|
||||
remove?: boolean | null // 是否删除该套餐授权(true=删除)
|
||||
@@ -364,6 +364,6 @@ export interface GrantPackageItem {
|
||||
* 管理套餐请求
|
||||
*/
|
||||
export interface ManageGrantPackagesRequest {
|
||||
packages?: GrantPackageItem[] | null // 套餐操作列表
|
||||
expiry_base_override: PackageAllocationExpiryBaseOverride | null // 新增套餐统一生效条件
|
||||
packages: GrantPackageItem[] // 套餐操作列表,1~100项
|
||||
expiry_base_override?: PackageAllocationExpiryBaseOverride | null // 新增套餐统一生效条件
|
||||
}
|
||||
|
||||
37
src/types/api/systemConfig.ts
Normal file
37
src/types/api/systemConfig.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { PaginationParams } from './common'
|
||||
|
||||
export type SystemConfigModule = 'carrier_callback' | 'c2b.payment'
|
||||
|
||||
export type SystemConfigValueType = 'string' | 'int' | 'bool' | 'json'
|
||||
|
||||
export interface SystemConfigItem {
|
||||
config_key: string
|
||||
module: SystemConfigModule
|
||||
value: string
|
||||
value_type: SystemConfigValueType
|
||||
description: string
|
||||
control: string
|
||||
enum_values: string[]
|
||||
min: number | null
|
||||
max: number | null
|
||||
readonly: boolean
|
||||
registered: boolean
|
||||
sensitive: boolean
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
export interface SystemConfigQueryParams extends PaginationParams {
|
||||
module?: SystemConfigModule
|
||||
}
|
||||
|
||||
export interface SystemConfigPageResult {
|
||||
list: SystemConfigItem[]
|
||||
page: number
|
||||
page_size: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface UpdateSystemConfigRequest {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
44
src/utils/business/seriesGrantPackage.ts
Normal file
44
src/utils/business/seriesGrantPackage.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { GrantPackageInfo, PackageResponse } from '@/types/api'
|
||||
|
||||
export interface GrantPackageCandidate {
|
||||
id: number
|
||||
package_name?: string
|
||||
package_code?: string
|
||||
cost_price?: number
|
||||
suggested_retail_price?: number | null
|
||||
is_authorized: boolean
|
||||
authorized_package?: GrantPackageInfo
|
||||
}
|
||||
|
||||
export const mergeGrantPackageCandidates = (
|
||||
candidates: PackageResponse[],
|
||||
authorizedPackages: GrantPackageInfo[]
|
||||
): GrantPackageCandidate[] => {
|
||||
const merged = new Map<number, GrantPackageCandidate>()
|
||||
|
||||
candidates.forEach((candidate) => {
|
||||
merged.set(candidate.id, {
|
||||
id: candidate.id,
|
||||
package_name: candidate.package_name,
|
||||
package_code: candidate.package_code,
|
||||
cost_price: candidate.cost_price,
|
||||
suggested_retail_price: candidate.suggested_retail_price,
|
||||
is_authorized: false
|
||||
})
|
||||
})
|
||||
|
||||
authorizedPackages.forEach((authorizedPackage) => {
|
||||
const candidate = merged.get(authorizedPackage.package_id)
|
||||
merged.set(authorizedPackage.package_id, {
|
||||
id: authorizedPackage.package_id,
|
||||
package_name: candidate?.package_name ?? authorizedPackage.package_name,
|
||||
package_code: candidate?.package_code ?? authorizedPackage.package_code,
|
||||
cost_price: authorizedPackage.cost_price ?? candidate?.cost_price,
|
||||
suggested_retail_price: candidate?.suggested_retail_price,
|
||||
is_authorized: true,
|
||||
authorized_package: authorizedPackage
|
||||
})
|
||||
})
|
||||
|
||||
return [...merged.values()]
|
||||
}
|
||||
@@ -245,7 +245,7 @@
|
||||
:key="pkg.id"
|
||||
:label="`${pkg.package_name} (${pkg.package_code})`"
|
||||
:value="pkg.id"
|
||||
:disabled="isPackageAlreadyAdded(pkg.id)"
|
||||
:disabled="pkg.is_authorized"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
@@ -345,7 +345,6 @@
|
||||
} from '@/api/modules'
|
||||
import type {
|
||||
ShopSeriesGrantResponse,
|
||||
PackageResponse,
|
||||
GrantPackageItem,
|
||||
GrantPackageInfo,
|
||||
PackageAllocationExpiryBaseOverride,
|
||||
@@ -353,6 +352,10 @@
|
||||
} from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { getEnableStatusText } from '@/config/constants'
|
||||
import {
|
||||
mergeGrantPackageCandidates,
|
||||
type GrantPackageCandidate
|
||||
} from '@/utils/business/seriesGrantPackage'
|
||||
|
||||
defineOptions({ name: 'SeriesGrantsDetail' })
|
||||
|
||||
@@ -366,7 +369,7 @@
|
||||
const seriesData = ref<PackageSeriesResponse | null>(null)
|
||||
const packageDialogVisible = ref(false)
|
||||
const packageDialogType = ref<'add' | 'edit'>('add')
|
||||
const availablePackages = ref<PackageResponse[]>([])
|
||||
const availablePackages = ref<GrantPackageCandidate[]>([])
|
||||
const packageFormRef = ref<FormInstance>()
|
||||
|
||||
type ExpiryBaseSelection = PackageAllocationExpiryBaseOverride | 'default'
|
||||
@@ -559,7 +562,10 @@
|
||||
|
||||
const res = await PackageManageService.getPackages(params)
|
||||
if (res.code === 0) {
|
||||
availablePackages.value = res.data.items
|
||||
availablePackages.value = mergeGrantPackageCandidates(
|
||||
res.data.items,
|
||||
detailData.value.packages || []
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载套餐选项失败:', error)
|
||||
@@ -577,11 +583,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 检查套餐是否已添加
|
||||
const isPackageAlreadyAdded = (packageId: number) => {
|
||||
return detailData.value?.packages?.some((p) => p.package_id === packageId) || false
|
||||
}
|
||||
|
||||
// 保存套餐
|
||||
const handleSavePackage = async () => {
|
||||
if (!packageFormRef.value || !detailData.value) return
|
||||
@@ -600,7 +601,7 @@
|
||||
: packageForm.value.expiry_base_override,
|
||||
packages: [
|
||||
{
|
||||
package_id: packageForm.value.package_id,
|
||||
package_id: packageForm.value.package_id!,
|
||||
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
|
||||
}
|
||||
]
|
||||
@@ -616,7 +617,7 @@
|
||||
|
||||
const packages: GrantPackageItem[] = [
|
||||
{
|
||||
package_id: packageForm.value.package_id,
|
||||
package_id: packageForm.value.package_id!,
|
||||
cost_price: Math.round(packageForm.value.cost_price_yuan * 100)
|
||||
}
|
||||
]
|
||||
@@ -672,10 +673,7 @@
|
||||
}
|
||||
]
|
||||
|
||||
await ShopSeriesGrantService.manageGrantPackages(detailData.value.id, {
|
||||
expiry_base_override: row.expiry_base_override ?? null,
|
||||
packages
|
||||
})
|
||||
await ShopSeriesGrantService.manageGrantPackages(detailData.value.id, { packages })
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
// 刷新详情数据
|
||||
|
||||
@@ -113,17 +113,18 @@
|
||||
|
||||
<!-- 套餐配置 -->
|
||||
<div v-if="form.shop_id" class="form-section-title">
|
||||
<span class="title-text">套餐配置(可选)</span>
|
||||
<span class="title-text">套餐配置(必选)</span>
|
||||
</div>
|
||||
|
||||
<template v-if="form.shop_id">
|
||||
<!-- 选择套餐 -->
|
||||
<ElFormItem label="选择套餐">
|
||||
<ElFormItem label="选择套餐" prop="packages">
|
||||
<ElSelect
|
||||
v-model="selectedPackageIds"
|
||||
placeholder="请选择套餐"
|
||||
style="width: 100%"
|
||||
multiple
|
||||
:multiple-limit="100"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchPackages"
|
||||
@@ -142,7 +143,7 @@
|
||||
:value="pkg.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div class="form-tip">选择该授权下包含的套餐</div>
|
||||
<div class="form-tip">首次创建必须选择 1~100 个不重复套餐</div>
|
||||
</ElFormItem>
|
||||
|
||||
<!-- 套餐成本价 -->
|
||||
@@ -799,6 +800,24 @@
|
||||
shop_id: [{ required: true, message: '请选择店铺', trigger: 'change' }]
|
||||
}
|
||||
|
||||
baseRules.packages = [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
const packageIds = (value || []).map((pkg: { package_id: number }) => pkg.package_id)
|
||||
if (packageIds.length < 1) {
|
||||
callback(new Error('请至少选择 1 个套餐'))
|
||||
} else if (packageIds.length > 100) {
|
||||
callback(new Error('最多选择 100 个套餐'))
|
||||
} else if (new Set(packageIds).size !== packageIds.length) {
|
||||
callback(new Error('套餐不能重复'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
|
||||
// 只有启用一次性佣金时才添加佣金验证规则
|
||||
if (form.enable_one_time_commission) {
|
||||
// 根据佣金类型添加验证规则
|
||||
@@ -1092,9 +1111,9 @@
|
||||
|
||||
// 移除套餐
|
||||
const removePackage = (index: number) => {
|
||||
const removedPackageId = form.packages[index]?.package_id
|
||||
form.packages.splice(index, 1)
|
||||
// 同步更新选中的套餐ID
|
||||
const removedPackageId = form.packages[index]?.package_id
|
||||
if (removedPackageId) {
|
||||
const idIndex = selectedPackageIds.value.indexOf(removedPackageId)
|
||||
if (idIndex > -1) {
|
||||
@@ -1732,6 +1751,20 @@
|
||||
cost_price: Math.round((pkg.cost_price ?? 0) * 100)
|
||||
}))
|
||||
|
||||
if (dialogType.value === 'add') {
|
||||
const packageIds = packageItems.map((pkg: { package_id: number }) => pkg.package_id)
|
||||
if (packageIds.length < 1 || packageIds.length > 100) {
|
||||
ElMessage.error('首次创建必须选择 1~100 个套餐')
|
||||
submitLoading.value = false
|
||||
return
|
||||
}
|
||||
if (new Set(packageIds).size !== packageIds.length) {
|
||||
ElMessage.error('套餐不能重复')
|
||||
submitLoading.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (packageItems.length > 0) {
|
||||
// 验证是否包含赠送套餐
|
||||
for (const pkg of form.packages) {
|
||||
@@ -1815,8 +1848,8 @@
|
||||
let fieldsToValidate: string[] = []
|
||||
|
||||
if (currentStep.value === 0) {
|
||||
// 第一步:验证系列和店铺选择
|
||||
fieldsToValidate = ['series_id', 'shop_id']
|
||||
// 第一步:验证系列、店铺和初始套餐
|
||||
fieldsToValidate = ['series_id', 'shop_id', 'packages']
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
:key="pkg.id"
|
||||
:label="`${pkg.package_name} (${pkg.package_code})`"
|
||||
:value="pkg.id"
|
||||
:disabled="isPackageAlreadyAdded(pkg.id)"
|
||||
:disabled="pkg.is_authorized"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
@@ -193,10 +193,13 @@
|
||||
import type {
|
||||
GrantPackageInfo,
|
||||
GrantPackageItem,
|
||||
PackageAllocationExpiryBaseOverride,
|
||||
PackageResponse
|
||||
PackageAllocationExpiryBaseOverride
|
||||
} from '@/types/api'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import {
|
||||
mergeGrantPackageCandidates,
|
||||
type GrantPackageCandidate
|
||||
} from '@/utils/business/seriesGrantPackage'
|
||||
|
||||
defineOptions({ name: 'SeriesGrantPackages' })
|
||||
|
||||
@@ -213,7 +216,7 @@
|
||||
const seriesId = ref<number>(0)
|
||||
const grantName = ref<string>('')
|
||||
const packageList = ref<GrantPackageInfo[]>([])
|
||||
const availablePackages = ref<PackageResponse[]>([])
|
||||
const availablePackages = ref<GrantPackageCandidate[]>([])
|
||||
const packageFormRef = ref<FormInstance>()
|
||||
|
||||
const packageDialogVisible = ref(false)
|
||||
@@ -378,7 +381,7 @@
|
||||
}
|
||||
const res = await PackageManageService.getPackages(params)
|
||||
if (res.code === 0) {
|
||||
availablePackages.value = res.data.items
|
||||
availablePackages.value = mergeGrantPackageCandidates(res.data.items, packageList.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载套餐选项失败:', error)
|
||||
@@ -395,10 +398,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
const isPackageAlreadyAdded = (packageId: number) => {
|
||||
return packageList.value.some((p) => p.package_id === packageId)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => packageForm.value.package_id,
|
||||
(packageId) => {
|
||||
@@ -440,7 +439,7 @@
|
||||
packageForm.value.expiry_base_override === 'default'
|
||||
? null
|
||||
: packageForm.value.expiry_base_override,
|
||||
packages: [{ package_id: packageForm.value.package_id, cost_price: costPrice }]
|
||||
packages: [{ package_id: packageForm.value.package_id!, cost_price: costPrice }]
|
||||
})
|
||||
} else {
|
||||
const expiryBaseChanged =
|
||||
@@ -452,7 +451,7 @@
|
||||
}
|
||||
|
||||
const packages: GrantPackageItem[] = [
|
||||
{ package_id: packageForm.value.package_id, cost_price: costPrice }
|
||||
{ package_id: packageForm.value.package_id!, cost_price: costPrice }
|
||||
]
|
||||
if (expiryBaseChanged) {
|
||||
await ShopPackageAllocationService.updateShopPackageAllocationExpiryBase(
|
||||
@@ -499,10 +498,7 @@
|
||||
remove: true
|
||||
}
|
||||
]
|
||||
await ShopSeriesGrantService.manageGrantPackages(grantId.value, {
|
||||
expiry_base_override: row.expiry_base_override ?? null,
|
||||
packages
|
||||
})
|
||||
await ShopSeriesGrantService.manageGrantPackages(grantId.value, { packages })
|
||||
await fetchPackageList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
351
src/views/settings/system-configs/index.vue
Normal file
351
src/views/settings/system-configs/index.vue
Normal file
@@ -0,0 +1,351 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="system-configs-page" id="table-full-screen">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
label-width="90px"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="loadConfigs"
|
||||
/>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="config_key"
|
||||
:loading="loading"
|
||||
:data="configList"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="100"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
title="编辑系统配置"
|
||||
width="560px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ElForm label-width="120px">
|
||||
<ElFormItem label="配置 Key">
|
||||
<ElInput :model-value="currentConfig?.config_key || '-'" disabled />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="配置说明">
|
||||
<span>{{ currentConfig?.description || '-' }}</span>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="配置值">
|
||||
<ElSwitch v-if="isBooleanConfig" v-model="editBoolean" />
|
||||
<ElInputNumber
|
||||
v-else-if="isIntegerConfig"
|
||||
v-model="editNumber"
|
||||
:min="currentConfig?.min ?? undefined"
|
||||
:max="currentConfig?.max ?? undefined"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<ElSelect v-else-if="isEnumConfig" v-model="editValue" style="width: 100%">
|
||||
<ElOption
|
||||
v-for="item in currentConfig?.enum_values"
|
||||
:key="item"
|
||||
:value="item"
|
||||
:label="item"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElInput
|
||||
v-else-if="isJsonConfig"
|
||||
v-model="editValue"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入 JSON 配置"
|
||||
/>
|
||||
<ElInput
|
||||
v-else
|
||||
v-model="editValue"
|
||||
:type="currentConfig?.sensitive ? 'password' : 'text'"
|
||||
:show-password="Boolean(currentConfig?.sensitive)"
|
||||
placeholder="请输入配置值"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<div v-if="valueHint" class="config-hint">{{ valueHint }}</div>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="submitLoading" @click="handleSubmit"> 保存 </ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import { SystemConfigService } from '@/api/modules'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type { SystemConfigItem, SystemConfigModule } from '@/types/api/systemConfig'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
|
||||
defineOptions({ name: 'SystemConfigs' })
|
||||
|
||||
const moduleOptions = [
|
||||
{ label: '运营商回调配置', value: 'carrier_callback' },
|
||||
{ label: 'C 端支付方式配置', value: 'c2b.payment' }
|
||||
]
|
||||
|
||||
const searchForm = reactive<{ module?: SystemConfigModule }>({ module: undefined })
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '配置模块',
|
||||
prop: 'module',
|
||||
type: 'select',
|
||||
options: moduleOptions,
|
||||
config: { clearable: true, placeholder: '请选择配置模块' }
|
||||
}
|
||||
]
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const tableRef = ref()
|
||||
const currentConfig = ref<SystemConfigItem | null>(null)
|
||||
const configList = ref<SystemConfigItem[]>([])
|
||||
const editValue = ref('')
|
||||
const originalValue = ref('')
|
||||
const editBoolean = ref(false)
|
||||
const editNumber = ref<number | null>(null)
|
||||
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '配置 Key', prop: 'config_key' },
|
||||
{ label: '模块', prop: 'module' },
|
||||
{ label: '配置说明', prop: 'description' },
|
||||
{ label: '配置值', prop: 'value' },
|
||||
{ label: '控件', prop: 'control' },
|
||||
{ label: '注册状态', prop: 'registered' },
|
||||
{ label: '只读', prop: 'readonly' },
|
||||
{ label: '敏感', prop: 'sensitive' },
|
||||
{ label: '更新时间', prop: 'updated_at' }
|
||||
]
|
||||
|
||||
const moduleLabel = (module: SystemConfigModule) =>
|
||||
moduleOptions.find((item) => item.value === module)?.label || module
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{ prop: 'config_key', label: '配置 Key', minWidth: 220, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'module',
|
||||
label: '模块',
|
||||
minWidth: 150,
|
||||
formatter: (row: SystemConfigItem) => moduleLabel(row.module)
|
||||
},
|
||||
{ prop: 'description', label: '配置说明', minWidth: 180, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'value',
|
||||
label: '配置值',
|
||||
minWidth: 150,
|
||||
formatter: (row: SystemConfigItem) => row.value || '-'
|
||||
},
|
||||
{ prop: 'control', label: '控件', width: 100 },
|
||||
{
|
||||
prop: 'registered',
|
||||
label: '注册状态',
|
||||
width: 100,
|
||||
formatter: (row: SystemConfigItem) =>
|
||||
h(ElTag, { type: row.registered ? 'success' : 'info' }, () =>
|
||||
row.registered ? '已注册' : '未注册'
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'readonly',
|
||||
label: '只读',
|
||||
width: 80,
|
||||
formatter: (row: SystemConfigItem) => (row.readonly ? '是' : '否')
|
||||
},
|
||||
{
|
||||
prop: 'sensitive',
|
||||
label: '敏感',
|
||||
width: 80,
|
||||
formatter: (row: SystemConfigItem) => (row.sensitive ? '是' : '否')
|
||||
},
|
||||
{
|
||||
prop: 'updated_at',
|
||||
label: '更新时间',
|
||||
width: 180,
|
||||
formatter: (row: SystemConfigItem) => formatDateTime(row.updated_at)
|
||||
}
|
||||
])
|
||||
|
||||
const isBooleanConfig = computed(
|
||||
() => currentConfig.value?.value_type === 'bool' || currentConfig.value?.control === 'switch'
|
||||
)
|
||||
const isIntegerConfig = computed(
|
||||
() => !isBooleanConfig.value && currentConfig.value?.value_type === 'int'
|
||||
)
|
||||
const isEnumConfig = computed(
|
||||
() =>
|
||||
!isBooleanConfig.value &&
|
||||
!isIntegerConfig.value &&
|
||||
Boolean(currentConfig.value?.enum_values?.length)
|
||||
)
|
||||
const isJsonConfig = computed(
|
||||
() =>
|
||||
!isBooleanConfig.value && !isIntegerConfig.value && currentConfig.value?.value_type === 'json'
|
||||
)
|
||||
|
||||
const valueHint = computed(() => {
|
||||
if (!currentConfig.value) return ''
|
||||
const { min, max } = currentConfig.value
|
||||
if (min !== null && max !== null) return `取值范围:${min}~${max}`
|
||||
if (min !== null) return `最小值:${min}`
|
||||
if (max !== null) return `最大值:${max}`
|
||||
return ''
|
||||
})
|
||||
|
||||
const getSubmittedValue = () => {
|
||||
if (isBooleanConfig.value) return String(editBoolean.value)
|
||||
if (isIntegerConfig.value) return editNumber.value === null ? '' : String(editNumber.value)
|
||||
return editValue.value
|
||||
}
|
||||
|
||||
const validateValue = (value: string) => {
|
||||
const config = currentConfig.value
|
||||
if (!config) return '配置不存在'
|
||||
if (config.enum_values.length > 0 && !config.enum_values.includes(value)) {
|
||||
return '配置值不在允许的枚举范围内'
|
||||
}
|
||||
if (config.value_type === 'int') {
|
||||
if (!/^-?\d+$/.test(value)) return '请输入整数'
|
||||
const numberValue = Number(value)
|
||||
if (config.min !== null && numberValue < config.min) return `配置值不能小于 ${config.min}`
|
||||
if (config.max !== null && numberValue > config.max) return `配置值不能大于 ${config.max}`
|
||||
}
|
||||
if (config.value_type === 'json') {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
} catch {
|
||||
return '请输入有效的 JSON'
|
||||
}
|
||||
}
|
||||
if (value === '') return '配置值不能为空'
|
||||
return ''
|
||||
}
|
||||
|
||||
const getActions = (row: SystemConfigItem) =>
|
||||
row.readonly
|
||||
? []
|
||||
: [{ label: '编辑', handler: () => showEditDialog(row), type: 'primary' as const }]
|
||||
|
||||
const loadConfigs = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await SystemConfigService.getSystemConfigs({
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
module: searchForm.module
|
||||
})
|
||||
if (res.code === 0) {
|
||||
configList.value = res.data.list || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取系统配置失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const showEditDialog = (row: SystemConfigItem) => {
|
||||
currentConfig.value = row
|
||||
originalValue.value = row.value
|
||||
editValue.value = row.value
|
||||
editBoolean.value = row.value === 'true'
|
||||
editNumber.value = /^-?\d+$/.test(row.value) ? Number(row.value) : null
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!currentConfig.value || currentConfig.value.readonly) return
|
||||
const value = getSubmittedValue()
|
||||
if (currentConfig.value.sensitive && value === originalValue.value) {
|
||||
ElMessage.warning('敏感配置未输入新值,无需保存')
|
||||
return
|
||||
}
|
||||
const errorMessage = validateValue(value)
|
||||
if (errorMessage) {
|
||||
ElMessage.warning(errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const res = await SystemConfigService.updateSystemConfig(currentConfig.value.config_key, {
|
||||
key: currentConfig.value.config_key,
|
||||
value
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('系统配置更新成功')
|
||||
dialogVisible.value = false
|
||||
await loadConfigs()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '系统配置更新失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新系统配置失败:', error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.module = undefined
|
||||
pagination.page = 1
|
||||
loadConfigs()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadConfigs()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.page_size = size
|
||||
loadConfigs()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
loadConfigs()
|
||||
}
|
||||
|
||||
onMounted(loadConfigs)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.system-configs-page {
|
||||
.config-hint {
|
||||
margin: -12px 0 0 120px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user