【初始化】前端工程项目

This commit is contained in:
chudong
2025-05-09 15:11:21 +08:00
parent c012704c9a
commit d7c556c3b0
524 changed files with 55595 additions and 112 deletions

View File

@@ -0,0 +1,20 @@
import { useApiFormController } from '../useController'
import type { AccessItem } from '@/types/access'
export default defineComponent({
name: 'AddApiForm',
props: {
data: {
type: Object as PropType<AccessItem>,
default: () => {},
},
},
setup(props) {
const { ApiManageForm } = useApiFormController(props)
return () => (
<div class="p-4">
<ApiManageForm labelPlacement="top" requireMarkPlacement="right-hanging" />
</div>
)
},
})

View File

@@ -0,0 +1,74 @@
import { NInput, NButton } from 'naive-ui'
import { PlusOutlined } from '@vicons/antd'
import { Search } from '@vicons/carbon'
import { $t } from '@locales/index'
import { useThemeCssVar } from '@baota/naive-ui/theme'
import { useController } from './useController'
import BaseComponent from '@components/baseComponent'
/**
* 授权API管理页面组件
*/
export default defineComponent({
name: 'AuthApiManage',
setup() {
const { ApiTable, ApiTablePage, param, fetch, data, openAddForm } = useController()
const cssVar = useThemeCssVar(['contentPadding', 'borderColor', 'headerHeight', 'iconColorHover'])
return () => (
<div class="h-full flex flex-col" style={cssVar.value}>
<div class="mx-auto max-w-[1600px] w-full p-6">
<BaseComponent
v-slots={{
headerLeft: () => (
<NButton type="primary" size="large" class="px-5" onClick={openAddForm}>
<PlusOutlined class="text-[var(--text-color-3)] w-[1.6rem]" />
<span class="px-2">{$t('t_0_1745289355714')}</span>
</NButton>
),
headerRight: () => (
<NInput
v-model:value={param.value.search}
onKeydown={(e: KeyboardEvent) => {
if (e.key === 'Enter') fetch()
}}
onClear={() => useTimeoutFn(() => fetch(), 100)}
placeholder={$t('t_0_1745289808449')}
clearable
size="large"
class="min-w-[300px]"
v-slots={{
suffix: () => (
<div class="flex items-center" onClick={fetch}>
<Search class="text-[var(--text-color-3)] w-[1.6rem] cursor-pointer font-bold" />
</div>
),
}}
></NInput>
),
content: () => (
<div class="rounded-lg bg-white">
<ApiTable size="medium" />
</div>
),
footerRight: () => (
<div class="mt-4 flex justify-end">
<ApiTablePage
v-slots={{
prefix: () => (
<span>
{$t('t_15_1745227839354')} {data.value.total} {$t('t_16_1745227838930')}
</span>
),
}}
/>
</div>
),
}}
></BaseComponent>
</div>
</div>
)
},
})

View File

