QualityAggregateCrudPage.vue 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. <template>
  2. <AidopDemoShell :title="pageTitle" :subtitle="config.subtitle">
  3. <el-form :inline="true" :model="query" class="mb12" @submit.prevent>
  4. <el-form-item label="关键字">
  5. <el-input v-model="query.keyword" :placeholder="config.queryPlaceholder" clearable style="width: 260px" />
  6. </el-form-item>
  7. <el-form-item>
  8. <el-button type="primary" @click="loadList">查询</el-button>
  9. <el-button @click="resetQuery">重置</el-button>
  10. <el-button type="success" @click="openCreate">新增{{ config.entityLabel }}</el-button>
  11. </el-form-item>
  12. </el-form>
  13. <el-table :data="rows" v-loading="loading" border stripe>
  14. <el-table-column v-for="column in config.listColumns" :key="column.prop" :prop="column.prop" :label="column.label" :width="column.width" :min-width="column.minWidth" show-overflow-tooltip />
  15. <el-table-column label="操作" width="180" fixed="right" align="center">
  16. <template #default="{ row }">
  17. <el-button link type="primary" @click="openEdit(row)">编辑</el-button>
  18. <el-button link type="danger" @click="onDelete(row)">删除</el-button>
  19. </template>
  20. </el-table-column>
  21. </el-table>
  22. <div class="pager">
  23. <el-pagination
  24. v-model:current-page="query.page"
  25. v-model:page-size="query.pageSize"
  26. :total="total"
  27. :page-sizes="[20, 50, 100]"
  28. layout="total, sizes, prev, pager, next"
  29. @current-change="loadList"
  30. @size-change="loadList"
  31. />
  32. </div>
  33. <el-dialog v-model="dialogVisible" :title="dialogTitle" width="1200px" destroy-on-close @closed="resetForm">
  34. <el-form ref="formRef" :model="master" :rules="rules" label-width="120px" class="head-grid">
  35. <el-form-item v-for="field in config.headFields" :key="field.prop" :label="field.label" :prop="field.prop">
  36. <el-input-number
  37. v-if="field.type === 'number'"
  38. v-model="master[field.prop]"
  39. controls-position="right"
  40. style="width: 100%"
  41. />
  42. <el-input
  43. v-else
  44. v-model="master[field.prop]"
  45. :type="field.type === 'textarea' ? 'textarea' : 'text'"
  46. :rows="field.type === 'textarea' ? 3 : undefined"
  47. />
  48. </el-form-item>
  49. </el-form>
  50. <div class="detail-toolbar">
  51. <div class="detail-title">明细</div>
  52. <el-button type="primary" plain @click="addItem">新增行</el-button>
  53. </div>
  54. <el-table :data="items" border stripe class="detail-table">
  55. <el-table-column type="index" label="#" width="50" />
  56. <el-table-column v-for="column in config.detailColumns" :key="column.prop" :label="column.label" :width="column.type === 'number' ? 140 : undefined" :min-width="column.type === 'number' ? undefined : 150">
  57. <template #default="{ row }">
  58. <el-input-number
  59. v-if="column.type === 'number'"
  60. v-model="row[column.prop]"
  61. controls-position="right"
  62. style="width: 100%"
  63. />
  64. <el-input v-else v-model="row[column.prop]" />
  65. </template>
  66. </el-table-column>
  67. <el-table-column label="操作" width="80" fixed="right" align="center">
  68. <template #default="{ $index }">
  69. <el-button link type="danger" @click="removeItem($index)">删除</el-button>
  70. </template>
  71. </el-table-column>
  72. </el-table>
  73. <template #footer>
  74. <el-button @click="dialogVisible = false">取消</el-button>
  75. <el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
  76. </template>
  77. </el-dialog>
  78. </AidopDemoShell>
  79. </template>
  80. <script setup lang="ts">
  81. import { computed, onMounted, reactive, ref } from 'vue';
  82. import { useRoute } from 'vue-router';
  83. import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
  84. import AidopDemoShell from '../../../components/AidopDemoShell.vue';
  85. import type { QualityAggregatePageConfig } from '../qualityConfigs';
  86. const props = defineProps<{ config: QualityAggregatePageConfig }>();
  87. const route = useRoute();
  88. const pageTitle = computed(() => (route.meta?.title as string) || props.config.entityLabel);
  89. const query = reactive({ keyword: '', page: 1, pageSize: 20 });
  90. const rows = ref<Record<string, any>[]>([]);
  91. const total = ref(0);
  92. const loading = ref(false);
  93. const dialogVisible = ref(false);
  94. const dialogTitle = ref('');
  95. const editingId = ref<number | null>(null);
  96. const saving = ref(false);
  97. const formRef = ref<FormInstance>();
  98. const master = reactive<Record<string, any>>({});
  99. const items = ref<Record<string, any>[]>([]);
  100. const rules = computed<FormRules>(() =>
  101. props.config.headFields.reduce<FormRules>((acc, field) => {
  102. if (field.required) acc[field.prop] = [{ required: true, message: `请填写${field.label}`, trigger: 'blur' }];
  103. return acc;
  104. }, {})
  105. );
  106. function resetForm() {
  107. editingId.value = null;
  108. Object.keys(master).forEach((key) => delete master[key]);
  109. Object.assign(master, structuredClone(props.config.initialMaster));
  110. items.value = [];
  111. formRef.value?.clearValidate();
  112. }
  113. async function loadList() {
  114. loading.value = true;
  115. try {
  116. const data = await props.config.api.list({
  117. keyword: query.keyword || undefined,
  118. page: query.page,
  119. pageSize: query.pageSize,
  120. });
  121. rows.value = data.list ?? [];
  122. total.value = data.total ?? 0;
  123. } catch {
  124. rows.value = [];
  125. total.value = 0;
  126. } finally {
  127. loading.value = false;
  128. }
  129. }
  130. function resetQuery() {
  131. query.keyword = '';
  132. query.page = 1;
  133. void loadList();
  134. }
  135. function addItem() {
  136. items.value.push(props.config.createEmptyItem());
  137. }
  138. function removeItem(index: number) {
  139. items.value.splice(index, 1);
  140. }
  141. function openCreate() {
  142. resetForm();
  143. dialogTitle.value = `新增${props.config.entityLabel}`;
  144. addItem();
  145. dialogVisible.value = true;
  146. }
  147. async function openEdit(row: Record<string, any>) {
  148. resetForm();
  149. editingId.value = Number(row.id);
  150. dialogTitle.value = `编辑${props.config.entityLabel}`;
  151. const detail = await props.config.api.get(editingId.value);
  152. Object.assign(master, structuredClone(detail.master ?? {}));
  153. items.value = structuredClone(detail.items ?? []);
  154. dialogVisible.value = true;
  155. }
  156. async function submitForm() {
  157. await formRef.value?.validate();
  158. saving.value = true;
  159. try {
  160. const payload = { ...master, items: items.value.map((item) => ({ ...item })) };
  161. if (editingId.value) {
  162. await props.config.api.update(editingId.value, payload);
  163. ElMessage.success('已保存');
  164. } else {
  165. await props.config.api.create(payload);
  166. ElMessage.success('已创建');
  167. }
  168. dialogVisible.value = false;
  169. await loadList();
  170. } finally {
  171. saving.value = false;
  172. }
  173. }
  174. function onDelete(row: Record<string, any>) {
  175. ElMessageBox.confirm(`确定删除${props.config.entityLabel}「${row[props.config.listColumns[0]?.prop] ?? row.id}」?`, '确认', { type: 'warning' })
  176. .then(async () => {
  177. await props.config.api.delete(Number(row.id));
  178. ElMessage.success('已删除');
  179. await loadList();
  180. })
  181. .catch(() => {});
  182. }
  183. onMounted(() => {
  184. resetForm();
  185. void loadList();
  186. });
  187. </script>
  188. <style scoped lang="scss">
  189. @import '/@/views/aidop/styles/aidop-demo.scss';
  190. .mb12 {
  191. margin-bottom: 12px;
  192. }
  193. .pager {
  194. margin-top: 12px;
  195. display: flex;
  196. justify-content: flex-end;
  197. }
  198. .head-grid {
  199. display: grid;
  200. grid-template-columns: repeat(2, minmax(0, 1fr));
  201. column-gap: 12px;
  202. }
  203. .detail-toolbar {
  204. margin: 8px 0 12px;
  205. display: flex;
  206. align-items: center;
  207. justify-content: space-between;
  208. }
  209. .detail-title {
  210. font-size: 14px;
  211. font-weight: 600;
  212. }
  213. .detail-table {
  214. margin-bottom: 8px;
  215. }
  216. </style>