25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 
 
 

476 satır
24 KiB

  1. {% extends "../layout.html" %}
  2. {% block title %}栏目管理{% endblock %}
  3. {% block content %}
  4. <div id="columnApp">
  5. <el-card shadow="never">
  6. <template #header>
  7. <div class="flex items-center justify-between">
  8. <span class="font-medium">栏目管理</span>
  9. <div class="flex gap-2">
  10. <el-button @click="toggleAllTree">展开/折叠全部</el-button>
  11. <el-button type="primary" @click="showAddModal(0, null)">+ 新增顶级栏目</el-button>
  12. </div>
  13. </div>
  14. </template>
  15. <el-tree ref="treeRef" :data="treeData" node-key="id" default-expand-all :expand-on-click-node="false" v-loading="loading">
  16. <template #default="{ node, data }">
  17. <div class="tree-node flex items-center justify-between w-full py-2 pr-4">
  18. <div class="flex items-center gap-2">
  19. <el-icon v-if="data.icon && data.parent_id === 0" :size="18"><component :is="'el-icon-' + data.icon"></component></el-icon>
  20. <span v-else class="text-base">${ data.parent_id === 0 ? '📁' : '📄' }</span>
  21. <span class="font-medium">${ data.name }</span>
  22. <el-tag v-if="data.key" size="small" type="info">${ data.key }</el-tag>
  23. <el-tag v-if="data.parent_id === 0 && data.is_single_page" size="small">单页面</el-tag>
  24. <el-tag v-if="data.type" :type="typeTagMap[data.type]" size="small">${ typeMap[data.type] }</el-tag>
  25. <el-tag v-if="!data.visible" type="info" size="small">隐藏</el-tag>
  26. </div>
  27. <div class="flex items-center gap-1">
  28. <el-button type="primary" link size="small" @click.stop="editItem(data)">编辑</el-button>
  29. <el-button v-if="data.parent_id === 0" type="primary" link size="small" @click.stop="showAddModal(data.id, data)">+ 子栏目</el-button>
  30. <el-button v-if="data.type === 'form'" type="warning" link size="small" @click.stop="openFormConfig(data)">表单配置</el-button>
  31. <el-button v-if="data.parent_id !== 0" type="danger" link size="small" @click.stop="deleteItem(data)">删除</el-button>
  32. </div>
  33. </div>
  34. </template>
  35. </el-tree>
  36. </el-card>
  37. <!-- 新增/编辑弹窗 -->
  38. <el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px" destroy-on-close>
  39. <el-tabs v-model="activeTab" v-if="showSeoTab">
  40. <el-tab-pane label="基本信息" name="basic">
  41. <el-form :model="form" label-width="100px">
  42. <el-form-item label="栏目名称" required>
  43. <el-input v-model="form.name" placeholder="请输入栏目名称"></el-input>
  44. </el-form-item>
  45. <el-form-item label="英文名称" v-if="form.parent_id === 0">
  46. <el-input v-model="form.name_en" placeholder="如: ABOUT US"></el-input>
  47. </el-form-item>
  48. <el-form-item label="栏目描述" v-if="form.parent_id === 0">
  49. <el-input v-model="form.description" placeholder="一句话描述"></el-input>
  50. </el-form-item>
  51. <el-form-item label="Banner图" v-if="form.parent_id === 0">
  52. <div class="flex items-center gap-3">
  53. <el-image v-if="form.banner_image" :src="form.banner_image" style="width:200px;height:80px;border-radius:4px;" fit="cover"></el-image>
  54. <el-button @click="uploadBanner">上传图片</el-button>
  55. <el-button v-if="form.banner_image" link type="danger" @click="form.banner_image = ''">清除</el-button>
  56. </div>
  57. </el-form-item>
  58. <el-form-item label="栏目标识">
  59. <el-input v-model="form.key" placeholder="如: about-us(用于路由)"></el-input>
  60. </el-form-item>
  61. <el-form-item label="图标" v-if="form.parent_id === 0">
  62. <div class="flex items-center gap-2">
  63. <el-button @click="iconPickerVisible = true">
  64. <el-icon v-if="form.icon"><component :is="'el-icon-' + form.icon"></component></el-icon>
  65. <span v-else>选择图标</span>
  66. </el-button>
  67. <span v-if="form.icon" class="text-gray-500">${ form.icon }</span>
  68. <el-button v-if="form.icon" link type="danger" @click="form.icon = ''">清除</el-button>
  69. </div>
  70. </el-form-item>
  71. <el-form-item label="栏目类型" v-if="form.parent_id === 0">
  72. <el-radio-group v-model="form.is_single_page">
  73. <el-radio :value="1">单页面(二级为模块)</el-radio>
  74. <el-radio :value="0">多页面(二级为独立页面)</el-radio>
  75. </el-radio-group>
  76. </el-form-item>
  77. <el-form-item label="内容类型">
  78. <el-select v-model="form.type" placeholder="无" style="width:100%;" :teleported="false">
  79. <el-option label="无" value=""></el-option>
  80. <el-option label="图文列表" value="article"></el-option>
  81. <el-option label="图片列表" value="image"></el-option>
  82. <el-option label="文字列表" value="text"></el-option>
  83. <el-option label="单页" value="page"></el-option>
  84. <el-option label="人员列表" value="person"></el-option>
  85. <el-option label="自定义表单" value="form"></el-option>
  86. <el-option label="捐赠收支数据" value="donation"></el-option>
  87. <el-option label="岗位管理" value="job"></el-option>
  88. <el-option label="轮播图" value="banner"></el-option>
  89. </el-select>
  90. </el-form-item>
  91. <el-form-item label="排序">
  92. <el-input-number v-model="form.sort" :min="1" style="width:120px;"></el-input-number>
  93. </el-form-item>
  94. <el-form-item label="显示状态">
  95. <el-switch v-model="form.visible" :active-value="1" :inactive-value="0"></el-switch>
  96. </el-form-item>
  97. <el-form-item label="外部链接">
  98. <el-input v-model="form.link" placeholder="留空则使用系统页面"></el-input>
  99. </el-form-item>
  100. </el-form>
  101. </el-tab-pane>
  102. <el-tab-pane label="SEO配置" name="seo"></el-tab-pane>
  103. <el-form :model="form" label-width="120px">
  104. <el-form-item label="页面标题">
  105. <el-input v-model="form.seo_title" placeholder="用于浏览器标签"></el-input>
  106. </el-form-item>
  107. <el-form-item label="关键词">
  108. <el-input v-model="form.seo_keywords" placeholder="多个关键词用逗号分隔"></el-input>
  109. </el-form-item>
  110. <el-form-item label="描述">
  111. <el-input v-model="form.seo_description" type="textarea" :rows="3"></el-input>
  112. </el-form-item>
  113. <el-form-item label="URL别名">
  114. <el-input v-model="form.slug" placeholder="如: about-us"></el-input>
  115. </el-form-item>
  116. </el-form>
  117. </el-tab-pane>
  118. </el-tabs>
  119. <!-- 无SEO配置时直接显示基本信息 -->
  120. <el-form v-if="!showSeoTab" :model="form" label-width="100px">
  121. <el-form-item label="栏目名称" required>
  122. <el-input v-model="form.name" placeholder="请输入栏目名称"></el-input>
  123. </el-form-item>
  124. <el-form-item label="英文名称" v-if="form.parent_id === 0">
  125. <el-input v-model="form.name_en" placeholder="如: ABOUT US"></el-input>
  126. </el-form-item>
  127. <el-form-item label="栏目描述" v-if="form.parent_id === 0">
  128. <el-input v-model="form.description" placeholder="一句话描述"></el-input>
  129. </el-form-item>
  130. <el-form-item label="Banner图" v-if="form.parent_id === 0">
  131. <div class="flex items-center gap-3">
  132. <el-image v-if="form.banner_image" :src="form.banner_image" style="width:200px;height:80px;border-radius:4px;" fit="cover"></el-image>
  133. <el-button @click="uploadBanner">上传图片</el-button>
  134. <el-button v-if="form.banner_image" link type="danger" @click="form.banner_image = ''">清除</el-button>
  135. </div>
  136. </el-form-item>
  137. <el-form-item label="栏目标识">
  138. <el-input v-model="form.key" placeholder="如: about-us(用于路由)"></el-input>
  139. </el-form-item>
  140. <el-form-item label="图标" v-if="form.parent_id === 0">
  141. <div class="flex items-center gap-2">
  142. <el-button @click="iconPickerVisible = true">
  143. <el-icon v-if="form.icon"><component :is="'el-icon-' + form.icon"></component></el-icon>
  144. <span v-else>选择图标</span>
  145. </el-button>
  146. <span v-if="form.icon" class="text-gray-500">${ form.icon }</span>
  147. <el-button v-if="form.icon" link type="danger" @click="form.icon = ''">清除</el-button>
  148. </div>
  149. </el-form-item>
  150. <el-form-item label="栏目类型" v-if="form.parent_id === 0">
  151. <el-radio-group v-model="form.is_single_page">
  152. <el-radio :value="1">单页面(二级为模块)</el-radio>
  153. <el-radio :value="0">多页面(二级为独立页面)</el-radio>
  154. </el-radio-group>
  155. </el-form-item>
  156. <el-form-item label="内容类型">
  157. <el-select v-model="form.type" placeholder="无" style="width:100%;" :teleported="false">
  158. <el-option label="无" value=""></el-option>
  159. <el-option label="图文列表" value="article"></el-option>
  160. <el-option label="图片列表" value="image"></el-option>
  161. <el-option label="文字列表" value="text"></el-option>
  162. <el-option label="单页" value="page"></el-option>
  163. <el-option label="人员列表" value="person"></el-option>
  164. <el-option label="自定义表单" value="form"></el-option>
  165. <el-option label="捐赠收支数据" value="donation"></el-option>
  166. <el-option label="岗位管理" value="job"></el-option>
  167. <el-option label="轮播图" value="banner"></el-option>
  168. </el-select>
  169. </el-form-item>
  170. <el-form-item label="排序">
  171. <el-input-number v-model="form.sort" :min="1" style="width:120px;"></el-input-number>
  172. </el-form-item>
  173. <el-form-item label="显示状态">
  174. <el-switch v-model="form.visible" :active-value="1" :inactive-value="0"></el-switch>
  175. </el-form-item>
  176. <el-form-item label="外部链接">
  177. <el-input v-model="form.link" placeholder="留空则使用系统页面"></el-input>
  178. </el-form-item>
  179. </el-form>
  180. <template #footer></template>
  181. <el-button @click="dialogVisible = false">取消</el-button>
  182. <el-button type="primary" @click="saveItem" :loading="saving">保存</el-button>
  183. </template>
  184. </el-dialog>
  185. <!-- 图标选择弹窗 -->
  186. <el-dialog v-model="iconPickerVisible" title="选择图标" width="600px">
  187. <el-input v-model="iconSearch" placeholder="搜索图标..." class="mb-4" clearable></el-input>
  188. <div class="icon-grid">
  189. <div v-for="icon in filteredIcons" :key="icon" class="icon-item" :class="{ active: form.icon === icon }" @click="selectIcon(icon)">
  190. <el-icon :size="20"><component :is="'el-icon-' + icon"></component></el-icon>
  191. <span class="icon-name">${ icon }</span>
  192. </div>
  193. </div>
  194. </el-dialog>
  195. <!-- 表单配置抽屉 -->
  196. <el-drawer v-model="formDrawerVisible" :title="formDrawerTitle" size="450px" destroy-on-close>
  197. <div class="mb-3">
  198. <el-button type="primary" size="small" @click="addFormField">+ 添加字段</el-button>
  199. </div>
  200. <div v-if="formFields.length === 0" class="text-center text-gray-400 py-10">暂无字段</div>
  201. <div v-else class="space-y-3">
  202. <el-card v-for="(field, idx) in formFields" :key="idx" shadow="never">
  203. <template #header>
  204. <div class="flex items-center justify-between">
  205. <span>#${ idx + 1 }</span>
  206. <div class="flex gap-1">
  207. <el-button v-if="idx > 0" link size="small" @click="moveField(idx, -1)">↑</el-button>
  208. <el-button v-if="idx < formFields.length - 1" link size="small" @click="moveField(idx, 1)">↓</el-button>
  209. <el-button type="danger" link size="small" @click="removeField(idx)">删除</el-button>
  210. </div>
  211. </div>
  212. </template>
  213. <el-form label-width="70px" size="small">
  214. <el-form-item label="字段名"><el-input v-model="field.name"></el-input></el-form-item>
  215. <el-form-item label="类型">
  216. <el-select v-model="field.fieldType" style="width:100%;" :teleported="false">
  217. <el-option label="单行文本" value="text"></el-option>
  218. <el-option label="多行文本" value="textarea"></el-option>
  219. <el-option label="邮箱" value="email"></el-option>
  220. <el-option label="日期" value="date"></el-option>
  221. <el-option label="单选" value="radio"></el-option>
  222. <el-option label="下拉选择" value="select"></el-option>
  223. <el-option label="多选" value="checkbox"></el-option>
  224. <el-option label="文件上传" value="file"></el-option>
  225. </el-select>
  226. </el-form-item>
  227. <el-form-item label="必填"><el-switch v-model="field.required"></el-switch></el-form-item>
  228. </el-form>
  229. </el-card>
  230. </div>
  231. <template #footer>
  232. <el-button @click="formDrawerVisible = false">取消</el-button>
  233. <el-button type="primary" @click="saveFormConfig" :loading="formSaving">保存</el-button>
  234. </template>
  235. </el-drawer>
  236. </div>
  237. <style>
  238. .tree-node { min-height: 40px; }
  239. .el-tree-node__content { height: auto !important; padding: 4px 0; }
  240. .icon-grid { display: grid; grid-template-columns: repeat(10, 1fr); gap: 6px; max-height: 360px; overflow-y: auto; }
  241. .icon-item { display: flex; flex-direction: column; align-items: center; padding: 8px 4px; border: 1px solid #eee; border-radius: 4px; cursor: pointer; }
  242. .icon-item:hover { border-color: #ff7800; background: #fff7f0; }
  243. .icon-item.active { border-color: #ff7800; background: #ff7800; color: #fff; }
  244. .icon-name { font-size: 9px; text-align: center; word-break: break-all; margin-top: 4px; line-height: 1.2; max-width: 100%; overflow: hidden; }
  245. </style>
  246. {% endblock %}
  247. {% block js %}
  248. <script>
  249. const { createApp, ref, reactive, computed, onMounted } = Vue;
  250. // Element Plus 常用图标列表
  251. const iconList = [
  252. 'HomeFilled', 'House', 'OfficeBuilding', 'School', 'Shop', 'ShoppingCart',
  253. 'Document', 'DocumentCopy', 'Folder', 'FolderOpened', 'Files', 'Collection',
  254. 'Reading', 'Notebook', 'Tickets', 'Memo', 'Postcard', 'Message', 'ChatDotRound',
  255. 'Phone', 'PhoneFilled', 'Cellphone', 'Position', 'Location', 'LocationFilled',
  256. 'User', 'UserFilled', 'Avatar', 'Service', 'Coordinate', 'Management',
  257. 'Setting', 'Tools', 'Operation', 'Platform', 'Monitor', 'DataAnalysis',
  258. 'PieChart', 'DataLine', 'TrendCharts', 'Histogram', 'Odometer', 'Timer',
  259. 'Calendar', 'Clock', 'AlarmClock', 'Watch', 'Stopwatch', 'Bell', 'BellFilled',
  260. 'Notification', 'Flag', 'Trophy', 'Medal', 'Present', 'GoodsFilled', 'Goods',
  261. 'Box', 'Briefcase', 'Suitcase', 'SuitcaseLine', 'Wallet', 'WalletFilled',
  262. 'Money', 'Coin', 'CreditCard', 'Discount', 'PriceTag', 'Sell', 'SoldOut',
  263. 'Camera', 'CameraFilled', 'Picture', 'PictureFilled', 'Film', 'VideoCamera',
  264. 'Headset', 'Microphone', 'Mute', 'VideoPlay', 'VideoPause', 'MuteNotification',
  265. 'Star', 'StarFilled', 'Sunny', 'Moon', 'Cloudy', 'PartlyCloudy', 'Lightning',
  266. 'Sunrise', 'Sunset', 'MostlyCloudy', 'Pouring', 'Drizzling', 'WindPower',
  267. 'Link', 'Share', 'Connection', 'Promotion', 'Guide', 'Help', 'HelpFilled',
  268. 'QuestionFilled', 'InfoFilled', 'WarningFilled', 'SuccessFilled', 'CircleCheckFilled',
  269. 'Edit', 'EditPen', 'Delete', 'DeleteFilled', 'Plus', 'Minus', 'Check', 'Close',
  270. 'Search', 'ZoomIn', 'ZoomOut', 'View', 'Hide', 'Refresh', 'RefreshRight',
  271. 'Upload', 'Download', 'UploadFilled', 'Printer', 'List', 'Grid', 'Menu',
  272. 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'DArrowLeft', 'DArrowRight',
  273. 'Top', 'Bottom', 'Back', 'Right', 'Sort', 'SortUp', 'SortDown', 'Rank',
  274. 'Lock', 'Unlock', 'Key', 'Open', 'TurnOff', 'SwitchButton', 'SwitchFilled',
  275. 'Cpu', 'SetUp', 'Opportunity', 'Aim', 'Compass', 'MapLocation', 'Place',
  276. 'Bicycle', 'Ship', 'Van', 'Truck', 'CaretTop', 'CaretBottom', 'CaretLeft', 'CaretRight',
  277. 'Coffee', 'CoffeeCup', 'Dessert', 'IceCream', 'Food', 'Burger', 'KnifeFork',
  278. 'Apple', 'Grape', 'Cherry', 'Orange', 'Pear', 'Watermelon', 'Sugar', 'Lollipop',
  279. 'FirstAidKit', 'MagicStick', 'Brush', 'Scissor', 'Stamp', 'Paperclip', 'Magnet',
  280. 'Basketball', 'Football', 'Baseball', 'Soccer', 'GobletFull', 'GobletSquareFull',
  281. 'IceDrink', 'IceTea', 'Milk', 'HotWater', 'NoSmoking', 'Smoking', 'Female', 'Male'
  282. ];
  283. const app = createApp({
  284. delimiters: ['${', '}'],
  285. setup() {
  286. const loading = ref(false);
  287. const treeRef = ref(null);
  288. const treeData = ref([]);
  289. const typeMap = {
  290. 'article': '图文列表', 'image': '图片列表', 'text': '文字列表',
  291. 'page': '单页', 'person': '人员列表', 'form': '自定义表单',
  292. 'donation': '捐赠收支', 'job': '岗位管理', 'banner': '轮播图'
  293. };
  294. const typeTagMap = {
  295. 'article': 'primary', 'image': 'success', 'text': 'warning',
  296. 'page': 'info', 'person': 'danger', 'form': 'primary',
  297. 'donation': 'success', 'job': 'info', 'banner': 'warning'
  298. };
  299. const dialogVisible = ref(false);
  300. const dialogTitle = ref('新增栏目');
  301. const activeTab = ref('basic');
  302. const saving = ref(false);
  303. const parentIsSinglePage = ref(false);
  304. const form = reactive({
  305. id: null, parent_id: 0, name: '', name_en: '', description: '', banner_image: '', key: '', icon: '', type: '', is_single_page: 0, sort: 1, visible: 1,
  306. link: '', seo_title: '', seo_keywords: '', seo_description: '', slug: ''
  307. });
  308. // 图标选择
  309. const iconPickerVisible = ref(false);
  310. const iconSearch = ref('');
  311. const filteredIcons = computed(() => {
  312. if (!iconSearch.value) return iconList;
  313. return iconList.filter(i => i.toLowerCase().includes(iconSearch.value.toLowerCase()));
  314. });
  315. function selectIcon(icon) {
  316. form.icon = icon;
  317. iconPickerVisible.value = false;
  318. }
  319. // Banner图片上传
  320. function uploadBanner() {
  321. const input = document.createElement('input');
  322. input.type = 'file';
  323. input.accept = 'image/*';
  324. input.onchange = async (e) => {
  325. const file = e.target.files[0];
  326. if (!file) return;
  327. const fd = new FormData();
  328. fd.append('file', file);
  329. try {
  330. const res = await fetch('/admin/upload', { method: 'POST', body: fd }).then(r => r.json());
  331. if (res.code === 0) { form.banner_image = res.data.url; }
  332. else { ElementPlus.ElMessage.error(res.msg || '上传失败'); }
  333. } catch { ElementPlus.ElMessage.error('上传失败'); }
  334. };
  335. input.click();
  336. }
  337. const showSeoTab = computed(() => {
  338. if (form.parent_id === 0 && form.is_single_page === 1) return true;
  339. if (form.parent_id !== 0 && !parentIsSinglePage.value) return true;
  340. return false;
  341. });
  342. const formDrawerVisible = ref(false);
  343. const formDrawerTitle = ref('表单字段配置');
  344. const formFields = ref([]);
  345. const formSaving = ref(false);
  346. const currentFormColumnId = ref(null);
  347. async function loadList() {
  348. loading.value = true;
  349. try {
  350. const res = await fetch('/admin/system/column/list').then(r => r.json());
  351. if (res.code === 0) treeData.value = res.data || [];
  352. } finally { loading.value = false; }
  353. }
  354. let allExpanded = true;
  355. function toggleAllTree() {
  356. if (!treeRef.value) return;
  357. allExpanded = !allExpanded;
  358. Object.values(treeRef.value.store.nodesMap).forEach(node => {
  359. if (node.childNodes?.length > 0) node.expanded = allExpanded;
  360. });
  361. }
  362. function showAddModal(parentId, parentData) {
  363. dialogTitle.value = parentId ? '新增子栏目' : '新增顶级栏目';
  364. activeTab.value = 'basic';
  365. parentIsSinglePage.value = parentData ? !!parentData.is_single_page : false;
  366. Object.assign(form, {
  367. id: null, parent_id: parentId || 0, name: '', name_en: '', description: '', banner_image: '', key: '', icon: '', type: '', is_single_page: 0, sort: 1, visible: 1,
  368. link: '', seo_title: '', seo_keywords: '', seo_description: '', slug: ''
  369. });
  370. dialogVisible.value = true;
  371. }
  372. function editItem(row) {
  373. dialogTitle.value = '编辑栏目';
  374. activeTab.value = 'basic';
  375. if (row.parent_id !== 0) {
  376. const parent = treeData.value.find(p => p.id === row.parent_id);
  377. parentIsSinglePage.value = parent ? !!parent.is_single_page : false;
  378. } else {
  379. parentIsSinglePage.value = false;
  380. }
  381. Object.assign(form, {
  382. id: row.id, parent_id: row.parent_id || 0, name: row.name, name_en: row.name_en || '', description: row.description || '',
  383. banner_image: row.banner_image || '', key: row.key || '', icon: row.icon || '',
  384. type: row.type || '', is_single_page: row.is_single_page || 0, sort: row.sort || 1, visible: row.visible,
  385. link: row.link || '', seo_title: row.seo_title || '', seo_keywords: row.seo_keywords || '',
  386. seo_description: row.seo_description || '', slug: row.slug || ''
  387. });
  388. dialogVisible.value = true;
  389. }
  390. async function saveItem() {
  391. if (!form.name.trim()) { ElementPlus.ElMessage.warning('请输入栏目名称'); return; }
  392. saving.value = true;
  393. try {
  394. const url = form.id ? '/admin/system/column/edit' : '/admin/system/column/add';
  395. const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form) }).then(r => r.json());
  396. if (res.code === 0) { ElementPlus.ElMessage.success('保存成功'); dialogVisible.value = false; loadList(); }
  397. else { ElementPlus.ElMessage.error(res.msg || '保存失败'); }
  398. } finally { saving.value = false; }
  399. }
  400. async function deleteItem(row) {
  401. try {
  402. await ElementPlus.ElMessageBox.confirm('确定删除"' + row.name + '"?', '提示', { type: 'warning' });
  403. const res = await fetch('/admin/system/column/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: row.id }) }).then(r => r.json());
  404. if (res.code === 0) { ElementPlus.ElMessage.success('删除成功'); loadList(); }
  405. else { ElementPlus.ElMessage.error(res.msg || '删除失败'); }
  406. } catch {}
  407. }
  408. function openFormConfig(row) {
  409. currentFormColumnId.value = row.id;
  410. formDrawerTitle.value = '【' + row.name + '】表单配置';
  411. formFields.value = row.form_config ? JSON.parse(row.form_config) : [];
  412. formDrawerVisible.value = true;
  413. }
  414. function addFormField() { formFields.value.push({ name: '新字段', fieldType: 'text', required: false }); }
  415. function moveField(idx, dir) {
  416. const t = idx + dir;
  417. if (t < 0 || t >= formFields.value.length) return;
  418. [formFields.value[idx], formFields.value[t]] = [formFields.value[t], formFields.value[idx]];
  419. }
  420. function removeField(idx) { formFields.value.splice(idx, 1); }
  421. async function saveFormConfig() {
  422. formSaving.value = true;
  423. try {
  424. const res = await fetch('/admin/system/column/saveFormConfig', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: currentFormColumnId.value, form_config: formFields.value }) }).then(r => r.json());
  425. if (res.code === 0) { ElementPlus.ElMessage.success('保存成功'); formDrawerVisible.value = false; loadList(); }
  426. else { ElementPlus.ElMessage.error(res.msg || '保存失败'); }
  427. } finally { formSaving.value = false; }
  428. }
  429. onMounted(() => loadList());
  430. return {
  431. loading, treeRef, treeData, typeMap, typeTagMap,
  432. dialogVisible, dialogTitle, activeTab, saving, form, showSeoTab,
  433. iconPickerVisible, iconSearch, filteredIcons, selectIcon,
  434. formDrawerVisible, formDrawerTitle, formFields, formSaving,
  435. loadList, toggleAllTree, showAddModal, editItem, saveItem, deleteItem,
  436. openFormConfig, addFormField, moveField, removeField, saveFormConfig,
  437. uploadBanner
  438. };
  439. }
  440. });
  441. // 注册所有图标组件
  442. for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  443. app.component('el-icon-' + key, component);
  444. }
  445. app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
  446. app.mount('#columnApp');
  447. </script>
  448. {% endblock %}