337 lines
8.3 KiB
Vue
337 lines
8.3 KiB
Vue
<template>
|
||
<div class="create-app-template">
|
||
<div class="page-header">
|
||
<div class="header-content">
|
||
<div class="header-text">
|
||
<h1>{{ t('Create App Template') }}</h1>
|
||
<p class="description">{{ t('Create a modern, efficient template app') }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<n-card>
|
||
<n-form
|
||
ref="formRef"
|
||
:model="form"
|
||
:rules="rules"
|
||
label-placement="left"
|
||
label-width="140px"
|
||
size="large"
|
||
>
|
||
<n-grid :cols="3" :x-gap="24" responsive="screen">
|
||
<n-form-item-grid-item>
|
||
<n-form-item :label="t('App Name')" path="appName">
|
||
<n-input
|
||
v-model:value="form.appName"
|
||
:placeholder="t('Enter app name (spaces auto-converted to underscores)')"
|
||
@input="updateSlug"
|
||
/>
|
||
</n-form-item>
|
||
</n-form-item-grid-item>
|
||
|
||
<n-form-item-grid-item>
|
||
<n-form-item :label="t('App Title')" path="appTitle">
|
||
<n-input
|
||
v-model:value="form.appTitle"
|
||
:placeholder="t('App title (auto-generated from app name)')"
|
||
/>
|
||
</n-form-item>
|
||
</n-form-item-grid-item>
|
||
|
||
<n-form-item-grid-item>
|
||
<n-form-item :label="t('Publisher')" path="publisher">
|
||
<n-input
|
||
v-model:value="form.publisher"
|
||
:placeholder="t('Enter publisher name')"
|
||
/>
|
||
</n-form-item>
|
||
</n-form-item-grid-item>
|
||
|
||
<n-form-item-grid-item>
|
||
<n-form-item :label="t('Description')" path="description">
|
||
<n-input
|
||
v-model:value="form.description"
|
||
:placeholder="t('Enter app description')"
|
||
/>
|
||
</n-form-item>
|
||
</n-form-item-grid-item>
|
||
|
||
<n-form-item-grid-item>
|
||
<n-form-item :label="t('Email')" path="email">
|
||
<n-input
|
||
v-model:value="form.email"
|
||
:placeholder="t('Enter support email')"
|
||
/>
|
||
</n-form-item>
|
||
</n-form-item-grid-item>
|
||
|
||
<n-form-item-grid-item>
|
||
<n-form-item :label="t('License')" path="license">
|
||
<n-select
|
||
v-model:value="form.license"
|
||
:options="licenseOptions"
|
||
:placeholder="t('Select license')"
|
||
/>
|
||
</n-form-item>
|
||
</n-form-item-grid-item>
|
||
</n-grid>
|
||
|
||
|
||
<div class="form-actions">
|
||
<n-space>
|
||
<n-button @click="resetForm" size="large">
|
||
{{ t('Reset') }}
|
||
</n-button>
|
||
<n-button
|
||
type="primary"
|
||
size="large"
|
||
:loading="submitting"
|
||
@click="createAppTemplate"
|
||
>
|
||
<template #icon>
|
||
<n-icon><Icon icon="tabler:app-window" /></n-icon>
|
||
</template>
|
||
{{ t('Create App Template') }}
|
||
</n-button>
|
||
</n-space>
|
||
</div>
|
||
</n-form>
|
||
</n-card>
|
||
|
||
<!-- 创建结果 -->
|
||
<n-card v-if="result" class="mt-6">
|
||
<template #header>
|
||
<div class="flex items-center gap-2">
|
||
<n-icon color="#18a058"><Icon icon="tabler:check" /></n-icon>
|
||
<span>{{ t('App Template Created Successfully') }}</span>
|
||
</div>
|
||
</template>
|
||
|
||
<n-descriptions :column="2" bordered>
|
||
<n-descriptions-item :label="t('App Name')">
|
||
{{ result.appName }}
|
||
</n-descriptions-item>
|
||
<n-descriptions-item :label="t('App Path')">
|
||
<n-text code>{{ result.appPath }}</n-text>
|
||
</n-descriptions-item>
|
||
<n-descriptions-item :label="t('Backend Path')">
|
||
<n-text code>{{ result.backendPath }}</n-text>
|
||
</n-descriptions-item>
|
||
<n-descriptions-item :label="t('Frontend Path')">
|
||
<n-text code>{{ result.frontendPath }}</n-text>
|
||
</n-descriptions-item>
|
||
</n-descriptions>
|
||
|
||
</n-card>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue'
|
||
import { useMessage } from 'naive-ui'
|
||
import { Icon } from '@iconify/vue'
|
||
import axios from 'axios'
|
||
import { t } from '@/shared/i18n'
|
||
|
||
const message = useMessage()
|
||
|
||
const formRef = ref()
|
||
const submitting = ref(false)
|
||
const result = ref<any>(null)
|
||
|
||
const form = ref({
|
||
appName: '',
|
||
appTitle: '',
|
||
publisher: 'Your Company',
|
||
description: '',
|
||
email: 'support@yourcompany.com',
|
||
license: 'MIT'
|
||
})
|
||
|
||
const licenseOptions = [
|
||
{ label: 'MIT', value: 'MIT' },
|
||
{ label: 'Apache 2.0', value: 'Apache 2.0' },
|
||
{ label: 'GPL v3', value: 'GPL v3' },
|
||
{ label: 'AGPL v3', value: 'AGPL v3' },
|
||
{ label: 'BSD 3-Clause', value: 'BSD 3-Clause' }
|
||
]
|
||
|
||
const rules = {
|
||
appName: [
|
||
{ required: true, message: t('App name is required') },
|
||
{ pattern: /^[a-z][a-z0-9_]*$/, message: t('App name must start with lowercase letter and contain only lowercase letters, numbers, and underscores') }
|
||
],
|
||
appTitle: [
|
||
{ required: true, message: t('App title is required') }
|
||
],
|
||
publisher: [
|
||
{ required: true, message: t('Publisher is required') }
|
||
],
|
||
description: [
|
||
{ required: true, message: t('Description is required') }
|
||
],
|
||
email: [
|
||
{ required: true, message: t('Email is required') },
|
||
{ type: 'email', message: t('Please enter a valid email') }
|
||
]
|
||
}
|
||
|
||
const updateSlug = () => {
|
||
if (form.value.appName) {
|
||
// 自动转换为小写,空格转换为下划线
|
||
form.value.appName = form.value.appName
|
||
.toLowerCase()
|
||
.replace(/\s+/g, '_')
|
||
.replace(/[^a-z0-9_]/g, '') // 只保留小写字母、数字和下划线
|
||
|
||
// 自动生成应用标题:将下划线分隔的单词转换为标题格式
|
||
// 例如:my_app -> My App, customer_management -> Customer Management
|
||
form.value.appTitle = form.value.appName
|
||
.split('_')
|
||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||
.join(' ')
|
||
} else {
|
||
// 如果应用名称为空,清空应用标题
|
||
form.value.appTitle = ''
|
||
}
|
||
}
|
||
|
||
const resetForm = () => {
|
||
form.value = {
|
||
appName: '',
|
||
appTitle: '',
|
||
publisher: 'Your Company',
|
||
description: '',
|
||
email: 'support@yourcompany.com',
|
||
license: 'MIT'
|
||
}
|
||
result.value = null
|
||
}
|
||
|
||
const createAppTemplate = async () => {
|
||
try {
|
||
await formRef.value?.validate()
|
||
submitting.value = true
|
||
|
||
const response = await axios.post('/jingrow/dev/create-app-template', form.value)
|
||
|
||
if (response.data.success) {
|
||
result.value = response.data
|
||
message.success(t('App template created successfully'))
|
||
} else {
|
||
message.error(response.data.error || t('Failed to create app template'))
|
||
}
|
||
} catch (error: any) {
|
||
console.error('Create app template error:', error)
|
||
message.error(error.response?.data?.detail || error.message || t('Failed to create app template'))
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.create-app-template {
|
||
padding: 16px 24px;
|
||
width: 100%;
|
||
margin: 0;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.page-header {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.header-content {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.header-text h1 {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
margin: 0 0 8px 0;
|
||
color: var(--text-color);
|
||
}
|
||
|
||
.description {
|
||
color: var(--text-color-2);
|
||
margin: 0;
|
||
}
|
||
|
||
.content {
|
||
margin-top: 24px;
|
||
width: 100%;
|
||
}
|
||
|
||
.form-actions {
|
||
margin-top: 32px;
|
||
padding-top: 24px;
|
||
border-top: 1px solid var(--border-color);
|
||
}
|
||
|
||
.ml-2 {
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.mt-4 {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.mt-6 {
|
||
margin-top: 24px;
|
||
}
|
||
|
||
.flex {
|
||
display: flex;
|
||
}
|
||
|
||
.items-center {
|
||
align-items: center;
|
||
}
|
||
|
||
.gap-2 {
|
||
gap: 8px;
|
||
}
|
||
|
||
.gap-24 {
|
||
gap: 24px;
|
||
}
|
||
|
||
.space-y-2 > * + * {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.list-decimal {
|
||
list-style-type: decimal;
|
||
}
|
||
|
||
.list-inside {
|
||
list-style-position: inside;
|
||
}
|
||
|
||
/* 响应式布局 */
|
||
@media (max-width: 1200px) {
|
||
.create-app-template {
|
||
padding: 12px 16px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.create-app-template {
|
||
padding: 8px 12px;
|
||
}
|
||
|
||
.page-header {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.content {
|
||
margin-top: 16px;
|
||
}
|
||
}
|
||
</style>
|