Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 
 
 

370 rindas
18 KiB

  1. {% extends "../layout.html" %}
  2. {% block title %}{{ columnInfo.name if columnInfo else 'Banner管理' }}{% endblock %}
  3. {% block content %}
  4. <div id="bannerApp">
  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 'Banner管理' }}</span>
  10. <el-tag type="warning" size="small">轮播图</el-tag>
  11. </div>
  12. <el-button type="primary" @click="openDialog()">+ 新增Banner</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" class="ml-2">重置</el-button>
  24. </div>
  25. <!-- 卡片列表 -->
  26. <div class="banner-grid" v-loading="loading">
  27. <div v-for="item in list" :key="item.id" class="banner-card">
  28. <div class="banner-thumb">
  29. <img :src="item.image || '/static/images/placeholder.png'" :alt="item.title">
  30. <span class="banner-sort">${ item.sort }</span>
  31. <span v-if="item.glass" class="banner-glass">毛玻璃</span>
  32. <!-- 文字叠加预览 -->
  33. <div class="banner-overlay">
  34. <div class="banner-overlay-title">${ item.title }</div>
  35. <div v-if="item.subtitle" class="banner-overlay-sub">${ item.subtitle }</div>
  36. <div class="banner-overlay-btns">
  37. <span v-if="item.btn1_show && item.btn1_text">${ item.btn1_text }</span>
  38. <span v-if="item.btn2_show && item.btn2_text">${ item.btn2_text }</span>
  39. </div>
  40. </div>
  41. </div>
  42. <div class="banner-info">
  43. <div class="banner-title">${ item.title }</div>
  44. <div v-if="item.subtitle" class="banner-subtitle">${ item.subtitle }</div>
  45. <div class="banner-meta">
  46. <el-tag :type="item.status ? 'success' : 'info'" size="small">${ item.status ? '启用' : '禁用' }</el-tag>
  47. <span class="banner-date">${ item.create_time?.slice(0,10) }</span>
  48. </div>
  49. </div>
  50. <div class="banner-actions">
  51. <el-button type="primary" link size="small" @click="openDialog(item)">编辑</el-button>
  52. <el-button type="danger" link size="small" @click="deleteItem(item)">删除</el-button>
  53. </div>
  54. </div>
  55. <!-- 新增卡片 -->
  56. <div class="banner-card banner-card-add" @click="openDialog()">
  57. <div class="add-inner">
  58. <el-icon :size="36"><Plus /></el-icon>
  59. <span>新增Banner</span>
  60. </div>
  61. </div>
  62. </div>
  63. <!-- 分页 -->
  64. <div class="flex justify-end mt-4" v-if="total > pageSize">
  65. <el-pagination background layout="prev, pager, next" :total="total" :page-size="pageSize" v-model:current-page="page" @current-change="loadList"></el-pagination>
  66. </div>
  67. </el-card>
  68. <!-- 新增/编辑弹窗 -->
  69. <el-dialog v-model="dialogVisible" :title="dialogTitle" width="800px" destroy-on-close draggable top="5vh">
  70. <div class="dialog-scroll-body">
  71. <el-form :model="form" label-width="100px">
  72. <!-- 图片上传 -->
  73. <el-form-item label="Banner图片" required>
  74. <div class="img-upload-wrap">
  75. <div class="img-upload-area" @click="triggerUpload">
  76. <img v-if="form.image" :src="form.image" class="img-preview">
  77. <div v-else class="img-placeholder">
  78. <el-icon :size="32"><Plus /></el-icon>
  79. <span>点击上传图片</span>
  80. </div>
  81. </div>
  82. <input type="file" ref="fileInput" accept="image/*" style="display:none;" @change="handleUpload">
  83. <div class="img-tip">建议尺寸 1920×800,支持 JPG/PNG</div>
  84. </div>
  85. </el-form-item>
  86. <!-- 基本信息 -->
  87. <el-form-item label="主标题" required>
  88. <el-input v-model="form.title" placeholder="如:也许因为您的一次帮助"></el-input>
  89. </el-form-item>
  90. <el-form-item label="副标题">
  91. <el-input v-model="form.subtitle" placeholder="如:天真的笑容将再次回到他的脸上"></el-input>
  92. </el-form-item>
  93. <el-form-item label="描述文字">
  94. <el-input v-model="form.description" type="textarea" :rows="2" placeholder="Banner描述文字"></el-input>
  95. </el-form-item>
  96. <!-- 按钮配置 -->
  97. <el-divider content-position="left">按钮配置</el-divider>
  98. <div class="flex gap-3 items-end mb-4">
  99. <el-form-item label="按钮1文字" class="mb-0" style="flex:2;">
  100. <el-input v-model="form.btn1_text" placeholder="了解更多"></el-input>
  101. </el-form-item>
  102. <el-form-item label="链接" class="mb-0" style="flex:2;">
  103. <el-input v-model="form.btn1_link" placeholder="#"></el-input>
  104. </el-form-item>
  105. <el-form-item label="显示" class="mb-0" style="flex:1;">
  106. <el-switch v-model="form.btn1_show" :active-value="1" :inactive-value="0"></el-switch>
  107. </el-form-item>
  108. </div>
  109. <div class="flex gap-3 items-end">
  110. <el-form-item label="按钮2文字" class="mb-0" style="flex:2;">
  111. <el-input v-model="form.btn2_text" placeholder="我要捐赠"></el-input>
  112. </el-form-item>
  113. <el-form-item label="链接" class="mb-0" style="flex:2;">
  114. <el-input v-model="form.btn2_link" placeholder="#"></el-input>
  115. </el-form-item>
  116. <el-form-item label="显示" class="mb-0" style="flex:1;">
  117. <el-switch v-model="form.btn2_show" :active-value="1" :inactive-value="0"></el-switch>
  118. </el-form-item>
  119. </div>
  120. <!-- 样式设置 -->
  121. <el-divider content-position="left">样式设置</el-divider>
  122. <el-form-item label="文字位置">
  123. <el-radio-group v-model="form.position">
  124. <el-radio value="left">左侧</el-radio>
  125. <el-radio value="center">居中</el-radio>
  126. <el-radio value="right">右侧</el-radio>
  127. </el-radio-group>
  128. </el-form-item>
  129. <el-form-item label="毛玻璃效果">
  130. <el-switch v-model="form.glass" :active-value="1" :inactive-value="0"></el-switch>
  131. <span class="ml-2 text-gray-400 text-sm">启用后文字区域会有半透明模糊背景</span>
  132. </el-form-item>
  133. <div class="flex gap-4">
  134. <el-form-item label="排序" class="flex-1">
  135. <el-input-number v-model="form.sort" :min="1" style="width:100%;"></el-input-number>
  136. </el-form-item>
  137. <el-form-item label="状态" class="flex-1">
  138. <el-switch v-model="form.status" :active-value="1" :inactive-value="0"></el-switch>
  139. </el-form-item>
  140. </div>
  141. <!-- 预览区域 -->
  142. <el-divider content-position="left">前台预览</el-divider>
  143. <div class="banner-preview" :class="'pos-' + form.position">
  144. <img :src="form.image || '/static/images/placeholder.png'" class="preview-bg">
  145. <div class="preview-overlay"></div>
  146. <div class="preview-content" :class="{ 'glass-on': form.glass }">
  147. <div class="preview-title">${ form.title || '标题文字' }${ form.subtitle ? '<br>' + form.subtitle : '' }</div>
  148. <div v-if="form.description" class="preview-desc">${ form.description }</div>
  149. <div class="preview-btns">
  150. <span v-if="form.btn1_show && form.btn1_text" class="pv-btn pv-btn-w">${ form.btn1_text }</span>
  151. <span v-if="form.btn2_show && form.btn2_text" class="pv-btn pv-btn-o">${ form.btn2_text }</span>
  152. </div>
  153. </div>
  154. </div>
  155. </el-form>
  156. </div>
  157. <template #footer>
  158. <el-button @click="dialogVisible = false">取消</el-button>
  159. <el-button type="primary" @click="saveItem" :loading="saving">保存</el-button>
  160. </template>
  161. </el-dialog>
  162. </div>
  163. <style>
  164. .banner-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; margin-top: 16px; }
  165. .banner-card { background: #fff; border: 1px solid #eee; border-radius: 8px; overflow: hidden; transition: .2s; }
  166. .banner-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,.08); transform: translateY(-2px); }
  167. .banner-thumb { position: relative; aspect-ratio: 16/9; background: #f5f5f5; overflow: hidden; }
  168. .banner-thumb img { width: 100%; height: 100%; object-fit: cover; }
  169. .banner-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; }
  170. .banner-glass { position: absolute; top: 6px; right: 6px; background: rgba(255,255,255,.2); backdrop-filter: blur(4px); padding: 1px 6px; border-radius: 3px; font-size: 9px; color: #fff; border: 1px solid rgba(255,255,255,.3); }
  171. .banner-overlay { position: absolute; bottom: 0; left: 0; right: 0; padding: 8px 10px; background: linear-gradient(transparent, rgba(0,0,0,.7)); color: #fff; }
  172. .banner-overlay-title { font-size: 12px; font-weight: 600; line-height: 1.3; text-shadow: 0 1px 2px rgba(0,0,0,.5); }
  173. .banner-overlay-sub { font-size: 10px; opacity: .85; margin-top: 2px; }
  174. .banner-overlay-btns { display: flex; gap: 4px; margin-top: 4px; }
  175. .banner-overlay-btns span { font-size: 9px; padding: 1px 6px; border-radius: 2px; border: 1px solid rgba(255,255,255,.6); color: rgba(255,255,255,.9); }
  176. .banner-info { padding: 10px 12px 6px; }
  177. .banner-title { font-size: 13px; font-weight: 500; color: #333; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  178. .banner-subtitle { font-size: 11px; color: #999; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  179. .banner-meta { display: flex; align-items: center; gap: 8px; }
  180. .banner-date { font-size: 11px; color: #bbb; }
  181. .banner-actions { display: flex; align-items: center; gap: 4px; padding: 6px 12px 10px; border-top: 1px solid #f0f0f0; }
  182. .banner-card-add { border: 2px dashed #ddd; display: flex; align-items: center; justify-content: center; cursor: pointer; min-height: 200px; }
  183. .banner-card-add:hover { border-color: #ff7800; background: #fff7f0; }
  184. .add-inner { display: flex; flex-direction: column; align-items: center; gap: 8px; color: #bbb; font-size: 13px; }
  185. /* 弹窗滚动区域 */
  186. .dialog-scroll-body { max-height: 65vh; overflow-y: auto; overflow-x: hidden; }
  187. /* 图片上传 */
  188. .img-upload-wrap { width: 100%; max-width: 360px; }
  189. .img-upload-area { width: 100%; aspect-ratio: 16/9; border: 2px dashed #ddd; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; overflow: hidden; transition: .2s; background: #fafafa; }
  190. .img-upload-area:hover { border-color: #ff7800; background: #fff7f0; }
  191. .img-preview { width: 100%; height: 100%; object-fit: cover; }
  192. .img-placeholder { display: flex; flex-direction: column; align-items: center; gap: 6px; color: #bbb; font-size: 12px; }
  193. .img-tip { font-size: 12px; color: #999; margin-top: 8px; }
  194. /* 预览区域 */
  195. .banner-preview { width: 100%; aspect-ratio: 16/7; border-radius: 8px; overflow: hidden; position: relative; background: #222; }
  196. .preview-bg { width: 100%; height: 100%; object-fit: cover; position: absolute; inset: 0; }
  197. .preview-overlay { position: absolute; inset: 0; background: rgba(0,0,0,.35); }
  198. .preview-content { position: absolute; top: 50%; transform: translateY(-50%); padding: 16px 24px; max-width: 60%; color: #fff; z-index: 2; }
  199. .pos-left .preview-content { left: 24px; }
  200. .pos-center .preview-content { left: 50%; transform: translate(-50%, -50%); text-align: center; max-width: 70%; }
  201. .pos-right .preview-content { right: 24px; text-align: right; }
  202. .preview-content.glass-on { background: rgba(255,255,255,.08); backdrop-filter: blur(12px); border-radius: 8px; border: 1px solid rgba(255,255,255,.12); padding: 20px 28px; }
  203. .preview-title { font-size: 16px; font-weight: 700; line-height: 1.4; margin-bottom: 6px; }
  204. .preview-desc { font-size: 11px; color: rgba(255,255,255,.75); line-height: 1.5; margin-bottom: 10px; }
  205. .preview-btns { display: flex; gap: 8px; }
  206. .pv-btn { padding: 4px 14px; border-radius: 4px; font-size: 11px; }
  207. .pv-btn-w { background: #fff; color: #333; }
  208. .pv-btn-o { border: 1px solid rgba(255,255,255,.7); color: #fff; background: transparent; }
  209. .pos-center .preview-btns { justify-content: center; }
  210. .pos-right .preview-btns { justify-content: flex-end; }
  211. </style>
  212. {% endblock %}
  213. {% block js %}
  214. <script>
  215. const col = '{{ col }}';
  216. const { createApp, ref, reactive, onMounted } = Vue;
  217. const app = createApp({
  218. delimiters: ['${', '}'],
  219. setup() {
  220. const loading = ref(false);
  221. const list = ref([]);
  222. const total = ref(0);
  223. const page = ref(1);
  224. const pageSize = ref(20);
  225. const query = reactive({ keyword: '', status: '' });
  226. const dialogVisible = ref(false);
  227. const dialogTitle = ref('新增Banner');
  228. const activeTab = ref('basic');
  229. const saving = ref(false);
  230. const fileInput = ref(null);
  231. const defaultForm = {
  232. id: null, title: '', subtitle: '', description: '', image: '',
  233. btn1_text: '了解更多', btn1_link: '#', btn1_show: 1,
  234. btn2_text: '我要捐赠', btn2_link: '#', btn2_show: 1,
  235. position: 'left', glass: 1, sort: 1, status: 1
  236. };
  237. const form = reactive({ ...defaultForm });
  238. async function loadList() {
  239. loading.value = true;
  240. try {
  241. const params = new URLSearchParams({ col, page: page.value, pageSize: pageSize.value, ...query });
  242. const res = await fetch('/admin/content/banner/list?' + params).then(r => r.json());
  243. if (res.code === 0) {
  244. list.value = res.data.data || [];
  245. total.value = res.data.count || 0;
  246. }
  247. } finally { loading.value = false; }
  248. }
  249. function resetQuery() {
  250. query.keyword = '';
  251. query.status = '';
  252. page.value = 1;
  253. loadList();
  254. }
  255. function openDialog(item) {
  256. activeTab.value = 'basic';
  257. if (item) {
  258. dialogTitle.value = '编辑Banner';
  259. Object.assign(form, {
  260. id: item.id, title: item.title, subtitle: item.subtitle || '', description: item.description || '',
  261. image: item.image, btn1_text: item.btn1_text || '', btn1_link: item.btn1_link || '',
  262. btn1_show: item.btn1_show, btn2_text: item.btn2_text || '', btn2_link: item.btn2_link || '',
  263. btn2_show: item.btn2_show, position: item.position || 'left', glass: item.glass,
  264. sort: item.sort || 1, status: item.status
  265. });
  266. } else {
  267. dialogTitle.value = '新增Banner';
  268. Object.assign(form, { ...defaultForm, sort: list.value.length + 1 });
  269. }
  270. dialogVisible.value = true;
  271. }
  272. function triggerUpload() {
  273. fileInput.value?.click();
  274. }
  275. async function handleUpload(e) {
  276. const file = e.target.files[0];
  277. if (!file) return;
  278. const formData = new FormData();
  279. formData.append('file', file);
  280. try {
  281. const res = await fetch('/admin/upload', { method: 'POST', body: formData }).then(r => r.json());
  282. if (res.code === 0) {
  283. form.image = res.data.url;
  284. ElementPlus.ElMessage.success('上传成功');
  285. } else {
  286. ElementPlus.ElMessage.error(res.msg || '上传失败');
  287. }
  288. } catch (err) {
  289. ElementPlus.ElMessage.error('上传失败');
  290. }
  291. e.target.value = '';
  292. }
  293. function refreshPreview() {
  294. // 触发Vue响应式更新
  295. }
  296. async function saveItem() {
  297. if (!form.title.trim()) { ElementPlus.ElMessage.warning('请输入标题'); return; }
  298. if (!form.image) { ElementPlus.ElMessage.warning('请上传图片'); return; }
  299. saving.value = true;
  300. try {
  301. const url = form.id ? '/admin/content/banner/edit' : '/admin/content/banner/add';
  302. const body = { ...form, col };
  303. const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(r => r.json());
  304. if (res.code === 0) {
  305. ElementPlus.ElMessage.success('保存成功');
  306. dialogVisible.value = false;
  307. loadList();
  308. } else {
  309. ElementPlus.ElMessage.error(res.msg || '保存失败');
  310. }
  311. } finally { saving.value = false; }
  312. }
  313. async function deleteItem(item) {
  314. try {
  315. await ElementPlus.ElMessageBox.confirm('确定删除"' + item.title + '"?', '提示', { type: 'warning' });
  316. const res = await fetch('/admin/content/banner/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: item.id }) }).then(r => r.json());
  317. if (res.code === 0) {
  318. ElementPlus.ElMessage.success('删除成功');
  319. loadList();
  320. } else {
  321. ElementPlus.ElMessage.error(res.msg || '删除失败');
  322. }
  323. } catch {}
  324. }
  325. onMounted(() => loadList());
  326. return {
  327. loading, list, total, page, pageSize, query,
  328. dialogVisible, dialogTitle, activeTab, saving, form, fileInput,
  329. loadList, resetQuery, openDialog, triggerUpload, handleUpload, refreshPreview, saveItem, deleteItem
  330. };
  331. }
  332. });
  333. for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  334. app.component(key, component);
  335. }
  336. app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
  337. app.mount('#bannerApp');
  338. </script>
  339. {% endblock %}