Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 
 

305 linhas
13 KiB

  1. {% extends "../layout.html" %}
  2. {% block title %}{{ columnInfo.name if columnInfo else '人员列表' }}{% endblock %}
  3. {% block content %}
  4. <div id="personApp">
  5. <el-card shadow="never">
  6. <template #header>
  7. <div class="flex items-center justify-between">
  8. <div class="flex items-center gap-2">
  9. <span class="font-medium">{{ columnInfo.name if columnInfo else '人员列表' }}</span>
  10. <el-tag type="danger" size="small">人员列表</el-tag>
  11. </div>
  12. <el-button type="primary" @click="openDialog()">+ 新增人员</el-button>
  13. </div>
  14. </template>
  15. <!-- 筛选栏 -->
  16. <div class="flex items-center gap-4 mb-4">
  17. <el-input v-model="query.keyword" placeholder="搜索姓名..." style="width:160px;" clearable @keyup.enter="loadList"></el-input>
  18. <el-select v-model="query.category" placeholder="分类" style="width:140px;" clearable>
  19. <el-option v-for="cat in categories" :key="cat" :label="cat" :value="cat"></el-option>
  20. </el-select>
  21. <el-button type="primary" @click="loadList">搜索</el-button>
  22. <el-button @click="resetQuery">重置</el-button>
  23. </div>
  24. <!-- 卡片列表 -->
  25. <div class="person-grid" v-loading="loading">
  26. <div v-for="(item, index) in list" :key="item.id" class="person-card">
  27. <div class="person-avatar" :style="{ background: item.avatar ? 'transparent' : getColor(index) }">
  28. <img v-if="item.avatar" :src="item.avatar" :alt="item.name">
  29. <span v-else>${ getInitial(item.name) }</span>
  30. </div>
  31. <div class="person-body">
  32. <div class="person-name">${ item.name }</div>
  33. <div class="person-title">${ item.title }</div>
  34. <div class="person-category" v-if="item.category">
  35. <el-tag size="small" type="info">${ item.category }</el-tag>
  36. </div>
  37. <div class="person-desc" v-if="item.description">${ item.description }</div>
  38. <div class="person-actions">
  39. <span class="person-sort">排序: ${ item.sort }</span>
  40. <el-button type="primary" link size="small" @click="openDialog(item)">编辑</el-button>
  41. <el-button type="danger" link size="small" @click="deleteItem(item)">删除</el-button>
  42. </div>
  43. </div>
  44. </div>
  45. <!-- 新增卡片 -->
  46. <div class="person-card person-card-add" @click="openDialog()">
  47. <div class="add-inner">
  48. <el-icon :size="32"><Plus /></el-icon>
  49. <span>新增人员</span>
  50. </div>
  51. </div>
  52. </div>
  53. <!-- 分页 -->
  54. <div class="flex justify-end mt-4" v-if="total > pageSize">
  55. <el-pagination background layout="prev, pager, next" :total="total" :page-size="pageSize" v-model:current-page="page" @current-change="loadList"></el-pagination>
  56. </div>
  57. </el-card>
  58. <!-- 新增/编辑弹窗 -->
  59. <el-dialog v-model="dialogVisible" :title="dialogTitle" width="580px" destroy-on-close draggable top="5vh">
  60. <div class="dialog-scroll-body">
  61. <el-form :model="form" label-width="80px">
  62. <!-- 头像上传 -->
  63. <el-form-item label="头像">
  64. <div class="avatar-upload-wrap">
  65. <div class="avatar-upload" @click="triggerUpload">
  66. <img v-if="form.avatar" :src="form.avatar" class="avatar-preview">
  67. <template v-else>
  68. <div class="person-avatar-ph">
  69. <span class="person-avatar-plus">+</span>
  70. <span class="person-avatar-txt">上传<br>头像</span>
  71. </div>
  72. </template>
  73. </div>
  74. <input type="file" ref="fileInput" accept="image/*" style="display:none;" @change="handleUpload">
  75. <div class="avatar-tip">建议尺寸 200×200,支持 JPG/PNG</div>
  76. </div>
  77. </el-form-item>
  78. <el-form-item label="姓名" required>
  79. <el-input v-model="form.name" placeholder="请输入姓名"></el-input>
  80. </el-form-item>
  81. <div class="flex gap-4">
  82. <el-form-item label="职务" required class="flex-1">
  83. <el-input v-model="form.title" placeholder="如:理事长"></el-input>
  84. </el-form-item>
  85. <el-form-item label="分类" required class="flex-1">
  86. <el-select v-model="form.category" placeholder="选择或输入分类" filterable allow-create style="width:100%;">
  87. <el-option v-for="cat in categories" :key="cat" :label="cat" :value="cat"></el-option>
  88. </el-select>
  89. </el-form-item>
  90. </div>
  91. <el-form-item label="简介">
  92. <el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入人员简介"></el-input>
  93. </el-form-item>
  94. <div class="flex gap-4">
  95. <el-form-item label="排序" class="flex-1">
  96. <el-input-number v-model="form.sort" :min="1" style="width:100%;"></el-input-number>
  97. </el-form-item>
  98. <el-form-item label="状态" class="flex-1">
  99. <el-switch v-model="form.status" :active-value="1" :inactive-value="0"></el-switch>
  100. </el-form-item>
  101. </div>
  102. </el-form>
  103. </div>
  104. <template #footer>
  105. <el-button @click="dialogVisible = false">取消</el-button>
  106. <el-button type="primary" @click="saveItem" :loading="saving">保存</el-button>
  107. </template>
  108. </el-dialog>
  109. </div>
  110. <style>
  111. .person-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; }
  112. .person-card { background: #fff; border: 1px solid #eee; border-radius: 8px; padding: 16px; display: flex; gap: 14px; transition: .2s; align-items: flex-start; }
  113. .person-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,.08); }
  114. .person-avatar { width: 64px; height: 64px; border-radius: 50%; background: #ff7800; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 22px; font-weight: 600; flex-shrink: 0; overflow: hidden; }
  115. .person-avatar img { width: 100%; height: 100%; object-fit: cover; }
  116. .person-body { flex: 1; min-width: 0; }
  117. .person-name { font-size: 15px; font-weight: 600; color: #333; margin-bottom: 2px; }
  118. .person-title { font-size: 12px; color: #ff7800; margin-bottom: 4px; }
  119. .person-category { margin-bottom: 6px; }
  120. .person-desc { font-size: 12px; color: #909399; line-height: 1.6; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
  121. .person-actions { display: flex; align-items: center; gap: 8px; padding-top: 8px; border-top: 1px solid #f0f0f0; margin-top: 8px; }
  122. .person-sort { font-size: 11px; color: #c0c4cc; margin-right: auto; }
  123. .person-card-add { border: 2px dashed #ddd; display: flex; align-items: center; justify-content: center; cursor: pointer; min-height: 140px; }
  124. .person-card-add:hover { border-color: #ff7800; background: #fff7f0; }
  125. .add-inner { display: flex; flex-direction: column; align-items: center; gap: 8px; color: #bbb; font-size: 13px; }
  126. /* 弹窗滚动区域 */
  127. .dialog-scroll-body { max-height: 65vh; overflow-y: auto; overflow-x: hidden; }
  128. /* 头像上传 */
  129. .avatar-upload-wrap { display: flex; flex-direction: column; align-items: flex-start; }
  130. .avatar-upload { width: 80px; height: 80px; border-radius: 50%; border: 2px dashed #ddd; cursor: pointer; display: flex; align-items: center; justify-content: center; overflow: hidden; transition: .2s; background: #fafafa; }
  131. .avatar-upload:hover { border-color: #ff7800; background: #fff7f0; }
  132. .avatar-preview { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
  133. .person-avatar-ph { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
  134. .person-avatar-plus { font-size: 24px; color: #bbb; line-height: 1; }
  135. .person-avatar-txt { font-size: 11px; color: #bbb; line-height: 1.3; margin-top: 2px; }
  136. .avatar-tip { font-size: 12px; color: #999; margin-top: 8px; }
  137. </style>
  138. {% endblock %}
  139. {% block js %}
  140. <script>
  141. const col = '{{ col }}';
  142. const { createApp, ref, reactive, onMounted } = Vue;
  143. const avatarColors = ['#ff7800', '#1A3550', '#67c23a', '#e6a23c', '#909399', '#409eff', '#f56c6c'];
  144. const app = createApp({
  145. delimiters: ['${', '}'],
  146. setup() {
  147. const loading = ref(false);
  148. const list = ref([]);
  149. const total = ref(0);
  150. const page = ref(1);
  151. const pageSize = ref(50);
  152. const query = reactive({ keyword: '', category: '' });
  153. const categories = ref([]);
  154. const dialogVisible = ref(false);
  155. const dialogTitle = ref('新增人员');
  156. const saving = ref(false);
  157. const fileInput = ref(null);
  158. const defaultForm = { id: null, name: '', title: '', category: '', avatar: '', description: '', sort: 1, status: 1 };
  159. const form = reactive({ ...defaultForm });
  160. function getInitial(name) {
  161. return (name || '?').charAt(0);
  162. }
  163. function getColor(index) {
  164. return avatarColors[index % avatarColors.length];
  165. }
  166. async function loadCategories() {
  167. try {
  168. const res = await fetch('/admin/content/person/categories?col=' + col).then(r => r.json());
  169. if (res.code === 0) {
  170. categories.value = res.data || [];
  171. }
  172. } catch {}
  173. }
  174. async function loadList() {
  175. loading.value = true;
  176. try {
  177. const params = new URLSearchParams({ col, page: page.value, pageSize: pageSize.value, ...query });
  178. const res = await fetch('/admin/content/person/list?' + params).then(r => r.json());
  179. if (res.code === 0) {
  180. list.value = res.data.data || [];
  181. total.value = res.data.count || 0;
  182. }
  183. } finally { loading.value = false; }
  184. }
  185. function resetQuery() {
  186. query.keyword = '';
  187. query.category = '';
  188. page.value = 1;
  189. loadList();
  190. }
  191. function openDialog(item) {
  192. if (item) {
  193. dialogTitle.value = '编辑人员';
  194. Object.assign(form, {
  195. id: item.id, name: item.name, title: item.title || '',
  196. category: item.category || '', avatar: item.avatar || '',
  197. description: item.description || '', sort: item.sort || 1, status: item.status
  198. });
  199. } else {
  200. dialogTitle.value = '新增人员';
  201. Object.assign(form, { ...defaultForm, sort: list.value.length + 1 });
  202. }
  203. dialogVisible.value = true;
  204. }
  205. function triggerUpload() {
  206. fileInput.value?.click();
  207. }
  208. async function handleUpload(e) {
  209. const file = e.target.files[0];
  210. if (!file) return;
  211. const formData = new FormData();
  212. formData.append('file', file);
  213. try {
  214. const res = await fetch('/admin/upload', { method: 'POST', body: formData }).then(r => r.json());
  215. if (res.code === 0) {
  216. form.avatar = res.data.url;
  217. ElementPlus.ElMessage.success('上传成功');
  218. } else {
  219. ElementPlus.ElMessage.error(res.msg || '上传失败');
  220. }
  221. } catch (err) {
  222. ElementPlus.ElMessage.error('上传失败');
  223. }
  224. e.target.value = '';
  225. }
  226. async function saveItem() {
  227. if (!form.name.trim()) { ElementPlus.ElMessage.warning('请输入姓名'); return; }
  228. if (!form.title.trim()) { ElementPlus.ElMessage.warning('请输入职务'); return; }
  229. if (!form.category) { ElementPlus.ElMessage.warning('请选择分类'); return; }
  230. saving.value = true;
  231. try {
  232. const url = form.id ? '/admin/content/person/edit' : '/admin/content/person/add';
  233. const body = { ...form, col };
  234. const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(r => r.json());
  235. if (res.code === 0) {
  236. ElementPlus.ElMessage.success('保存成功');
  237. dialogVisible.value = false;
  238. loadList();
  239. loadCategories();
  240. } else {
  241. ElementPlus.ElMessage.error(res.msg || '保存失败');
  242. }
  243. } finally { saving.value = false; }
  244. }
  245. async function deleteItem(item) {
  246. try {
  247. await ElementPlus.ElMessageBox.confirm('确定删除"' + item.name + '"?', '提示', { type: 'warning' });
  248. const res = await fetch('/admin/content/person/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: item.id }) }).then(r => r.json());
  249. if (res.code === 0) {
  250. ElementPlus.ElMessage.success('删除成功');
  251. loadList();
  252. } else {
  253. ElementPlus.ElMessage.error(res.msg || '删除失败');
  254. }
  255. } catch {}
  256. }
  257. onMounted(() => {
  258. loadCategories();
  259. loadList();
  260. });
  261. return {
  262. loading, list, total, page, pageSize, query, categories,
  263. dialogVisible, dialogTitle, saving, form, fileInput,
  264. getInitial, getColor,
  265. loadList, resetQuery, openDialog, triggerUpload, handleUpload, saveItem, deleteItem
  266. };
  267. }
  268. });
  269. for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  270. app.component(key, component);
  271. }
  272. app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
  273. app.mount('#personApp');
  274. </script>
  275. {% endblock %}