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.
 
 
 
 
 
 

310 rindas
13 KiB

  1. {% extends "../layout.html" %}
  2. {% block title %}{{ columnInfo.name if columnInfo else '文字列表' }}{% endblock %}
  3. {% block content %}
  4. <div id="textApp">
  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="warning" 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:200px;" clearable @keyup.enter="loadList"></el-input>
  18. <el-select v-model="query.year" placeholder="年度" style="width:100px;" clearable>
  19. <el-option v-for="y in yearOptions" :key="y" :label="y" :value="y"></el-option>
  20. </el-select>
  21. <el-select v-model="query.status" placeholder="状态" style="width:100px;" clearable>
  22. <el-option label="已发布" :value="1"></el-option>
  23. <el-option label="待审核" :value="2"></el-option>
  24. <el-option label="草稿" :value="0"></el-option>
  25. </el-select>
  26. <el-button type="primary" @click="loadList">搜索</el-button>
  27. <el-button @click="resetQuery" class="ml-2">重置</el-button>
  28. </div>
  29. <!-- 信息栏 -->
  30. <div class="flex items-center gap-3 mb-3 text-sm text-gray-500">
  31. <span>共 <b class="text-orange-500">${ total }</b> 条记录</span>
  32. <div class="flex-1"></div>
  33. <el-button size="small" :disabled="!selectedIds.length" @click="batchDelete">批量删除</el-button>
  34. </div>
  35. <!-- 表格 -->
  36. <el-table :data="list" v-loading="loading" @selection-change="handleSelectionChange" border>
  37. <el-table-column type="selection" width="40"></el-table-column>
  38. <el-table-column prop="title" label="标题" min-width="300">
  39. <template #default="{ row }">
  40. <a :href="row.file_url" target="_blank" class="text-link">${ row.title }</a>
  41. </template>
  42. </el-table-column>
  43. <el-table-column prop="category" label="分类" width="120"></el-table-column>
  44. <el-table-column prop="file_type" label="附件" width="80">
  45. <template #default="{ row }">
  46. <span class="file-badge" :class="'file-' + row.file_type?.toLowerCase()">${ row.file_type || '-' }</span>
  47. </template>
  48. </el-table-column>
  49. <el-table-column prop="year" label="年度" width="80"></el-table-column>
  50. <el-table-column prop="status" label="状态" width="90">
  51. <template #default="{ row }">
  52. <el-tag :type="statusMap[row.status]?.type" size="small">${ statusMap[row.status]?.text }</el-tag>
  53. </template>
  54. </el-table-column>
  55. <el-table-column prop="create_time" label="发布日期" width="110">
  56. <template #default="{ row }">${ row.create_time?.slice(0,10) }</template>
  57. </el-table-column>
  58. <el-table-column label="操作" width="150" fixed="right">
  59. <template #default="{ row }">
  60. <el-button type="primary" link size="small" @click="openDialog(row)">编辑</el-button>
  61. <el-button type="primary" link size="small" @click="downloadFile(row)" :disabled="!row.file_url">下载</el-button>
  62. <el-button type="danger" link size="small" @click="deleteItem(row)">删除</el-button>
  63. </template>
  64. </el-table-column>
  65. </el-table>
  66. <!-- 分页 -->
  67. <div class="flex justify-end mt-4" v-if="total > pageSize">
  68. <el-pagination background layout="prev, pager, next" :total="total" :page-size="pageSize" v-model:current-page="page" @current-change="loadList"></el-pagination>
  69. </div>
  70. </el-card>
  71. <!-- 新增/编辑弹窗 -->
  72. <el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px" destroy-on-close draggable top="5vh">
  73. <el-form :model="form" label-width="80px">
  74. <el-form-item label="标题" required>
  75. <el-input v-model="form.title" placeholder="请输入标题"></el-input>
  76. </el-form-item>
  77. <div class="flex gap-4">
  78. <el-form-item label="分类" class="flex-1">
  79. <el-select v-model="form.category" placeholder="选择分类" style="width:100%;" :teleported="false" filterable allow-create>
  80. <el-option v-for="c in categoryOptions" :key="c" :label="c" :value="c"></el-option>
  81. </el-select>
  82. </el-form-item>
  83. <el-form-item label="年度" class="flex-1">
  84. <el-select v-model="form.year" placeholder="选择年度" style="width:100%;" :teleported="false">
  85. <el-option v-for="y in yearOptions" :key="y" :label="y" :value="y"></el-option>
  86. </el-select>
  87. </el-form-item>
  88. </div>
  89. <el-form-item label="附件">
  90. <div class="flex items-center gap-2 w-full">
  91. <el-input v-model="form.file_name" placeholder="选择附件(PDF/DOC/XLS)" readonly class="flex-1"></el-input>
  92. <el-button @click="triggerUpload">选择文件</el-button>
  93. <input type="file" ref="fileInput" accept=".pdf,.doc,.docx,.xls,.xlsx" style="display:none;" @change="handleUpload">
  94. </div>
  95. <div class="text-xs text-gray-400 mt-1">支持 PDF、DOC、XLS 格式</div>
  96. </el-form-item>
  97. <div class="flex gap-4">
  98. <el-form-item label="附件类型" class="flex-1">
  99. <el-select v-model="form.file_type" style="width:100%;" :teleported="false">
  100. <el-option label="PDF" value="PDF"></el-option>
  101. <el-option label="DOC" value="DOC"></el-option>
  102. <el-option label="XLS" value="XLS"></el-option>
  103. </el-select>
  104. </el-form-item>
  105. <el-form-item label="状态" class="flex-1">
  106. <el-select v-model="form.status" style="width:100%;" :teleported="false">
  107. <el-option label="已发布" :value="1"></el-option>
  108. <el-option label="待审核" :value="2"></el-option>
  109. <el-option label="草稿" :value="0"></el-option>
  110. </el-select>
  111. </el-form-item>
  112. </div>
  113. </el-form>
  114. <template #footer>
  115. <el-button @click="dialogVisible = false">取消</el-button>
  116. <el-button type="primary" @click="saveItem" :loading="saving">保存</el-button>
  117. </template>
  118. </el-dialog>
  119. </div>
  120. <style>
  121. .text-link { color: #333; transition: .15s; }
  122. .text-link:hover { color: #ff7800; }
  123. .file-badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 3px; font-size: 11px; font-weight: 600; background: #fef0f0; color: #f56c6c; letter-spacing: .5px; }
  124. .file-badge.file-xls { background: #f0f9eb; color: #67c23a; }
  125. .file-badge.file-xlsx { background: #f0f9eb; color: #67c23a; }
  126. .file-badge.file-doc { background: #ecf5ff; color: #409eff; }
  127. .file-badge.file-docx { background: #ecf5ff; color: #409eff; }
  128. </style>
  129. {% endblock %}
  130. {% block js %}
  131. <script>
  132. const col = '{{ col }}';
  133. const { createApp, ref, reactive, computed, onMounted } = Vue;
  134. const app = createApp({
  135. delimiters: ['${', '}'],
  136. setup() {
  137. const loading = ref(false);
  138. const list = ref([]);
  139. const total = ref(0);
  140. const page = ref(1);
  141. const pageSize = ref(20);
  142. const query = reactive({ keyword: '', year: '', status: '' });
  143. const selectedIds = ref([]);
  144. const dialogVisible = ref(false);
  145. const dialogTitle = ref('新增');
  146. const saving = ref(false);
  147. const fileInput = ref(null);
  148. const currentYear = new Date().getFullYear();
  149. const yearOptions = Array.from({ length: 5 }, (_, i) => String(currentYear - i));
  150. const categoryOptions = ['药品援助公示', '管理制度', '机构年报', '审计报告', '财务报告', '关联方信息', '项目执行报告', '党建规章'];
  151. const statusMap = {
  152. 1: { type: 'success', text: '已发布' },
  153. 2: { type: 'warning', text: '待审核' },
  154. 0: { type: 'info', text: '草稿' }
  155. };
  156. const defaultForm = {
  157. id: null, title: '', category: '', year: String(currentYear),
  158. file_url: '', file_name: '', file_type: 'PDF', status: 1
  159. };
  160. const form = reactive({ ...defaultForm });
  161. async function loadList() {
  162. loading.value = true;
  163. try {
  164. const params = new URLSearchParams({ col, page: page.value, pageSize: pageSize.value, ...query });
  165. const res = await fetch('/admin/content/text/list?' + params).then(r => r.json());
  166. if (res.code === 0) {
  167. list.value = res.data.data || [];
  168. total.value = res.data.count || 0;
  169. }
  170. } finally { loading.value = false; }
  171. }
  172. function resetQuery() {
  173. query.keyword = '';
  174. query.year = '';
  175. query.status = '';
  176. page.value = 1;
  177. loadList();
  178. }
  179. function handleSelectionChange(rows) {
  180. selectedIds.value = rows.map(r => r.id);
  181. }
  182. function openDialog(item) {
  183. if (item) {
  184. dialogTitle.value = '编辑';
  185. Object.assign(form, {
  186. id: item.id, title: item.title, category: item.category || '',
  187. year: item.year || String(currentYear), file_url: item.file_url || '',
  188. file_name: item.file_name || '', file_type: item.file_type || 'PDF',
  189. status: item.status
  190. });
  191. } else {
  192. dialogTitle.value = '新增';
  193. Object.assign(form, { ...defaultForm });
  194. }
  195. dialogVisible.value = true;
  196. }
  197. function triggerUpload() {
  198. fileInput.value?.click();
  199. }
  200. async function handleUpload(e) {
  201. const file = e.target.files[0];
  202. if (!file) return;
  203. const formData = new FormData();
  204. formData.append('file', file);
  205. try {
  206. const res = await fetch('/admin/upload', { method: 'POST', body: formData }).then(r => r.json());
  207. if (res.code === 0) {
  208. form.file_url = res.data.url;
  209. form.file_name = file.name;
  210. // 自动识别文件类型
  211. const ext = file.name.split('.').pop().toUpperCase();
  212. if (['PDF'].includes(ext)) form.file_type = 'PDF';
  213. else if (['DOC', 'DOCX'].includes(ext)) form.file_type = 'DOC';
  214. else if (['XLS', 'XLSX'].includes(ext)) form.file_type = 'XLS';
  215. ElementPlus.ElMessage.success('上传成功');
  216. } else {
  217. ElementPlus.ElMessage.error(res.msg || '上传失败');
  218. }
  219. } catch (err) {
  220. ElementPlus.ElMessage.error('上传失败');
  221. }
  222. e.target.value = '';
  223. }
  224. async function saveItem() {
  225. if (!form.title.trim()) { ElementPlus.ElMessage.warning('请输入标题'); return; }
  226. saving.value = true;
  227. try {
  228. const url = form.id ? '/admin/content/text/edit' : '/admin/content/text/add';
  229. const body = { ...form, col };
  230. const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(r => r.json());
  231. if (res.code === 0) {
  232. ElementPlus.ElMessage.success('保存成功');
  233. dialogVisible.value = false;
  234. loadList();
  235. } else {
  236. ElementPlus.ElMessage.error(res.msg || '保存失败');
  237. }
  238. } finally { saving.value = false; }
  239. }
  240. async function deleteItem(item) {
  241. try {
  242. await ElementPlus.ElMessageBox.confirm('确定删除"' + item.title + '"?', '提示', { type: 'warning' });
  243. const res = await fetch('/admin/content/text/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: item.id }) }).then(r => r.json());
  244. if (res.code === 0) {
  245. ElementPlus.ElMessage.success('删除成功');
  246. loadList();
  247. } else {
  248. ElementPlus.ElMessage.error(res.msg || '删除失败');
  249. }
  250. } catch {}
  251. }
  252. async function batchDelete() {
  253. if (!selectedIds.value.length) return;
  254. try {
  255. await ElementPlus.ElMessageBox.confirm('确定删除选中的 ' + selectedIds.value.length + ' 条记录?', '提示', { type: 'warning' });
  256. const res = await fetch('/admin/content/text/batchDelete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: selectedIds.value }) }).then(r => r.json());
  257. if (res.code === 0) {
  258. ElementPlus.ElMessage.success('删除成功');
  259. loadList();
  260. } else {
  261. ElementPlus.ElMessage.error(res.msg || '删除失败');
  262. }
  263. } catch {}
  264. }
  265. function downloadFile(row) {
  266. if (row.file_url) {
  267. window.open(row.file_url, '_blank');
  268. }
  269. }
  270. onMounted(() => loadList());
  271. return {
  272. loading, list, total, page, pageSize, query, selectedIds,
  273. dialogVisible, dialogTitle, saving, form, fileInput,
  274. yearOptions, categoryOptions, statusMap,
  275. loadList, resetQuery, handleSelectionChange, openDialog,
  276. triggerUpload, handleUpload, saveItem, deleteItem, batchDelete, downloadFile
  277. };
  278. }
  279. });
  280. app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
  281. app.mount('#textApp');
  282. </script>
  283. {% endblock %}