增加修改所有者模板资料的api端点及所有者模板列表页

This commit is contained in:
jingrow 2025-08-06 00:06:15 +08:00
parent 4051577bcd
commit 3279396294
3 changed files with 737 additions and 0 deletions

View File

@ -0,0 +1,547 @@
<template>
<div class="space-y-4">
<!-- 页面头部 -->
<div class="flex items-center justify-between">
<div>
<h2 class="text-lg font-medium text-gray-900">域名所有者模板</h2>
<p class="text-sm text-gray-600">管理域名注册使用的所有者信息模板</p>
</div>
<Button
@click="createDomainOwner"
variant="solid"
>
新建模板
</Button>
</div>
<!-- 搜索和筛选 -->
<div class="flex flex-col sm:flex-row gap-4">
<div class="flex-1">
<TextInput
v-model="searchQuery"
placeholder="搜索所有者姓名、单位名称..."
class="w-full"
>
<template #prefix>
<SearchIcon class="h-4 w-4 text-gray-400" />
</template>
</TextInput>
</div>
<div class="flex gap-2">
<select
v-model="selectedType"
class="rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
>
<option value="">全部类型</option>
<option value="I">个人</option>
<option value="E">企业/组织</option>
</select>
<select
v-model="selectedStatus"
class="rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
>
<option value="">全部状态</option>
<option value="1">已实名认证</option>
<option value="0">未实名认证</option>
</select>
</div>
</div>
<!-- 列表内容 -->
<div class="rounded-lg border border-gray-200 bg-white">
<!-- 表头 -->
<div class="grid grid-cols-12 gap-4 border-b border-gray-200 bg-gray-50 px-6 py-3 text-sm font-medium text-gray-700">
<div class="col-span-3">名称</div>
<div class="col-span-2">类型</div>
<div class="col-span-2">实名状态</div>
<div class="col-span-3">联系信息</div>
<div class="col-span-2">操作</div>
</div>
<!-- 加载状态 -->
<div v-if="isLoading" class="flex items-center justify-center py-12">
<Loader2Icon class="h-6 w-6 animate-spin text-gray-400" />
<span class="ml-2 text-gray-600">加载中...</span>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="flex items-center justify-center py-12">
<AlertCircleIcon class="h-6 w-6 text-red-500" />
<span class="ml-2 text-red-600">{{ error }}</span>
</div>
<!-- 空状态 -->
<div v-else-if="filteredOwners.length === 0" class="flex flex-col items-center justify-center py-12 text-gray-500">
<UsersIcon class="mb-4 h-12 w-12 text-gray-300" />
<p class="text-lg font-medium">暂无域名所有者模板</p>
<p class="text-sm">点击上方按钮创建第一个模板</p>
</div>
<!-- 数据列表 -->
<div v-else class="divide-y divide-gray-200">
<div
v-for="owner in filteredOwners"
:key="owner.name"
class="grid grid-cols-12 gap-4 px-6 py-4 hover:bg-gray-50 transition-colors cursor-pointer"
@click="viewOwner(owner)"
>
<!-- 名称列 -->
<div class="col-span-3">
<div class="font-medium text-gray-900">{{ getDisplayName(owner) }}</div>
<div class="text-sm text-gray-500">{{ owner.title || owner.name }}</div>
</div>
<!-- 类型列 -->
<div class="col-span-2 flex items-center">
<Badge :variant="owner.c_regtype === 'I' ? 'blue' : 'purple'">
{{ owner.c_regtype === 'I' ? '个人' : '企业/组织' }}
</Badge>
</div>
<!-- 实名状态列 -->
<div class="col-span-2 flex items-center">
<Badge :variant="getRealNameStatusVariant(owner.r_status)">
{{ getRealNameStatusText(owner.r_status) }}
</Badge>
</div>
<!-- 联系信息列 -->
<div class="col-span-3">
<div class="text-sm text-gray-900">{{ owner.c_em || '-' }}</div>
<div class="text-sm text-gray-500">{{ owner.c_ph || '-' }}</div>
</div>
<!-- 操作列 -->
<div class="col-span-2 flex items-center gap-2" @click.stop>
<Button
@click="viewOwner(owner)"
variant="ghost"
size="sm"
class="text-blue-600 hover:text-blue-700"
title="查看详情"
>
<EyeIcon class="h-4 w-4" />
</Button>
<Button
@click="editOwner(owner)"
variant="ghost"
size="sm"
class="text-gray-600 hover:text-gray-700"
title="编辑"
>
<EditIcon class="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div v-if="pagination.total > pagination.limit" class="flex items-center justify-between">
<div class="text-sm text-gray-700">
显示 {{ (pagination.pageno - 1) * pagination.limit + 1 }}
{{ Math.min(pagination.pageno * pagination.limit, pagination.total) }}
{{ pagination.total }} 条记录
</div>
<div class="flex gap-2">
<Button
@click="previousPage"
:disabled="pagination.pageno <= 1"
variant="outline"
size="sm"
>
上一页
</Button>
<Button
@click="nextPage"
:disabled="pagination.pageno >= pagination.pagecount"
variant="outline"
size="sm"
>
下一页
</Button>
</div>
</div>
<!-- 创建域名所有者弹窗 -->
<DomainOwnerDialog
:visible="showCreateDialog"
:isLoading="createLoading"
@close="handleCloseDialog"
@submit="handleCreateSubmit"
/>
<!-- 详情查看弹窗 -->
<div v-if="showDetailDialog" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-2xl max-w-2xl w-full mx-4 max-h-[90vh] flex flex-col overflow-hidden">
<!-- 标题栏 -->
<div class="p-4 border-b border-gray-200 bg-white rounded-t-lg flex-shrink-0">
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900">域名所有者详情</h3>
<button
@click="closeDetailDialog"
class="text-gray-400 hover:text-gray-600 transition-colors duration-200"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<!-- 内容区域 -->
<div class="p-6 overflow-y-auto flex-1" v-if="selectedOwner">
<div class="space-y-6">
<!-- 基本信息 -->
<div>
<h4 class="text-base font-medium text-gray-900 mb-4">基本信息</h4>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">所有者类型</label>
<p class="mt-1 text-sm text-gray-900">
<Badge :variant="selectedOwner.c_regtype === 'I' ? 'blue' : 'purple'">
{{ selectedOwner.c_regtype === 'I' ? '个人' : '企业/组织' }}
</Badge>
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">实名认证状态</label>
<p class="mt-1 text-sm text-gray-900">
<Badge :variant="getRealNameStatusVariant(selectedOwner.r_status)">
{{ getRealNameStatusText(selectedOwner.r_status) }}
</Badge>
</p>
</div>
<div v-if="selectedOwner.c_regtype === 'E'">
<label class="block text-sm font-medium text-gray-700">单位名称</label>
<p class="mt-1 text-sm text-gray-900">{{ selectedOwner.c_org_m || '-' }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">姓名</label>
<p class="mt-1 text-sm text-gray-900">{{ getDisplayName(selectedOwner) }}</p>
</div>
</div>
</div>
<!-- 联系信息 -->
<div>
<h4 class="text-base font-medium text-gray-900 mb-4">联系信息</h4>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">电子邮箱</label>
<p class="mt-1 text-sm text-gray-900">{{ selectedOwner.c_em || '-' }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">手机号码</label>
<p class="mt-1 text-sm text-gray-900">{{ selectedOwner.c_ph || '-' }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">省份</label>
<p class="mt-1 text-sm text-gray-900">{{ selectedOwner.c_st_m || '-' }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">城市</label>
<p class="mt-1 text-sm text-gray-900">{{ selectedOwner.c_ct_m || '-' }}</p>
</div>
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700">通讯地址</label>
<p class="mt-1 text-sm text-gray-900">{{ selectedOwner.c_adr_m || '-' }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">邮编</label>
<p class="mt-1 text-sm text-gray-900">{{ selectedOwner.c_pc || '-' }}</p>
</div>
</div>
</div>
<!-- 证件信息 -->
<div>
<h4 class="text-base font-medium text-gray-900 mb-4">证件信息</h4>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">证件类型</label>
<p class="mt-1 text-sm text-gray-900">{{ getCertificateTypeName(selectedOwner.c_idtype_gswl) }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">证件号码</label>
<p class="mt-1 text-sm text-gray-900">{{ selectedOwner.c_idnum_gswl || '-' }}</p>
</div>
</div>
</div>
</div>
</div>
<!-- 底部按钮 -->
<div class="p-4 border-t border-gray-200 bg-white rounded-b-lg flex-shrink-0">
<div class="flex justify-end gap-3">
<Button
@click="closeDetailDialog"
variant="outline"
>
关闭
</Button>
<Button
@click="editOwner(selectedOwner)"
variant="solid"
>
编辑
</Button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Badge, Button, TextInput, createResource } from 'jingrow-ui';
import { toast } from 'vue-sonner';
import { getToastErrorMessage } from '../utils/toast';
import { confirmDialog } from '../utils/components';
import DomainOwnerDialog from './DomainOwnerDialog.vue';
import SearchIcon from '~icons/lucide/search';
import EyeIcon from '~icons/lucide/eye';
import EditIcon from '~icons/lucide/edit';
import Loader2Icon from '~icons/lucide/loader-2';
import AlertCircleIcon from '~icons/lucide/alert-circle';
import UsersIcon from '~icons/lucide/users';
export default {
name: 'DomainOwner',
components: {
Badge,
Button,
TextInput,
SearchIcon,
EyeIcon,
EditIcon,
Loader2Icon,
AlertCircleIcon,
UsersIcon,
DomainOwnerDialog,
},
data() {
return {
owners: [],
isLoading: false,
error: null,
searchQuery: '',
selectedType: '',
selectedStatus: '',
pagination: {
pageno: 1,
limit: 20,
total: 0,
pagecount: 0
},
showCreateDialog: false,
createLoading: false,
showDetailDialog: false,
selectedOwner: null
};
},
computed: {
filteredOwners() {
let filtered = [...this.owners];
//
if (this.searchQuery) {
const query = this.searchQuery.toLowerCase();
filtered = filtered.filter(owner => {
const displayName = this.getDisplayName(owner).toLowerCase();
const title = (owner.title || '').toLowerCase();
const email = (owner.c_em || '').toLowerCase();
return displayName.includes(query) || title.includes(query) || email.includes(query);
});
}
//
if (this.selectedType) {
filtered = filtered.filter(owner => owner.c_regtype === this.selectedType);
}
//
if (this.selectedStatus) {
filtered = filtered.filter(owner => String(owner.r_status) === this.selectedStatus);
}
return filtered;
}
},
methods: {
//
getDisplayName(owner) {
if (owner.c_regtype === 'I') {
// fullname +
if (owner.fullname) {
return owner.fullname;
}
const lastName = owner.c_ln_m || '';
const firstName = owner.c_fn_m || '';
return `${lastName}${firstName}`.trim() || owner.title || owner.name;
} else if (owner.c_regtype === 'E') {
// c_org_m title
return owner.c_org_m || owner.title || owner.name;
}
return owner.title || owner.name;
},
//
getRealNameStatusText(status) {
const statusMap = {
'1': '已认证',
'0': '未认证',
'2': '认证中',
'3': '认证失败',
'4': '认证过期',
'5': '待审核'
};
return statusMap[String(status)] || '未知';
},
//
getRealNameStatusVariant(status) {
const variantMap = {
'1': 'success', // - 绿
'0': 'warning', // -
'2': 'blue', // -
'3': 'danger', // -
'4': 'danger', // -
'5': 'blue' // -
};
return variantMap[String(status)] || 'default';
},
//
async fetchOwners() {
this.isLoading = true;
this.error = null;
try {
const ownersRequest = createResource({
url: 'jcloud.api.domain_west.get_domain_owners',
onSuccess: (response) => {
if (response.status === 'Success') {
this.owners = response.data || [];
this.pagination.total = this.owners.length;
this.pagination.pagecount = Math.ceil(this.pagination.total / this.pagination.limit);
} else {
this.error = response.message || '获取域名所有者列表失败';
}
this.isLoading = false;
},
onError: (error) => {
this.error = getToastErrorMessage(error);
this.isLoading = false;
}
});
ownersRequest.submit();
} catch (error) {
this.error = '获取域名所有者列表失败';
this.isLoading = false;
}
},
//
createDomainOwner() {
this.showCreateDialog = true;
},
//
handleCloseDialog() {
this.showCreateDialog = false;
this.createLoading = false;
},
//
async handleCreateSubmit(data) {
this.createLoading = true;
try {
const createRequest = createResource({
url: 'jcloud.api.domain_west.create_domain_owner',
params: data,
onSuccess: (response) => {
if (response.status === 'Success') {
toast.success('域名所有者模板创建成功!');
this.fetchOwners(); //
this.handleCloseDialog(); //
} else {
toast.error(response.message || '创建失败');
this.createLoading = false;
}
},
onError: (error) => {
toast.error(getToastErrorMessage(error));
this.createLoading = false;
}
});
createRequest.submit();
} catch (error) {
toast.error('创建失败,请稍后重试');
this.createLoading = false;
}
},
//
viewOwner(owner) {
this.selectedOwner = owner;
this.showDetailDialog = true;
},
//
closeDetailDialog() {
this.showDetailDialog = false;
this.selectedOwner = null;
},
//
editOwner(owner) {
// TODO:
console.log('编辑所有者:', owner);
//
if (this.showDetailDialog) {
this.closeDetailDialog();
}
},
//
getCertificateTypeName(type) {
const typeMap = {
'SFZ': '身份证',
'HZ': '护照',
'YYZZ': '营业执照',
'ORG': '组织机构代码证'
};
return typeMap[type] || type || '-';
},
//
previousPage() {
if (this.pagination.pageno > 1) {
this.pagination.pageno--;
}
},
//
nextPage() {
if (this.pagination.pageno < this.pagination.pagecount) {
this.pagination.pageno++;
}
},
//
refresh() {
this.fetchOwners();
}
},
mounted() {
//
this.fetchOwners();
}
};
</script>

View File

@ -180,6 +180,15 @@ export default {
props: domain => {
return { domain: domain.pg?.name };
}
},
{
label: '所有者模板',
route: 'owners',
type: 'Component',
component: defineAsyncComponent(() => import('../components/DomainOwner.vue')),
props: domain => {
return { domain: domain.pg?.name };
}
}
],
fields: [

View File

@ -484,6 +484,23 @@ class WestDomain:
return self._make_request('/audit/', 'POST', body_params=encoded_data)
def modify_contact_template(self, template_data: Dict[str, Any]) -> Dict[str, Any]:
"""
修改实名模板资料
Args:
template_data: 模板数据必须包含c_sysid字段
"""
if not template_data.get('c_sysid'):
return {"status": "error", "message": "缺少必要参数c_sysid"}
body_params = {
'act': 'auditmod',
**template_data
}
return self._make_request('/audit/', 'POST', body_params=body_params)
def get_domain_real_info(self, domain: str) -> Dict[str, Any]:
"""
获取域名实名信息
@ -3024,3 +3041,167 @@ def upload_domain_real_name_files(**data):
return {"status": "error", "message": "上传实名资料失败,请重试"}
@jingrow.whitelist()
def modify_west_contact_template(**data):
"""
修改西部数码实名模板资料
必填参数
- c_sysid: 模板标识
可选参数
- c_ln_m: (中文)联系人姓
- c_fn_m: (中文)联系人名
- c_co: 所属国家简称(CN: 中国, US: 美国)
- cocode: 国家电话代码(+86: 中国, +1: 美国)
- c_st_m: (中文)所属省
- c_ct_m: (中文)所属市
- c_dt_m: (中文)所属县
- c_adr_m: (中文)通讯地址
- c_pc: 邮编
- c_ph_type: 联系电话类型(0:手机, 1:座机)
- c_ph: 手机号码(当c_ph_type为0时必填)
- c_ph_code: 座机号码区号(当c_ph_type为1时必填)
- c_ph_num: 座机号码(当c_ph_type为1时必填)
- c_ph_fj: 座机号码分机号(可选)
- c_em: 所有者电子邮箱
- c_ln: (英文)联系人姓
- c_fn: (英文)联系人名
- c_st: (英文)省份
- c_ct: (英文)城市
- c_adr: (英文)通讯地址
- reg_contact_type: 适用范围(hk,gswl等)
- c_idtype_hk: hk域名证件类型
- c_idnum_hk: hk域名证件号码
- c_idtype_gswl: 特殊中文域名证件类型
- c_idnum_gswl: 特殊中文域名证件号码
"""
client = get_west_client()
if not client:
return {"status": "error", "message": "API客户端初始化失败"}
# 验证必填字段
c_sysid = data.get('c_sysid')
if not c_sysid:
return {"status": "error", "message": "缺少必要参数c_sysid"}
# 验证电话号码字段根据c_ph_type判断
c_ph_type = data.get('c_ph_type', '0') # 默认为手机
if c_ph_type == '0' and data.get('c_ph'): # 手机
# 如果提供了手机号码,验证格式
pass
elif c_ph_type == '1' and (data.get('c_ph_code') or data.get('c_ph_num')): # 座机
# 如果提供了座机信息,验证完整性
if data.get('c_ph_code') and not data.get('c_ph_num'):
return {"status": "error", "message": "座机号码 c_ph_num 是必填项"}
if data.get('c_ph_num') and not data.get('c_ph_code'):
return {"status": "error", "message": "座机区号 c_ph_code 是必填项"}
# 验证字段长度
if data.get('c_ln_m'):
if len(data['c_ln_m']) < 1 or len(data['c_ln_m']) > 16:
return {"status": "error", "message": "姓(中文)长度必须为116位"}
if data.get('c_fn_m'):
if len(data['c_fn_m']) < 1 or len(data['c_fn_m']) > 64:
return {"status": "error", "message": "名(中文)长度必须为164位"}
if data.get('c_st_m'):
if len(data['c_st_m']) < 2 or len(data['c_st_m']) > 10:
return {"status": "error", "message": "省份(中文)长度必须为210位"}
# 验证完整姓名长度
if data.get('c_ln_m') and data.get('c_fn_m'):
fullname = data['c_ln_m'] + data['c_fn_m']
if len(fullname) < 2 or len(fullname) > 64:
return {"status": "error", "message": "完整姓名(中文)长度必须为264位"}
# 生成英文姓名(如果提供了中文姓名)
if data.get('c_ln_m') and data.get('c_fn_m'):
try:
from pypinyin import lazy_pinyin
last_name_pinyin = ' '.join(lazy_pinyin(data['c_ln_m']))
first_name_pinyin = ' '.join(lazy_pinyin(data['c_fn_m']))
data['c_ln'] = last_name_pinyin.title()
data['c_fn'] = first_name_pinyin.title()
except ImportError:
# 如果没有pypinyin库使用默认值
data['c_ln'] = data.get('c_ln', '')
data['c_fn'] = data.get('c_fn', '')
# 生成英文地址(如果提供了中文地址)
if data.get('c_adr_m'):
try:
from pypinyin import lazy_pinyin
address_pinyin = ' '.join(lazy_pinyin(data['c_adr_m']))
data['c_adr'] = address_pinyin.title()
except ImportError:
data['c_adr'] = data.get('c_adr', '')
# 生成英文省份和城市(如果提供了中文省份和城市)
if data.get('c_st_m'):
try:
from pypinyin import lazy_pinyin
state_pinyin = ' '.join(lazy_pinyin(data['c_st_m']))
data['c_st'] = state_pinyin.title()
except ImportError:
data['c_st'] = data.get('c_st', '')
if data.get('c_ct_m'):
try:
from pypinyin import lazy_pinyin
city_pinyin = ' '.join(lazy_pinyin(data['c_ct_m']))
data['c_ct'] = city_pinyin.title()
except ImportError:
data['c_ct'] = data.get('c_ct', '')
# 设置默认值
template_data = {
'c_sysid': c_sysid,
'c_co': data.get('c_co', 'CN'),
'cocode': data.get('cocode', '+86'),
'c_ph_type': data.get('c_ph_type', '0'),
}
# 添加可选字段(只添加有值的字段)
optional_fields = [
'c_ln_m', 'c_fn_m', 'c_st_m', 'c_ct_m', 'c_dt_m', 'c_adr_m', 'c_pc',
'c_em', 'c_ln', 'c_fn', 'c_st', 'c_ct', 'c_adr', 'reg_contact_type',
'c_idtype_hk', 'c_idnum_hk', 'c_idtype_gswl', 'c_idnum_gswl'
]
for field in optional_fields:
if data.get(field):
template_data[field] = data[field]
# 根据c_ph_type添加电话相关字段
if data.get('c_ph_type') == '1': # 座机
if data.get('c_ph_code'):
template_data['c_ph_code'] = data['c_ph_code']
if data.get('c_ph_num'):
template_data['c_ph_num'] = data['c_ph_num']
if data.get('c_ph_fj'):
template_data['c_ph_fj'] = data['c_ph_fj']
else: # 手机
if data.get('c_ph'):
template_data['c_ph'] = data['c_ph']
try:
result = client.modify_contact_template(template_data)
if result.get('result') == 200:
return {
"status": "success",
"message": "实名模板资料修改成功",
"data": {
"c_sysid": result.get('data', {}).get('c_sysid', c_sysid)
}
}
else:
error_msg = result.get('msg', result.get('message', '未知错误'))
return {"status": "error", "message": f"修改实名模板资料失败: {error_msg}"}
except Exception as e:
return {"status": "error", "message": f"修改实名模板资料失败: {str(e)}"}