feat: shrimp-admin v0.1.0 — 从 hand-admin 抽离的框架前端
Shrimp 框架前端 — 一种消费 ShrimpServer + ShrimpTable 契约的经典 Vue3 +
Element Plus 实现。不是框架本身,只是按 shrimp-server 约定写的一份参考解法。
包含:
- ShrimpClient + VueAdmin(factory:createShrimpAdmin)
- 5 个 Shrimp* 组件 + 10 个内置 Field*.vue + PowerTree + LangSwitch
- 4 个内置模块:user / role / operationLog / FieldRelativeTime
- 完整后台壳 views(layout/login/sideM/topbar/userbar/navMenu/App/404/logo)
- composables/useTableUrlSync, locales(zh-cn/en 框架默认), style 全套
API 改造:
- createShrimpAdmin({ config, moduleSources, customFields, locales }) factory
- vueAdmin 改为 Proxy 单例,createShrimpAdmin 调用前访问会抛错
- import.meta.glob 业务字段/模块由消费侧传入(vite 静态扫描机制限制)
- 框架默认 locales 与消费侧 locales 在 i18n 初始化时合并
源码 ship 不打包,消费侧靠 vite 直接吃 .ts/.vue。
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<el-drawer class="shrimp-drawer" :model-value="visible" @update:model-value="$emit('update:visible', $event)" :title="title" size="400px">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" @submit.prevent>
|
||||
<template v-for="field in fields" :key="field.key">
|
||||
<el-form-item :label="vueAdmin.t(field.label)" :prop="field.key" :required="field.required">
|
||||
<slot :name="`form-${field.key}`" :field="field" :data="formData" :isEdit="isEdit">
|
||||
<component :is="vueAdmin.getFieldComponent(moduleKey, field.type, field.key)" :field="field"
|
||||
:model-value="formData[field.key]"
|
||||
@update:model-value="onFieldChange(field.key, $event)"
|
||||
mode="edit"
|
||||
:disabled="isEdit && (field.editable === 'readonly' || field.editable === 'addOnly')" />
|
||||
</slot>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="$emit('update:visible', false)">{{ $t('base.cancel') }}</el-button>
|
||||
<el-button type="primary" :disabled="!canSave" @click="$emit('save')">{{ $t('base.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { provide, computed, ref } from 'vue'
|
||||
import { IFieldConfig, IFieldRule } from 'shrimp-server/types'
|
||||
import { vueAdmin } from '../VueAdmin'
|
||||
import type { FormInstance, FormItemRule } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
title: string
|
||||
moduleKey: string
|
||||
fields: IFieldConfig[]
|
||||
formData: Record<string, any>
|
||||
isEdit: boolean
|
||||
canSave?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
provide('shrimpModuleKey', props.moduleKey)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
'update:formData': [value: Record<string, any>]
|
||||
save: []
|
||||
}>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
function buildRules(field: IFieldConfig): FormItemRule[] {
|
||||
const rules: FormItemRule[] = []
|
||||
|
||||
if (field.required) {
|
||||
rules.push({
|
||||
required: true,
|
||||
message: vueAdmin.t('shrimp.fieldRequired'),
|
||||
trigger: 'change',
|
||||
...(field.type === 'string' || field.type === 'textarea' || field.type === 'password' ? { trigger: 'blur' } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
if (field.rules) {
|
||||
for (const r of field.rules) {
|
||||
const rule: FormItemRule = { trigger: 'change' }
|
||||
if (r.min !== undefined) rule.min = r.min
|
||||
if (r.max !== undefined) rule.max = r.max
|
||||
if (r.pattern) rule.pattern = new RegExp(r.pattern)
|
||||
if (r.message) rule.message = vueAdmin.t(r.message)
|
||||
rules.push(rule)
|
||||
}
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
const formRules = computed(() => {
|
||||
const rules: Record<string, FormItemRule[]> = {}
|
||||
for (const field of props.fields) {
|
||||
const list = buildRules(field)
|
||||
if (list.length > 0) {
|
||||
rules[field.key] = list
|
||||
}
|
||||
}
|
||||
return rules
|
||||
})
|
||||
|
||||
function onFieldChange(key: string, value: any) {
|
||||
emit('update:formData', { ...props.formData, [key]: value })
|
||||
}
|
||||
|
||||
async function validate(): Promise<boolean> {
|
||||
if (!formRef.value) return true
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ validate })
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.shrimp-drawer .el-drawer__header {
|
||||
margin-bottom: 0;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.shrimp-drawer .el-drawer__body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.shrimp-drawer .el-drawer__footer {
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.shrimp-drawer .el-drawer__body .el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user