@@ -0,0 +1,518 @@
import {
FormInst,
FormItemRule,
FormRules,
NButton,
NFlex,
NFormItem,
NFormItemGi,
NGrid,
NInput,
NSelect,
NSpace,
NTag,
NText,
type DataTableColumns,
} from 'naive-ui'
import {
useModal,
useDialog,
useTable,
useTablePage,
useModalHooks,
useFormHooks,
useForm,
useLoadingMask,
} from '@baota/naive-ui/hooks'
import { useError } from '@baota/hooks/error'
import { isIp, isPort, isUrl } from '@baota/utils/business'
import { $t } from '@locales/index'
import { useStore } from './useStore'
import type { AccessItem, AccessListParams, AddAccessParams, SshAccessConfig, UpdateAccessParams } from '@/types/access'
import type { FormConfig } from '@baota/naive-ui/types/form'
import ApiManageForm from './components/apiManageForm'
import SvgIcon from '@components/svgIcon'
import TypeIcon from '@components/typeIcon'
// 状态和方法
const { accessTypes, apiFormProps, fetchAccessList, deleteExistingAccess, addNewAccess, updateExistingAccess } =
useStore()
// 消息和对话框
const { handleError } = useError()
/**
* @description 授权API管理业务逻辑控制器
* @returns {Object} 返回授权API相关的状态数据和处理方法
*/
export const useController = () => {
// 表格列配置
const accessTypeMap = {
dns: $t('t_3_1745735765112'),
host: $t('t_0_1746754500246'),
}
/**
* @description 创建表格列配置
* @returns {DataTableColumns<AccessItem>} 返回表格列配置数组
*/
const createColumns = (): DataTableColumns<AccessItem> => [
{
title: $t('t_2_1745289353944'),
key: 'name',
width: 200,
ellipsis: {
tooltip: true,
},
},
{
title: $t('t_1_1746754499371'),
key: 'type',
width: 180,
render: (row) => <TypeIcon icon={row.type} type="success" />,
},
{
title: $t('t_2_1746754500270'),
key: 'type',
width: 180,
render: (row) => (
<NSpace>
{row.access_type.map((type) => {
return (
<NTag type="default" size="small">
{accessTypeMap[type]}
</NTag>
)
})}
</NSpace>
),
},
{
title: $t('t_7_1745215914189'),
key: 'create_time',
width: 180,
},
{
title: $t('t_0_1745295228865'),
key: 'update_time',
width: 180,
},
{
title: $t('t_8_1745215914610'),
key: 'actions',
width: 180,
align: 'right',
fixed: 'right',
render: (row) => {
return (
<NSpace justify="end">
<NButton size="tiny" strong secondary type="primary" onClick={() => openEditForm(row)}>
{$t('t_11_1745215915429')}
</NButton>
<NButton size="tiny" strong secondary type="error" onClick={() => confirmDelete(row.id)}>
{$t('t_12_1745215914312')}
</NButton>
</NSpace>
)
},
},
]
// 表格实例
const {
component: ApiTable,
loading,
param,
data,
total,
fetch,
} = useTable<AccessItem, AccessListParams>({
config: createColumns(),
request: fetchAccessList,
defaultValue: {
p: 1,
limit: 10,
search: '',
},
watchValue: ['p', 'limit'],
})
// 分页实例
const { component: ApiTablePage } = useTablePage({
param,
total,
alias: {
page: 'p',
pageSize: 'limit',
},
})
/**
* @description 打开添加授权API弹窗
*/
const openAddForm = () => {
useModal({
title: $t('t_0_1745289355714'),
area: 500,
component: ApiManageForm,
footer: true,
onUpdateShow: (show) => {
if (!show) fetch()
},
})
}
/**
* @description 打开编辑授权API弹窗
* @param {AccessItem} row - 授权API信息
*/
const openEditForm = (row: AccessItem) => {
useModal({
title: $t('t_4_1745289354902'),
area: 500,
component: ApiManageForm,
componentProps: { data: row },
footer: true,
onUpdateShow: (show) => {
if (!show) fetch()
},
})
}
/**
* @description 确认删除授权API
* @param {number} id - 授权API ID
*/
const confirmDelete = (id: string) => {
useDialog({
title: $t('t_5_1745289355718'),
content: $t('t_6_1745289358340'),
confirmText: $t('t_5_1744870862719'),
cancelText: $t('t_4_1744870861589'),
onPositiveClick: async () => {
await deleteExistingAccess(id)
await fetch()
},
})
}
// 挂载时,获取数据
onMounted(fetch)
return {
loading,
fetch,
ApiTable,
ApiTablePage,
param,
data,
accessTypes,
openAddForm,
}
}
/**
* @description 授权API表单控制器
* @returns {object} 返回controller对象
*/
export const useApiFormController = (props: { data: AccessItem }) => {
const { confirm } = useModalHooks() // 弹窗挂载方法
const { open: openLoad, close: closeLoad } = useLoadingMask({ text: $t('t_0_1746667592819') })
const { useFormInput, useFormRadioButton, useFormSwitch, useFormTextarea, useFormCustom } = useFormHooks()
const param = (props.data?.id ? ref({ ...props.data, config: JSON.parse(props.data.config) }) : apiFormProps) as Ref<
AddAccessParams | UpdateAccessParams
>
// 表单规则
const rules = {
name: {
required: true,
message: $t('t_27_1745289355721'),
trigger: 'input',
},
type: {
required: true,
message: $t('t_28_1745289356040'),
trigger: 'change',
},
config: {
host: {
required: true,
trigger: 'input',
validator: (rule: FormItemRule, value: string, callback: (error?: Error) => void) => {
if (!isIp(value)) {
return callback(new Error($t('t_0_1745317313835')))
}
callback()
},
},
port: {
required: true,
trigger: 'input',
validator: (rule: FormItemRule, value: number, callback: (error?: Error) => void) => {
if (!isPort(value.toString())) {
return callback(new Error($t('t_1_1745317313096')))
}
callback()
},
},
user: {
required: true,
trigger: 'input',
message: $t('t_3_1744164839524'),
},
password: {
required: true,
message: $t('t_4_1744164840458'),
trigger: 'input',
},
key: {
required: true,
message: $t('t_31_1745289355715'),
trigger: 'input',
},
url: {
required: true,
trigger: 'input',
validator: (rule: FormItemRule, value: string, callback: (error?: Error) => void) => {
if (!isUrl(value)) {
return callback(new Error($t('t_2_1745317314362')))
}
callback()
},
},
api_key: {
required: true,
message: $t('t_3_1745317313561'),
trigger: 'input',
},
access_key_id: {
required: true,
message: $t('t_4_1745317314054'),
trigger: 'input',
},
access_key_secret: {
required: true,
message: $t('t_5_1745317315285'),
trigger: 'input',
},
secret_id: {
required: true,
message: $t('t_6_1745317313383'),
trigger: 'input',
},
secret_key: {
required: true,
message: $t('t_7_1745317313831'),
trigger: 'input',
},
},
}
// 类型列表
const typeList = Object.entries(accessTypes.value).map(([key, value]) => ({ label: value, value: key }))
// 表单配置
const config = computed(() => {
const items: FormConfig = [
useFormInput($t('t_2_1745289353944'), 'name'),
useFormCustom(() => {
return (
<NFormItem label={$t('t_41_1745289354902')} path="type">
<NSelect
class="w-full"
options={typeList}
renderLabel={renderLabel}
renderTag={renderSingleSelectTag}
disabled={!!props.data?.id}
filterable
placeholder={$t('t_0_1745833934390')}
v-model:value={param.value.type}
v-slots={{
empty: () => {
return <span class="text-[1.4rem]">{$t('t_0_1745833934390')}</span>
},
}}
/>
</NFormItem>
)
}),
]
switch (param.value.type) {
case 'ssh':
items.push(
useFormCustom(() => {
return (
<NGrid cols={24} xGap={4}>
<NFormItemGi label={$t('t_1_1745833931535')} span={16} path="config.host">
<NInput v-model:value={(param.value.config as SshAccessConfig).host} />
</NFormItemGi>
<NFormItemGi label={$t('t_2_1745833931404')} span={8} path="config.port">
<NInput v-model:value={(param.value.config as SshAccessConfig).port} />
</NFormItemGi>
</NGrid>
)
}),
useFormInput($t('t_44_1745289354583'), 'config.user'),
useFormRadioButton($t('t_45_1745289355714'), 'config.mode', [
{ label: $t('t_48_1745289355714'), value: 'password' },
{ label: $t('t_1_1746667588689'), value: 'key' },
]),
(param.value.config as SshAccessConfig)?.mode === 'password'
? useFormInput($t('t_48_1745289355714'), 'config.password')
: useFormTextarea($t('t_1_1746667588689'), 'config.key', {
rows: 3,
placeholder: $t('t_3_1745317313561'),
}),
)
break
case '1panel':
case 'btpanel':
items.push(
useFormInput($t('t_2_1746667592840'), 'config.url'),
useFormInput($t('t_55_1745289355715'), 'config.api_key'),
useFormSwitch(
$t('t_3_1746667592270'),
'config.ignore_ssl',
{
checkedValue: '1',
uncheckedValue: '0',
},
{
showRequireMark: false,
},
),
)
break
case 'aliyun':
items.push(
useFormInput('AccessKeyId', 'config.access_key'),
useFormInput('AccessKeySecret', 'config.access_secret'),
)
break
case 'tencentcloud':
items.push(useFormInput('SecretId', 'config.secret_id'), useFormInput('SecretKey', 'config.secret_key'))
break
default:
break
}
return items
})
// 切换类型时,重置表单
watch(
() => param.value.type,
(newVal) => {
switch (newVal) {
case 'ssh':
param.value.config = {
host: '',
port: 22,
user: 'root',
mode: 'password',
password: '',
}
break
case '1panel':
case 'btpanel':
param.value.config = {
url: '',
api_key: '',
ignore_ssl: '0',
}
break
case 'aliyun':
param.value.config = {
access_key_id: '',
access_key_secret: '',
}
break
case 'tencentcloud':
param.value.config = {
secret_id: '',
secret_key: '',
}
break
}
},
)
/**
* @description 渲染单选标签
* @param {Record<string, any>} option - 选项
* @returns {VNode} 渲染后的VNode
*/
const renderSingleSelectTag = ({ option }: Record<string, any>): VNode => {
return (
<div class="flex items-center">
{option.label ? (
<NFlex>
<SvgIcon icon={`resources-${option.value}`} size="2rem" />
<NText>{option.label}</NText>
</NFlex>
) : (
<span class="text-[1.4rem] text-gray-400">{$t('t_0_1745833934390')}</span>
)}
</div>
)
}
/**
* @description 渲染标签
* @param {Record<string, any>} option - 选项
* @returns {VNode} 渲染后的VNode
*/
const renderLabel = (option: { value: string; label: string }): VNode => {
return (
<NFlex>
<SvgIcon icon={`resources-${option.value}`} size="2rem" />
<NText>{option.label}</NText>
</NFlex>
)
}
/**
* @description 提交授权API表单
* @param {UpdateAccessParams | AddAccessParams} param 请求参数
* @param {Ref<FormInst>} formRef 表单实例
*/
const submitApiManageForm = async (param: UpdateAccessParams | AddAccessParams, formRef: Ref<FormInst>) => {
try {
const data = { ...param, config: JSON.stringify(param.config) } as UpdateAccessParams<string>
if ('id' in param) {
const { id, name, config } = data // 解构出 id, name, config
await updateExistingAccess({ id: id.toString(), name, config } as UpdateAccessParams<string>)
} else {
await addNewAccess(data as AddAccessParams<string>)
}
} catch (_) {
return handleError(new Error($t('t_4_1746667590873')))
}
}
// 使用表单hooks
const { component: ApiManageForm, fetch } = useForm({
config,
defaultValue: param,
request: submitApiManageForm,
rules: rules as FormRules,
})
// 关联确认按钮
confirm(async (close) => {
try {
openLoad()
await fetch()
close()
} catch (error) {
return handleError(error)
} finally {
closeLoad()
}
})
return {
ApiManageForm,
}
}

