后台右上角增加快速创建图标并实现快速创建pagetype记录的功能
This commit is contained in:
parent
7dde2c4026
commit
535c17bd9b
@ -55,6 +55,9 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 创建 -->
|
||||
<QuickCreate />
|
||||
|
||||
<!-- 通知 -->
|
||||
<NotificationDropdown />
|
||||
|
||||
@ -74,6 +77,7 @@ import { useAuthStore } from '../../shared/stores/auth'
|
||||
import { t } from '../../shared/i18n'
|
||||
import UserMenu from '../../shared/components/UserMenu.vue'
|
||||
import NotificationDropdown from '../../shared/components/NotificationDropdown.vue'
|
||||
import QuickCreate from '../../shared/components/QuickCreate.vue'
|
||||
import AwesomeBarDropdown from '../../shared/components/AwesomeBarDropdown.vue'
|
||||
import { useAwesomeBar } from '../../shared/composables/useAwesomeBar'
|
||||
|
||||
|
||||
@ -141,6 +141,16 @@ export const getPageTypeMeta = async (pagetype: string): Promise<{ success: bool
|
||||
}
|
||||
}
|
||||
|
||||
// Get quick entry fields for a PageType
|
||||
export const getQuickEntryFields = async (pagetype: string): Promise<{ success: boolean; data?: any[]; message?: string }> => {
|
||||
try {
|
||||
const result = await api.call('jingrow.client.get_quick_entry_fields', { pagetype })
|
||||
return { success: true, data: result.message || [] }
|
||||
} catch (error: any) {
|
||||
return { success: false, data: [], message: error.message || t('Failed to get quick entry fields') }
|
||||
}
|
||||
}
|
||||
|
||||
// Get record attachments list
|
||||
export const getRecordAttachments = async (pagetype: string, name: string): Promise<{ success: boolean; data?: any[]; message?: string }> => {
|
||||
try {
|
||||
|
||||
298
frontend/src/shared/components/QuickCreate.vue
Normal file
298
frontend/src/shared/components/QuickCreate.vue
Normal file
@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<n-popover
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
:show="showPopover"
|
||||
@update:show="handlePopoverUpdate"
|
||||
:show-arrow="false"
|
||||
:style="{ padding: 0 }"
|
||||
raw
|
||||
>
|
||||
<template #trigger>
|
||||
<n-button quaternary circle>
|
||||
<template #icon>
|
||||
<Icon icon="tabler:plus" :size="20" />
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<div class="quick-create">
|
||||
<!-- Menu View -->
|
||||
<template v-if="view === 'menu'">
|
||||
<div class="qc-header">{{ t('Quick Create') }}</div>
|
||||
<div class="qc-menu">
|
||||
<div
|
||||
v-for="item in menuItems"
|
||||
:key="item.label"
|
||||
class="qc-menu-item"
|
||||
@click="selectPageType(item)"
|
||||
>
|
||||
<Icon :icon="item.icon" :size="18" />
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Form View -->
|
||||
<template v-else>
|
||||
<div class="qc-header">
|
||||
<n-button text size="small" @click="backToMenu">
|
||||
<template #icon><Icon icon="tabler:arrow-left" :size="16" /></template>
|
||||
</n-button>
|
||||
<span class="qc-header-title">{{ t('New {0}', [selectedLabel]) }}</span>
|
||||
<n-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="submitting"
|
||||
@click="handleSubmit"
|
||||
class="save-btn-brand"
|
||||
>
|
||||
<template #icon><Icon icon="tabler:check" :size="14" /></template>
|
||||
{{ t('Save') }}
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingFields" class="qc-loading">
|
||||
<n-spin size="small" />
|
||||
</div>
|
||||
<div v-else class="qc-form">
|
||||
<div v-for="field in fields" :key="field.fieldname" class="qc-field">
|
||||
<FieldRenderer
|
||||
:df="field"
|
||||
:record="formData"
|
||||
:can-edit="true"
|
||||
:ctx="fieldCtx"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</n-popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, nextTick } from 'vue'
|
||||
import { NPopover, NButton, NSpin, useMessage } from 'naive-ui'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import FieldRenderer from '@/core/pagetype/form/FieldRenderer.vue'
|
||||
import { getQuickEntryFields, createRecord } from '../api/common'
|
||||
import { t } from '../i18n'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const showPopover = ref(false)
|
||||
const view = ref<'menu' | 'form'>('menu')
|
||||
const selectedPageType = ref('')
|
||||
const selectedLabel = ref('')
|
||||
const fields = ref<any[]>([])
|
||||
const formData = reactive<Record<string, any>>({})
|
||||
const loadingFields = ref(false)
|
||||
const submitting = ref(false)
|
||||
|
||||
// Minimal ctx for FieldRenderer
|
||||
const fieldCtx: Record<string, any> = {
|
||||
t,
|
||||
entity: '',
|
||||
getSelectOptions(field: any) {
|
||||
if (!field.options) return []
|
||||
return field.options.split('\n').filter((s: string) => s.trim() !== '').map((opt: string) => ({ label: t(opt), value: opt }))
|
||||
},
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
{ label: 'Activity', icon: 'tabler:activity' },
|
||||
{ label: 'ToDo', icon: 'tabler:checklist' },
|
||||
{ label: 'Knowledge Base', icon: 'tabler:book' },
|
||||
{ label: 'Note', icon: 'tabler:note' },
|
||||
]
|
||||
|
||||
const handlePopoverUpdate = (show: boolean) => {
|
||||
showPopover.value = show
|
||||
}
|
||||
|
||||
const selectPageType = async (item: { label: string; icon: string }) => {
|
||||
selectedPageType.value = item.label
|
||||
selectedLabel.value = item.label
|
||||
fieldCtx.entity = item.label
|
||||
view.value = 'form'
|
||||
loadingFields.value = true
|
||||
|
||||
// Reset form data
|
||||
Object.keys(formData).forEach(k => delete formData[k])
|
||||
|
||||
const result = await getQuickEntryFields(item.label)
|
||||
if (result.success && result.data) {
|
||||
fields.value = result.data
|
||||
// Set defaults
|
||||
for (const field of result.data) {
|
||||
if (field.default) {
|
||||
if (field.fieldtype === 'Check') {
|
||||
formData[field.fieldname] = field.default ? 1 : 0
|
||||
} else if (field.fieldtype === 'Date' && field.default === 'Today') {
|
||||
formData[field.fieldname] = new Date().toISOString().split('T')[0]
|
||||
} else {
|
||||
formData[field.fieldname] = field.default
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message.error(result.message || t('Failed to load fields'))
|
||||
}
|
||||
loadingFields.value = false
|
||||
}
|
||||
|
||||
const backToMenu = () => {
|
||||
view.value = 'menu'
|
||||
fields.value = []
|
||||
Object.keys(formData).forEach(k => delete formData[k])
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// Validate required fields
|
||||
for (const field of fields.value) {
|
||||
if (field.reqd && !formData[field.fieldname]) {
|
||||
message.warning(t('{0} is required', [field.label]))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
// Convert Check boolean to int
|
||||
const data = { ...formData }
|
||||
for (const field of fields.value) {
|
||||
if (field.fieldtype === 'Check') {
|
||||
data[field.fieldname] = data[field.fieldname] ? 1 : 0
|
||||
}
|
||||
}
|
||||
|
||||
const result = await createRecord(selectedPageType.value, data)
|
||||
if (result.success) {
|
||||
message.success(t('{0} created successfully', [selectedLabel.value]))
|
||||
nextTick(() => {
|
||||
showPopover.value = false
|
||||
backToMenu()
|
||||
})
|
||||
} else {
|
||||
message.error(result.message || t('Failed to create record'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('Failed to create record'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.quick-create {
|
||||
width: 600px;
|
||||
background: var(--n-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qc-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid var(--n-border-color, #f0f0f0);
|
||||
}
|
||||
|
||||
.qc-header-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 保存按钮 - 与详情页风格一致 */
|
||||
.save-btn-brand {
|
||||
background: #e6f8f0 !important;
|
||||
border: 1px solid #1fc76f !important;
|
||||
color: #0d684b !important;
|
||||
}
|
||||
|
||||
.save-btn-brand :deep(.n-button__border) {
|
||||
border: none !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.save-btn-brand :deep(.n-button__state-border) {
|
||||
border: none !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.save-btn-brand:hover {
|
||||
background: #dcfce7 !important;
|
||||
border: 1px solid #1fc76f !important;
|
||||
color: #166534 !important;
|
||||
box-shadow: 0 2px 8px rgba(31, 199, 111, 0.15) !important;
|
||||
}
|
||||
|
||||
.save-btn-brand:hover :deep(.n-button__border),
|
||||
.save-btn-brand:hover :deep(.n-button__state-border) {
|
||||
border: none !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.save-btn-brand:focus {
|
||||
background: #dcfce7 !important;
|
||||
border: 1px solid #1fc76f !important;
|
||||
color: #166534 !important;
|
||||
box-shadow: 0 0 0 2px rgba(31, 199, 111, 0.2) !important;
|
||||
}
|
||||
|
||||
.save-btn-brand:focus :deep(.n-button__border),
|
||||
.save-btn-brand:focus :deep(.n-button__state-border) {
|
||||
border: none !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.save-btn-brand:active {
|
||||
background: #1fc76f !important;
|
||||
border: 1px solid #1fc76f !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.save-btn-brand:active :deep(.n-button__border),
|
||||
.save-btn-brand:active :deep(.n-button__state-border) {
|
||||
border: none !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.qc-menu {
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.qc-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.qc-menu-item:hover {
|
||||
background-color: var(--n-color-hover, #f5f5f5);
|
||||
}
|
||||
|
||||
.qc-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.qc-form {
|
||||
padding: 16px;
|
||||
max-height: 520px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.qc-field {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
@ -95,6 +95,30 @@ def get(pagetype, name=None, filters=None, parent=None):
|
||||
return pg.as_dict()
|
||||
|
||||
|
||||
@jingrow.whitelist()
|
||||
def get_quick_entry_fields(pagetype):
|
||||
"""Returns fields for quick entry form of given pagetype"""
|
||||
meta = jingrow.get_meta(pagetype)
|
||||
fields = []
|
||||
for df in meta.fields:
|
||||
if (
|
||||
(df.reqd or df.allow_in_quick_entry)
|
||||
and not df.read_only
|
||||
and not df.is_virtual
|
||||
and df.fieldtype not in ("Tab Break", "Section Break", "Column Break")
|
||||
):
|
||||
fields.append({
|
||||
"fieldname": df.fieldname,
|
||||
"label": df.label,
|
||||
"fieldtype": df.fieldtype,
|
||||
"reqd": df.reqd or 0,
|
||||
"options": df.options or "",
|
||||
"default": df.default or "",
|
||||
"description": df.description or "",
|
||||
})
|
||||
return fields
|
||||
|
||||
|
||||
@jingrow.whitelist()
|
||||
def get_value(pagetype, fieldname, filters=None, as_dict=True, debug=False, parent=None):
|
||||
"""Returns a value form a document
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
@ -30,6 +31,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"default": "Note",
|
||||
"fieldname": "activity_type",
|
||||
"fieldtype": "Select",
|
||||
@ -44,6 +46,7 @@
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "content",
|
||||
"fieldtype": "Jeditor",
|
||||
"label": "Content"
|
||||
@ -76,7 +79,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-20 21:35:36.193833",
|
||||
"modified": "2026-04-21 23:42:39.213855",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Activity",
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_filter": 1,
|
||||
@ -21,11 +22,13 @@
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "content",
|
||||
"fieldtype": "Jeditor",
|
||||
"label": "Content"
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
@ -42,6 +45,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "category",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@ -53,7 +57,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-01 22:31:27.311442",
|
||||
"modified": "2026-04-21 23:43:13.225221",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Knowledge Base",
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
@ -59,6 +60,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"bold": 1,
|
||||
"description": "Help: To link to another record in the system, use \"/app/note/[Note Name]\" as the Link URL. (don't use \"http://\")",
|
||||
"fieldname": "content",
|
||||
@ -85,7 +87,7 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-01 14:07:24.540571",
|
||||
"modified": "2026-04-21 23:44:37.725453",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Note",
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
"options": "Todo\nIn Progress\nReview\nDone\nCancelled"
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"default": "Medium",
|
||||
"fieldname": "priority",
|
||||
"fieldtype": "Select",
|
||||
@ -77,6 +78,7 @@
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Jeditor",
|
||||
"in_global_search": 1,
|
||||
@ -148,6 +150,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "allocated_to",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
@ -158,8 +161,10 @@
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Title",
|
||||
"reqd": 1
|
||||
@ -168,7 +173,7 @@
|
||||
"icon": "fa fa-check",
|
||||
"idx": 2,
|
||||
"links": [],
|
||||
"modified": "2026-04-21 21:31:33.885406",
|
||||
"modified": "2026-04-21 23:41:45.406544",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "ToDo",
|
||||
|
||||
@ -35761,3 +35761,26 @@ msgstr "下载备份"
|
||||
msgid "No ToDos"
|
||||
msgstr "没有待办事项"
|
||||
|
||||
# Quick Create
|
||||
msgid "Quick Create"
|
||||
msgstr "快速创建"
|
||||
|
||||
msgid "New {0}"
|
||||
msgstr "新建{0}"
|
||||
|
||||
msgid "{0} is required"
|
||||
msgstr "{0}为必填项"
|
||||
|
||||
msgid "{0} created successfully"
|
||||
msgstr "{0}创建成功"
|
||||
|
||||
msgid "Failed to load fields"
|
||||
msgstr "加载字段失败"
|
||||
|
||||
msgid "Failed to create record"
|
||||
msgstr "创建记录失败"
|
||||
|
||||
msgid "Failed to get quick entry fields"
|
||||
msgstr "获取快速创建字段失败"
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user