Full-Stack Frontend Implementation of a Publishing Platform: Pagination, CRUD, and Vue‑Element‑Plus Integration
This tutorial walks through the front‑end portion of a publishing platform series, demonstrating how to build a Vue 3 and Element‑plus UI with TailwindCSS, implement server‑side pagination via Koa, integrate Axios with proxy and interceptors, and add complete CRUD operations—including create, edit, and delete dialogs—while showcasing the essential code snippets.
The article continues a series on building a full‑stack publishing platform, focusing on the front‑end implementation using Vue, Element‑plus, TailwindCSS and a Koa back‑end.
Core Points
Front‑end and back‑end data pagination display
Full‑stack CRUD functionality
1. Building the Static UI
Using Element‑plus components such as el-card, el-form, el-table and TailwindCSS classes, a static page layout is created. The following snippet shows the initial markup:
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><!-- 写 tailwindcss 后再也不用想类名了! -->
<el-card class="w-screen">
<template #header>
<span class="font-bold">项目配置</span>
</template>
<!-- 筛选框部分 -->
<el-form inline>
<el-form-item label="项目名称"></el-form-item>
</el-form>
<!-- 表格部分 -->
<el-table>
<el-table-column label="项目名称" prop="projectName" />
...
</el-table>
<!-- 翻页 -->
<el-pagination ... />
</el-card>
</code>2. Back‑End Pagination API
The Koa route implements a paginated query. The key function getConfigList extracts query parameters, calls the service layer, and returns the result.
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menus, monospace; font-size: 12px">export async function getConfigList(ctx, next) {
try {
const { pageNo: page, pageSize, projectName } = ctx.request.query;
const pageData = await services.findJobPage(page, pageSize, { projectName });
ctx.state.apiResponse = { code: RESPONSE_CODE.SUC, data: pageData };
} catch (e) {
ctx.state.apiResponse = { code: RESPONSE_CODE.ERR, msg: '配置分页查询失败' };
}
next();
}
</code>The service layer uses Mongoose to perform find, skip, limit, and count operations:
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menus, monospace; font-size: 12px">export async function findJobPage(page, pageSize, params) {
Object.keys(params).forEach(key => {
if (!params[key]) Reflect.deleteProperty(params, key);
});
const DocumentUserList = await JobModel.find(params)
.skip((page - 1) * pageSize)
.limit(pageSize);
return DocumentUserList.map(_ => _.toObject());
}
export function countJob(params) {
Object.keys(params).forEach(key => {
if (!params[key]) Reflect.deleteProperty(params, key);
});
return JobModel.count(params);
}
</code>3. Front‑End Integration
Axios is used to request the paginated data. A proxy is configured in vite.config to forward /api calls to the back‑end.
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menus, monospace; font-size: 12px">proxy: {
'/api': {
target: 'http://localhost:3200/', // 代理到后端地址
changeOrigin: true,
rewrite: path => {
return path.replace(/^/api/, '');
}
}
}
</code>An interceptor simplifies the response format:
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menus, monospace; font-size: 12px">axios.interceptors.response.use(function (response) {
const data = response.data;
if (data.code === 0) {
return data.data; // 业务层直接拿到数据
}
data.message = data.message || data.msg;
return Promise.reject(data);
});
</code>The request function used in the component:
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menus, monospace; font-size: 12px">export async function getConfig(params) {
return axios.get('/job', { params });
}
</code>4. CRUD Implementation
Create : A button opens an el-dialog with a form. The submit handler onSubmit calls postSave and refreshes the table.
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menus, monospace; font-size: 12px">const formData = reactive({
projectName: '',
gitUrl: '',
gitBranch: '',
buildCommand: '',
uploadPath: ''
});
const onSubmit = async () => {
try {
await postSave(formData);
ElMessage.success('配置保存成功');
await initData();
dialogVisible.value = false;
} catch (e) {
ElMessage.error('配置保存失败');
}
};
</code>Edit : Clicking the edit button fills the form with the selected row and sets isEdit. The same onSubmit decides whether to call postUpdate or postSave.
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menus, monospace; font-size: 12px">const onEdit = rowData => {
isEdit.value = true;
dialogVisible.value = true;
Object.keys(formData).forEach(key => {
formData[key] = rowData[key];
});
formData.id = rowData._id;
};
const onSubmit = async () => {
try {
isEdit.value ? await postUpdate(formData) : await postSave(formData);
ElMessage.success(isEdit.value ? '配置编辑成功' : '配置保存成功');
await initData();
dialogVisible.value = false;
} catch (e) {
ElMessage.error(isEdit.value ? '配置编辑失败' : '配置保存失败');
}
};
</code>Delete : An el-popconfirm wraps a delete button. The handler onDel calls postDelete with the record id.
<code style="padding: 16px; color: #ddd; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menus, monospace; font-size: 12px">const onDel = async rowData => {
try {
await postDelete({ id: rowData._id });
ElMessage.success('配置删除成功');
await initData();
} catch (e) {
ElMessage.error('配置删除失败');
}
};
</code>Conclusion
The article completes the front‑end pagination display and full CRUD cycle for the publishing platform. The next installment will cover triggering builds from the front‑end and streaming Jenkins logs via WebSocket.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
