vue3-django 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/migration.md CHANGED
@@ -201,6 +201,10 @@ const actions = [
201
201
  `#actions` 插槽可替换操作区域。布局转发 `action-done(result, action)` 和 `error`。
202
202
  其他位置也可直接使用 `<Actions>`;抽屉操作需要位于 Layout 子树中。
203
203
 
204
+ 表单 `widget: 'textarea'` 沿用原版默认自适应高度:最少 8 行、最多 24 行。
205
+ 通过字段配置 `autosize: { minRows: 4, maxRows: 12 }` 自定义行数范围,
206
+ 也可设为 `true` 随内容自动增高,或 `false` 关闭自适应高度。
207
+
204
208
  JSON 表单组件已迁移到 Vue 3,可从包入口导入 `JsonInput`、`JsonEditor`。
205
209
  字段配置支持 `widget: 'JsonInput'` / `widget: 'JsonEditor'`(也支持 `json-input`、
206
210
  `json-editor`),或直接传入组件。使用 `v-model` / `update:modelValue` 替代旧版
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue3-django",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "DRF metadata-driven Vue 3 framework",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,6 +18,12 @@ const props = defineProps({
18
18
  const emit = defineEmits(['update:modelValue', 'uploading', 'error'])
19
19
  const value = computed({ get: () => props.modelValue, set: (v) => emit('update:modelValue', v) })
20
20
  const numberType = computed(() => ['integer', 'decimal', 'float'].includes(props.field.type))
21
+ // ElInputNumber's numeric step also drives its buttons. Set the native constraint
22
+ // separately so decimal input accepts any precision without breaking the controls.
23
+ function setNumberStep(el, { value }) {
24
+ el.querySelector('input')?.setAttribute('step', value)
25
+ }
26
+ const vNumberStep = { mounted: setNumberStep, updated: setNumberStep }
21
27
  const widget = computed(() => toRaw(props.field.widget))
22
28
  const jsonWidget = computed(() => {
23
29
  const name =
@@ -154,6 +160,7 @@ function mobileInput(v) {
154
160
  />
155
161
  <ElInputNumber
156
162
  v-else-if="numberType"
163
+ v-number-step="field.type === 'integer' ? '1' : 'any'"
157
164
  v-model="value"
158
165
  controls-position="right"
159
166
  :precision="field.type === 'integer' ? 0 : undefined"
@@ -173,6 +180,9 @@ function mobileInput(v) {
173
180
  v-else
174
181
  v-model="value"
175
182
  :type="['textarea', 'password'].includes(field.widget) ? field.widget : 'text'"
183
+ :autosize="
184
+ field.widget === 'textarea' ? (field.autosize ?? { minRows: 8, maxRows: 24 }) : false
185
+ "
176
186
  :maxlength="field.max_length"
177
187
  :autocomplete="field.autocomplete"
178
188
  :placeholder="field.placeholder ?? [field.label, field.help_text].filter(Boolean).join('\n')"
@@ -17,6 +17,7 @@ const { registry, revisions, store } = useDjango()
17
17
  const appModel = computed(() => props.appModel ?? route.path.split('/').slice(1, 3).join('.'))
18
18
  const id = computed(() => (props.mode === 'create' ? undefined : (props.id ?? route.params.id)))
19
19
  const data = ref({})
20
+ const savedData = ref({})
20
21
  const views = shallowRef()
21
22
  const error = ref('')
22
23
  const form = ref()
@@ -28,13 +29,37 @@ const parent = computed(() => ({
28
29
  viewsConfig: views.value,
29
30
  }))
30
31
  const panels = computed(() => views.value?.panels ?? views.value?.pannels ?? [])
31
- const persisted = computed(() => parent.value.id != null)
32
+ const relationParent = computed(() => ({
33
+ ...registry.get(appModel.value),
34
+ id: savedData.value[registry.getConfig(appModel.value).idField ?? 'id'] ?? id.value,
35
+ data: savedData.value,
36
+ viewsConfig: views.value,
37
+ }))
38
+ const persisted = computed(() => relationParent.value.id != null)
39
+ function snapshot(value) {
40
+ // API data can contain nested objects; drafts must not mutate the saved snapshot.
41
+ savedData.value = JSON.parse(JSON.stringify(value))
42
+ }
43
+ function loaded(model) {
44
+ snapshot(data.value)
45
+ emit('loaded', model)
46
+ }
47
+ function parentUpdated(value) {
48
+ const changes = Object.fromEntries(
49
+ Object.entries(value).filter(
50
+ ([key, item]) => JSON.stringify(item) !== JSON.stringify(savedData.value[key]),
51
+ ),
52
+ )
53
+ data.value = { ...data.value, ...changes }
54
+ snapshot(value)
55
+ }
32
56
  let generation = 0
33
57
  watch(
34
58
  () => [appModel.value, id.value],
35
59
  async () => {
36
60
  const current = ++generation
37
61
  data.value = {}
62
+ savedData.value = {}
38
63
  views.value = undefined
39
64
  error.value = ''
40
65
  activePanels.value = [0]
@@ -52,6 +77,7 @@ watch(
52
77
  )
53
78
  onBeforeUnmount(() => generation++)
54
79
  async function posted(event) {
80
+ snapshot({ ...savedData.value, ...event.data })
55
81
  if (store) store.invalidate(appModel.value)
56
82
  else if (revisions) revisions[appModel.value] = (revisions[appModel.value] ?? 0) + 1
57
83
  emit('form-posted', event)
@@ -59,6 +85,7 @@ async function posted(event) {
59
85
  if (event.intent === 'saveAndAnother') {
60
86
  const wasCreating = id.value == null
61
87
  data.value = {}
88
+ savedData.value = {}
62
89
  await router.replace(`/${appModel.value.replace('.', '/')}/add/`)
63
90
  if (wasCreating) await form.value?.load()
64
91
  } else if (id.value == null && event.data[key] != null) {
@@ -90,7 +117,7 @@ defineExpose({ submit: () => form.value?.submit(), load: () => form.value?.load(
90
117
  :app-model="appModel"
91
118
  :id="id"
92
119
  @form-posted="posted"
93
- @loaded="emit('loaded', $event)"
120
+ @loaded="loaded"
94
121
  @error="emit('error', $event)"
95
122
  >
96
123
  <template
@@ -122,8 +149,8 @@ defineExpose({ submit: () => form.value?.submit(), load: () => form.value?.load(
122
149
  </ElCollapseItem>
123
150
  </ElCollapse>
124
151
  <ModelRelations
125
- :parent="parent"
126
- @parent-updated="data = $event"
152
+ :parent="relationParent"
153
+ @parent-updated="parentUpdated"
127
154
  @edit="editRelation"
128
155
  @error="emit('error', $event)"
129
156
  />