View File

@@ -0,0 +1,146 @@
import { getAccessList, addAccess, updateAccess, deleteAccess } from '@api/access'
import { useError } from '@baota/hooks/error'
import { useMessage } from '@baota/naive-ui/hooks'
import { $t } from '@locales/index'
import type { AccessItem, AccessListParams, AddAccessParams, UpdateAccessParams } from '@/types/access'
import type { TableResponse } from '@baota/naive-ui/types/table'
const { handleError } = useError() // 导入错误处理钩子
const message = useMessage() // 导入消息钩子
/**
* 授权API管理状态 Store
* @description 用于管理授权API相关的状态和操作包括API列表、类型、分页等
*/
export const useAuthApiManageStore = defineStore('auth-api-manage-store', () => {
// -------------------- 状态定义 --------------------
const accessTypes = ref({
ssh: 'SSH',
aliyun: '阿里云',
tencentcloud: '腾讯云',
btpanel: '宝塔',
'1panel': '1Panel',
})
/** 添加/编辑API表单 */
const apiFormProps = ref({
name: '',
type: 'btpanel',
config: {
url: '',
api_key: '',
ignore_ssl: '0',
},
})
// -------------------- 请求方法 --------------------
/**
* 获取授权API列表
* @description 根据分页和关键词获取授权API列表数据
* @param {Object} params - 查询参数
* @returns {Promise<TableResponse<T>>} 返回列表数据和总数
*/
const fetchAccessList = async <T = AccessItem,>(params: AccessListParams): Promise<TableResponse<T>> => {
try {
const res = await getAccessList(params).fetch()
return {
list: (res.data || []) as T[],
total: res.count,
}
} catch (error) {
handleError(error)
return { list: [] as T[], total: 0 }
}
}
/**
* 新增授权API
* @description 创建新的授权API配置
* @param {AddAccessParams} params - 授权API参数
* @returns {Promise<{status: boolean, message: string}>} 操作结果
*/
const addNewAccess = async (params: AddAccessParams<string>) => {
try {
const { fetch, message } = addAccess(params)
message.value = true
await fetch()
resetApiForm()
} catch (error) {
if (handleError(error)) message.error($t('t_8_1745289354902'))
}
}
/**
* 更新授权API
* @description 更新指定的授权API配置信息
* @param {UpdateAccessParams} params - 授权API更新参数
* @returns {Promise<{status: boolean, message: string}>} 操作结果
*/
const updateExistingAccess = async (params: UpdateAccessParams<string>) => {
try {
const { fetch, message } = updateAccess(params)
message.value = true
await fetch()
resetApiForm()
} catch (error) {
if (handleError(error)) message.error($t('t_40_1745227838872'))
}
}
/**
* 删除授权API
* @description 删除指定的授权API配置
* @param {number} id - 授权API ID
* @returns {Promise<{status: boolean, message: string}>} 操作结果
*/
const deleteExistingAccess = async (id: string) => {
try {
const { fetch, message } = deleteAccess({ id })
message.value = true
await fetch()
resetApiForm()
} catch (error) {
if (handleError(error)) message.error($t('t_40_1745227838872'))
}
}
/**
* 重置API表单
* @description 重置表单数据为初始状态
*/
const resetApiForm = () => {
apiFormProps.value = {
name: '',
type: 'btpanel',
config: {
url: '',
api_key: '',
ignore_ssl: '0',
},
}
}
return {
// 状态
accessTypes,
apiFormProps,
// 方法
fetchAccessList,
addNewAccess,
updateExistingAccess,
deleteExistingAccess,
resetApiForm,
}
})
/**
* 组合式 API 使用 Store
* @description 提供对授权API管理 Store 的访问,并返回响应式引用
* @returns {Object} 包含所有 store 状态和方法的对象
*/
export const useStore = () => {
const store = useAuthApiManageStore()
return { ...store, ...storeToRefs(store) }
}