@hostlink/nuxt-light 1.64.2 → 1.65.0

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/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "light",
3
3
  "configKey": "light",
4
- "version": "1.64.2",
4
+ "version": "1.65.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -1,7 +1,7 @@
1
1
  <script setup>
2
- import { computed, ref, useAttrs } from "vue";
2
+ import { computed, ref, useAttrs, getCurrentInstance } from "vue";
3
3
  import { useRouter, useRoute } from "vue-router";
4
- import { useQuasar } from "quasar";
4
+ import { useQuasar, QForm } from "quasar";
5
5
  import { model } from "#imports";
6
6
  const route = useRoute();
7
7
  const router = useRouter();
@@ -33,17 +33,24 @@ const props = defineProps({
33
33
  }
34
34
  });
35
35
  const loading = ref(false);
36
+ const attrs = useAttrs();
36
37
  const emit = defineEmits(["save", "submit", "submitted"]);
38
+ const instance = getCurrentInstance();
39
+ const hasSubmitListener = computed(() => {
40
+ return !!instance?.vnode?.props?.onSubmit;
41
+ });
37
42
  const save = async () => {
43
+ if (!form.value) return;
38
44
  let valid = await form.value.validate();
39
45
  if (!valid) return;
40
- if (valid) {
41
- loading.value = true;
42
- emit("save");
43
- emit("submit");
46
+ emit("save");
47
+ if (hasSubmitListener.value) {
48
+ emit("submit", () => {
49
+ loading.value = false;
50
+ });
51
+ return;
44
52
  }
45
- if (props.modelValue) {
46
- const [module, id_name] = route.name.split("-");
53
+ if (props.modelValue && route.name) {
47
54
  try {
48
55
  if (route.params[id_name]) {
49
56
  if (await model(module).update(parseInt(route.params[id_name]), props.modelValue)) {
@@ -76,7 +83,6 @@ const onSubmit = (e) => {
76
83
  e.preventDefault();
77
84
  save();
78
85
  };
79
- const attrs = useAttrs();
80
86
  const localClass = computed(() => {
81
87
  if (attrs.class) {
82
88
  return attrs.class;
@@ -93,7 +99,7 @@ const localClass = computed(() => {
93
99
  </q-card-section>
94
100
 
95
101
  <q-card-actions align="right">
96
- <l-btn :icon="submitIcon" :label="submitLabel" @click="save" :loading="loading" type="submit" />
102
+ <l-btn :icon="submitIcon" :label="submitLabel" :loading="loading" type="submit" />
97
103
  </q-card-actions>
98
104
  </l-card>
99
105
  </q-form>
@@ -12,8 +12,12 @@ const props = defineProps({
12
12
  </script>
13
13
 
14
14
  <template>
15
- <router-link :to="props.to" style="text-decoration: underline;" class="text-primary">
15
+ <component
16
+ :is="props.to ? 'router-link' : 'span'"
17
+ :to="props.to || undefined"
18
+ :style="props.to ? 'text-decoration: underline;' : ''"
19
+ :class="props.to ? 'text-primary' : ''">
16
20
  <template v-if="props.label">{{ $t(props.label) }}</template>
17
21
  <slot v-else />
18
- </router-link>
22
+ </component>
19
23
  </template>
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { ref, reactive } from "vue";
2
+ import { ref, reactive, computed } from "vue";
3
3
  import { m, q, useAsyncData } from "#imports";
4
4
  import { useQuasar } from "quasar";
5
5
  import { useI18n } from "vue-i18n";
@@ -7,29 +7,74 @@ const $q = useQuasar();
7
7
  const { t } = useI18n();
8
8
  const { app } = await q({ app: { languages: true } });
9
9
  const splitterModel = ref(62);
10
+ const filter = ref("");
10
11
  const { data: all, refresh } = await useAsyncData("translate", () => {
11
12
  return q({ allTranslate: true }).then((res) => res.allTranslate);
12
13
  });
14
+ const filteredRows = computed(() => {
15
+ if (!filter.value || !all.value) return all.value;
16
+ const searchTerm = filter.value.toLowerCase();
17
+ return all.value.filter((row) => {
18
+ if (row.name?.toLowerCase().includes(searchTerm)) return true;
19
+ for (const language of app.languages) {
20
+ if (row[language.value]?.toLowerCase().includes(searchTerm)) return true;
21
+ }
22
+ return false;
23
+ });
24
+ });
13
25
  const obj = reactive({
14
- name: ""
26
+ name: "",
27
+ _originalName: ""
28
+ // 用於追蹤原始名稱(編輯模式)
15
29
  });
30
+ const isEditMode = computed(() => !!obj._originalName);
31
+ const onSelectRow = (row) => {
32
+ obj.name = row.name;
33
+ obj._originalName = row.name;
34
+ for (const language of app.languages) {
35
+ obj[language.value] = row[language.value] || "";
36
+ }
37
+ };
38
+ const onClearForm = () => {
39
+ obj.name = "";
40
+ obj._originalName = "";
41
+ for (const language of app.languages) {
42
+ obj[language.value] = "";
43
+ }
44
+ };
16
45
  const onSave = async () => {
17
- await m("addTranslate", {
18
- data: {
19
- name: obj.name,
20
- values: app.languages.map((language) => {
21
- return {
22
- language: language.value,
23
- value: obj[language.value]
24
- };
25
- })
46
+ if (isEditMode.value) {
47
+ for (const language of app.languages) {
48
+ await m("updateTranslate", {
49
+ name: obj._originalName,
50
+ language: language.value,
51
+ value: obj[language.value] || ""
52
+ });
26
53
  }
27
- });
28
- $q.notify({
29
- message: "Save success",
30
- color: "positive",
31
- icon: "check"
32
- });
54
+ $q.notify({
55
+ message: t("Update success"),
56
+ color: "positive",
57
+ icon: "check"
58
+ });
59
+ } else {
60
+ await m("addTranslate", {
61
+ data: {
62
+ name: obj.name,
63
+ values: app.languages.map((language) => {
64
+ return {
65
+ language: language.value,
66
+ value: obj[language.value] || ""
67
+ };
68
+ })
69
+ }
70
+ });
71
+ $q.notify({
72
+ message: t("Save success"),
73
+ color: "positive",
74
+ icon: "check"
75
+ });
76
+ }
77
+ onClearForm();
33
78
  await refresh();
34
79
  };
35
80
  const columns = [
@@ -52,31 +97,25 @@ for (const language of app.languages) {
52
97
  align: "left"
53
98
  });
54
99
  }
55
- const onUpdateTranslate = async (value, language, name) => {
56
- if (await m("updateTranslate", {
57
- name,
58
- language,
59
- value
60
- })) {
61
- $q.notify({
62
- message: "Update success",
63
- color: "positive",
64
- icon: "check"
65
- });
66
- }
67
- await refresh();
68
- };
69
100
  const onDelete = async (name) => {
70
- if (await m("deleteTranslate", {
71
- name
72
- })) {
73
- $q.notify({
74
- message: "Delete success",
75
- color: "positive",
76
- icon: "check"
77
- });
78
- await refresh();
79
- }
101
+ $q.dialog({
102
+ title: t("Confirm"),
103
+ message: t("Are you sure you want to delete this item?"),
104
+ cancel: true,
105
+ persistent: true
106
+ }).onOk(async () => {
107
+ if (await m("deleteTranslate", { name })) {
108
+ $q.notify({
109
+ message: t("Delete success"),
110
+ color: "positive",
111
+ icon: "check"
112
+ });
113
+ if (obj._originalName === name) {
114
+ onClearForm();
115
+ }
116
+ await refresh();
117
+ }
118
+ });
80
119
  };
81
120
  </script>
82
121
 
@@ -85,38 +124,115 @@ const onDelete = async (name) => {
85
124
  <l-card>
86
125
  <q-splitter v-model="splitterModel" style="height:680px">
87
126
  <template #before>
88
- <q-table :rows="all" flat :rows-per-page-options="[0]" :columns="columns" dense separator="cell"
89
- :bordered="false">
127
+ <q-table
128
+ :rows="filteredRows"
129
+ flat
130
+ :rows-per-page-options="[0]"
131
+ :columns="columns"
132
+ dense
133
+ separator="cell"
134
+ :bordered="false"
135
+ >
136
+ <template #top>
137
+ <q-input
138
+ v-model="filter"
139
+ dense
140
+ outlined
141
+ :placeholder="t('Search...')"
142
+ clearable
143
+ class="full-width"
144
+ >
145
+ <template #prepend>
146
+ <q-icon name="sym_o_search" />
147
+ </template>
148
+ </q-input>
149
+ </template>
90
150
  <template #body="props">
91
- <q-tr :props="props">
92
- <q-td key="_delete" auto-width>
93
- <q-btn dense flat round icon="sym_o_delete"
94
- @click="onDelete(props.row.name)"></q-btn>
151
+ <q-tr
152
+ :props="props"
153
+ :class="{ 'bg-blue-1': obj._originalName === props.row.name }"
154
+ class="cursor-pointer"
155
+ @click="onSelectRow(props.row)"
156
+ >
157
+ <q-td key="_delete" auto-width @click.stop>
158
+ <q-btn dense flat round icon="sym_o_delete" color="negative"
159
+ @click="onDelete(props.row.name)">
160
+ <q-tooltip>{{ t("Delete") }}</q-tooltip>
161
+ </q-btn>
95
162
  </q-td>
96
- <q-td key="name">
97
- {{ props.row.name }}
163
+ <q-td key="name" style="max-width: 200px;">
164
+ <div class="text-weight-medium ellipsis">
165
+ {{ props.row.name }}
166
+ <q-tooltip v-if="props.row.name?.length > 25" anchor="top middle" self="bottom middle">
167
+ {{ props.row.name }}
168
+ </q-tooltip>
169
+ </div>
98
170
  </q-td>
99
- <q-td :key="language.value" v-for="language in app.languages" :props="props">
100
- <div class="text-pre-wrap">{{ props.row[language.value] }}</div>
101
- <q-popup-edit v-model="props.row[language.value]" v-slot="scope" buttons
102
- :title="language.name"
103
- @save="onUpdateTranslate($event, language.value, props.row.name)">
104
- <q-input v-model="scope.value" dense autofocus @keyup.enter="scope.set" />
105
- </q-popup-edit>
171
+ <q-td :key="language.value" v-for="language in app.languages" :props="props" style="max-width: 250px;">
172
+ <div class="text-grey-8 ellipsis-2-lines">
173
+ {{ props.row[language.value] || '-' }}
174
+ <q-tooltip v-if="props.row[language.value]?.length > 50" anchor="top middle" self="bottom middle" max-width="400px">
175
+ <div class="text-pre-wrap">{{ props.row[language.value] }}</div>
176
+ </q-tooltip>
177
+ </div>
106
178
  </q-td>
107
179
  </q-tr>
108
180
  </template>
109
181
  </q-table>
110
-
111
182
  </template>
112
183
  <template #after>
113
- <l-form :bordered="false" @save="onSave">
114
- <l-input label="Name" required v-model.trim="obj.name" clearable></l-input>
115
- <l-input v-for="language in app.languages" :label="language.name" v-model="obj[language.value]"
116
- clearable></l-input>
117
- </l-form>
184
+ <div class="q-pa-md">
185
+ <div class="row items-center q-mb-md">
186
+ <div class="text-h6">
187
+ {{ isEditMode ? t("Edit Translation") : t("Add Translation") }}
188
+ </div>
189
+ <q-space />
190
+ <q-btn
191
+ v-if="isEditMode"
192
+ flat
193
+ dense
194
+ icon="sym_o_add"
195
+ :label="t('New')"
196
+ color="primary"
197
+ @click="onClearForm"
198
+ >
199
+ <q-tooltip>{{ t("Clear form and add new") }}</q-tooltip>
200
+ </q-btn>
201
+ </div>
202
+ <l-form :bordered="false" @submit="onSave">
203
+ <l-input
204
+ label="Name"
205
+ required
206
+ v-model.trim="obj.name"
207
+ clearable
208
+ :disable="isEditMode"
209
+ :hint="isEditMode ? t('Name cannot be changed in edit mode') : ''"
210
+ />
211
+ <l-input
212
+ v-for="language in app.languages"
213
+ :key="language.value"
214
+ :label="language.name"
215
+ v-model="obj[language.value]"
216
+ type="textarea"
217
+ />
218
+ <template #buttons>
219
+ <q-btn
220
+ v-if="isEditMode"
221
+ flat
222
+ :label="t('Cancel')"
223
+ @click="onClearForm"
224
+ class="q-mr-sm"
225
+ />
226
+ <l-save-btn :label="isEditMode ? t('Update') : t('Save')" />
227
+ </template>
228
+ </l-form>
229
+ </div>
118
230
  </template>
119
231
  </q-splitter>
120
232
  </l-card>
121
233
  </l-page>
122
234
  </template>
235
+
236
+ <style scoped>
237
+ .ellipsis{white-space:nowrap}.ellipsis,.ellipsis-2-lines{overflow:hidden;text-overflow:ellipsis}.ellipsis-2-lines{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;word-break:break-word}
238
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostlink/nuxt-light",
3
- "version": "1.64.2",
3
+ "version": "1.65.0",
4
4
  "description": "HostLink Nuxt Light Framework",
5
5
  "repository": {
6
6
  "type": "git",
@@ -56,7 +56,7 @@
56
56
  "@nuxt/test-utils": "^3.17.2",
57
57
  "@types/google.accounts": "^0.0.18",
58
58
  "@types/node": "^24.10.1",
59
- "changelogen": "^0.5.7",
59
+ "changelogen": "^0.6.2",
60
60
  "eslint": "^9.39.1",
61
61
  "nuxt": "^4.2.2",
62
62
  "typescript": "^5.9.2",