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.
 
 
 
 
 
 

261 linhas
11 KiB

  1. {% extends "../layout.html" %}
  2. {% block title %}{{ columnInfo.name if columnInfo else '图片列表' }}{% endblock %}
  3. {% block content %}
  4. <div id="imageApp">
  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="success" 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:180px;" clearable @keyup.enter="loadList"></el-input>
  18. <el-select v-model="query.status" placeholder="状态" style="width:100px;" clearable>
  19. <el-option label="启用" :value="1"></el-option>
  20. <el-option label="禁用" :value="0"></el-option>
  21. </el-select>
  22. <el-button type="primary" @click="loadList">搜索</el-button>
  23. <el-button @click="resetQuery">重置</el-button>
  24. </div>
  25. <!-- 卡片列表 -->
  26. <div class="image-grid" v-loading="loading">
  27. <div v-for="item in list" :key="item.id" class="image-card">
  28. <div class="image-thumb">
  29. <img :src="item.image || '/static/images/placeholder.png'" :alt="item.title">
  30. <span class="image-sort">${ item.sort }</span>
  31. </div>
  32. <div class="image-info">
  33. <div class="image-title">${ item.title }</div>
  34. <div class="image-meta">
  35. <el-tag :type="item.status ? 'success' : 'info'" size="small">${ item.status ? '启用' : '禁用' }</el-tag>
  36. <span class="image-date">${ item.create_time?.slice(0,10) }</span>
  37. </div>
  38. </div>
  39. <div class="image-actions">
  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. <!-- 新增卡片 -->
  45. <div class="image-card image-card-add" @click="openDialog()">
  46. <div class="add-inner">
  47. <el-icon :size="36"><Plus /></el-icon>
  48. <span>新增图片</span>
  49. </div>
  50. </div>
  51. </div>
  52. <!-- 分页 -->
  53. <div class="flex justify-end mt-4" v-if="total > pageSize">
  54. <el-pagination background layout="prev, pager, next" :total="total" :page-size="pageSize" v-model:current-page="page" @current-change="loadList"></el-pagination>
  55. </div>
  56. </el-card>
  57. <!-- 新增/编辑弹窗 -->
  58. <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" destroy-on-close draggable top="5vh">
  59. <div class="dialog-scroll-body">
  60. <el-form :model="form" label-width="80px">
  61. <!-- 图片上传 -->
  62. <el-form-item label="图片" required>
  63. <div class="img-upload-wrap">
  64. <div class="img-upload-area" @click="triggerUpload">
  65. <img v-if="form.image" :src="form.image" class="img-preview">
  66. <div v-else class="img-placeholder">
  67. <el-icon :size="32"><Plus /></el-icon>
  68. <span>点击上传图片</span>
  69. </div>
  70. </div>
  71. <input type="file" ref="fileInput" accept="image/*" style="display:none;" @change="handleUpload">
  72. <div class="img-tip">支持 JPG/PNG/GIF,建议宽度不小于 800px</div>
  73. </div>
  74. </el-form-item>
  75. <el-form-item label="标题" required>
  76. <el-input v-model="form.title" placeholder="请输入标题"></el-input>
  77. </el-form-item>
  78. <el-form-item label="链接地址">
  79. <el-input v-model="form.link" placeholder="点击图片跳转的链接(可选)"></el-input>
  80. </el-form-item>
  81. <div class="flex gap-4">
  82. <el-form-item label="排序" class="flex-1">
  83. <el-input-number v-model="form.sort" :min="1" style="width:100%;"></el-input-number>
  84. </el-form-item>
  85. <el-form-item label="状态" class="flex-1">
  86. <el-switch v-model="form.status" :active-value="1" :inactive-value="0"></el-switch>
  87. </el-form-item>
  88. </div>
  89. </el-form>
  90. </div>
  91. <template #footer>
  92. <el-button @click="dialogVisible = false">取消</el-button>
  93. <el-button type="primary" @click="saveItem" :loading="saving">保存</el-button>
  94. </template>
  95. </el-dialog>
  96. </div>
  97. <style>
  98. .image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; margin-top: 16px; }
  99. .image-card { background: #fff; border: 1px solid #eee; border-radius: 8px; overflow: hidden; transition: .2s; }
  100. .image-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,.08); transform: translateY(-2px); }
  101. .image-thumb { position: relative; aspect-ratio: 4/3; background: #f5f5f5; overflow: hidden; }
  102. .image-thumb img { width: 100%; height: 100%; object-fit: contain; background: #fafafa; }
  103. .image-sort { position: absolute; top: 6px; left: 6px; background: #ff7800; color: #fff; font-size: 11px; font-weight: 600; width: 22px; height: 22px; border-radius: 4px; display: flex; align-items: center; justify-content: center; }
  104. .image-info { padding: 10px 12px 6px; }
  105. .image-title { font-size: 13px; font-weight: 500; color: #333; margin-bottom: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  106. .image-meta { display: flex; align-items: center; gap: 8px; }
  107. .image-date { font-size: 11px; color: #bbb; }
  108. .image-actions { display: flex; align-items: center; gap: 4px; padding: 6px 12px 10px; border-top: 1px solid #f0f0f0; }
  109. .image-card-add { border: 2px dashed #ddd; display: flex; align-items: center; justify-content: center; cursor: pointer; min-height: 180px; }
  110. .image-card-add:hover { border-color: #ff7800; background: #fff7f0; }
  111. .add-inner { display: flex; flex-direction: column; align-items: center; gap: 8px; color: #bbb; font-size: 13px; }
  112. /* 弹窗滚动区域 */
  113. .dialog-scroll-body { max-height: 65vh; overflow-y: auto; overflow-x: hidden; }
  114. /* 图片上传 */
  115. .img-upload-wrap { width: 100%; max-width: 280px; }
  116. .img-upload-area { width: 100%; aspect-ratio: 4/3; border: 2px dashed #ddd; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; overflow: hidden; transition: .2s; background: #fafafa; }
  117. .img-upload-area:hover { border-color: #ff7800; background: #fff7f0; }
  118. .img-preview { width: 100%; height: 100%; object-fit: contain; }
  119. .img-placeholder { display: flex; flex-direction: column; align-items: center; gap: 6px; color: #bbb; font-size: 12px; }
  120. .img-tip { font-size: 12px; color: #999; margin-top: 8px; }
  121. </style>
  122. {% endblock %}
  123. {% block js %}
  124. <script>
  125. const col = '{{ col }}';
  126. const { createApp, ref, reactive, onMounted } = Vue;
  127. const app = createApp({
  128. delimiters: ['${', '}'],
  129. setup() {
  130. const loading = ref(false);
  131. const list = ref([]);
  132. const total = ref(0);
  133. const page = ref(1);
  134. const pageSize = ref(50);
  135. const query = reactive({ keyword: '', status: '' });
  136. const dialogVisible = ref(false);
  137. const dialogTitle = ref('新增图片');
  138. const saving = ref(false);
  139. const fileInput = ref(null);
  140. const defaultForm = { id: null, title: '', image: '', link: '', sort: 1, status: 1 };
  141. const form = reactive({ ...defaultForm });
  142. async function loadList() {
  143. loading.value = true;
  144. try {
  145. const params = new URLSearchParams({ col, page: page.value, pageSize: pageSize.value, ...query });
  146. const res = await fetch('/admin/content/image/list?' + params).then(r => r.json());
  147. if (res.code === 0) {
  148. list.value = res.data.data || [];
  149. total.value = res.data.count || 0;
  150. }
  151. } finally { loading.value = false; }
  152. }
  153. function resetQuery() {
  154. query.keyword = '';
  155. query.status = '';
  156. page.value = 1;
  157. loadList();
  158. }
  159. function openDialog(item) {
  160. if (item) {
  161. dialogTitle.value = '编辑图片';
  162. Object.assign(form, {
  163. id: item.id, title: item.title, image: item.image,
  164. link: item.link || '', sort: item.sort || 1, status: item.status
  165. });
  166. } else {
  167. dialogTitle.value = '新增图片';
  168. Object.assign(form, { ...defaultForm, sort: list.value.length + 1 });
  169. }
  170. dialogVisible.value = true;
  171. }
  172. function triggerUpload() {
  173. fileInput.value?.click();
  174. }
  175. async function handleUpload(e) {
  176. const file = e.target.files[0];
  177. if (!file) return;
  178. const formData = new FormData();
  179. formData.append('file', file);
  180. try {
  181. const res = await fetch('/admin/upload', { method: 'POST', body: formData }).then(r => r.json());
  182. if (res.code === 0) {
  183. form.image = res.data.url;
  184. ElementPlus.ElMessage.success('上传成功');
  185. } else {
  186. ElementPlus.ElMessage.error(res.msg || '上传失败');
  187. }
  188. } catch (err) {
  189. ElementPlus.ElMessage.error('上传失败');
  190. }
  191. e.target.value = '';
  192. }
  193. async function saveItem() {
  194. if (!form.title.trim()) { ElementPlus.ElMessage.warning('请输入标题'); return; }
  195. if (!form.image) { ElementPlus.ElMessage.warning('请上传图片'); return; }
  196. saving.value = true;
  197. try {
  198. const url = form.id ? '/admin/content/image/edit' : '/admin/content/image/add';
  199. const body = { ...form, col };
  200. const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(r => r.json());
  201. if (res.code === 0) {
  202. ElementPlus.ElMessage.success('保存成功');
  203. dialogVisible.value = false;
  204. loadList();
  205. } else {
  206. ElementPlus.ElMessage.error(res.msg || '保存失败');
  207. }
  208. } finally { saving.value = false; }
  209. }
  210. async function deleteItem(item) {
  211. try {
  212. await ElementPlus.ElMessageBox.confirm('确定删除"' + item.title + '"?', '提示', { type: 'warning' });
  213. const res = await fetch('/admin/content/image/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: item.id }) }).then(r => r.json());
  214. if (res.code === 0) {
  215. ElementPlus.ElMessage.success('删除成功');
  216. loadList();
  217. } else {
  218. ElementPlus.ElMessage.error(res.msg || '删除失败');
  219. }
  220. } catch {}
  221. }
  222. onMounted(() => loadList());
  223. return {
  224. loading, list, total, page, pageSize, query,
  225. dialogVisible, dialogTitle, saving, form, fileInput,
  226. loadList, resetQuery, openDialog, triggerUpload, handleUpload, saveItem, deleteItem
  227. };
  228. }
  229. });
  230. for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  231. app.component(key, component);
  232. }
  233. app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
  234. app.mount('#imageApp');
  235. </script>
  236. {% endblock %}