文章目录
今天看一个 ts 项目的 table 模块,亲身体验这是公司后台管理系统一定会使用到的,也是最常使用到的,这个项目对新手很友好,毕竟是一个相对来说比较空的项目模板,对于我来说就是一个学习的记录,一些技术的分享,手把手告知新手别人的代码怎么读,甚至还能帮该开源项目作者获取一些热度,我很乐于做这样的事情(已经争得原作者许可,感谢 🤓)项目地址:V3 Admin Vite
通过该文章可以学习到 :

我们来看一下具体代码是怎么实现的,我读别人的代码喜欢先看一下大体目录结构、然后从页面功能入手,然后在 html 中找到该组件,然后查看该组件使用的方法等,一直相连关联到底层封装代码(或者直接看脚本逻辑,从脚本逻辑入手,看大家习惯)
功能、底层封装、页面结构等等知道了,自然而然就通了
我一步一步就标注了我对该代码的思考,希望对于大家有所帮助
<script lang="ts" setup>
import { reactive, ref, watch } from "vue"
import { createTableDataApi, deleteTableDataApi, updateTableDataApi, getTableDataApi } from "@/api/table"
import { type FormInstance, type FormRules, ElMessage, ElMessageBox } from "element-plus"
import { Search, Refresh, CirclePlus, Delete, Download, RefreshRight } from "@element-plus/icons-vue"
import { usePagination } from "@/hooks/usePagination"
// 加载状态,这也是 element-ui-plus 的一种加载方法,可以查看 html 元素并访问 element-ui-plus 官网来找到该变量有什么用处
// 定义 loading 为响应式状态值,ts 限制为布尔类型
const loading = ref<boolean>(false)
// 自己封装的页面功能,可以转到 对应 src目录下 hooks 文件夹中的 usePagination 中查看对应方法的功能
// 相应代码在本文章下面,可对应查看或者直接去 GitHub 下载宝藏博主的源码码进行查看
const { paginationData, handleCurrentChange, handleSizeChange } = usePagination()
//#region 增
// 我们在 html 代码中可以看到 是使用在 el-dialog 组件的 v-module 属性上的,我们可以查看 element-ui-plus 文档查看该功能
// v-model 控制这该组件是否显示
const dialogVisible = ref<boolean>(false)
// 表单对象实例 ts限制为表单实例 或 null
// 在新增用户判断时需要使用到,被绑定在 表单的 ref 值上
const formRef = ref<FormInstance | null>(null)
// 表单输入值
const formData = reactive({
username: "",
password: ""
})
// 定义表单验证规则并使用 ts 进行类型规范
const formRules: FormRules = reactive({
username: [{ required: true, trigger: "blur", message: "请输入用户名" }],
password: [{ required: true, trigger: "blur", message: "请输入密码" }]
})
// 创建新用户/修改用户 方法
const handleCreate = () => {
// 判断表单实例是否存在
// 因为 validate 是 element-ui-plus 表单上的一个方法,所以需要使用到表单实例才可以使用该方法,现在我们知道了为什么要获取表单实例了
// validate 接收一个回调函数,或返回 Promise,执行之前是有一个前提的,需要表单实例是存在的
formRef.value?.validate((valid: boolean) => {
if (valid) {
// 如果 valid 存在,那么判断 currentUpdateId 是否为 undefined
// currentUpdateId 是否有值决定着用户操作的是新增还是修改
if (currentUpdateId.value === undefined) {
// 发起创建 table 请求,携带用户名与用户密码
createTableDataApi({
username: formData.username,
password: formData.password
}).then(() => {
// 数据请求成功之后弹出提示信息
ElMessage.success("新增成功")
// 并将弹框设置为不显示
dialogVisible.value = false
// 这里的方法在下面 但是从命名就不难看出 这是新增成功之后重新请求一下所有的数据 保证页面数据的最新
getTableData()
})
} else {
// 前面也有提到 这里是一个炫酷写法,将新增和修改放在一个方法中,执行哪个方法取决于 currentUpdateId 是否有值
// 不得不佩服作者代码写得很棒
updateTableDataApi({
id: currentUpdateId.value,
username: formData.username
}).then(() => {
ElMessage.success("修改成功")
dialogVisible.value = false
getTableData()
})
}
// 没有 valid 值,将会退出该方法不执行任何操作
} else {
return false
}
})
}
// 读到这里就知道 currentUpdateId 是一个关于什么的值了
// 我们查到找 html 代码发现当弹窗关闭的时候会触发该方法
// 捋一下思路,也就是弹窗关闭,currentUpdateId 值会清空(用户信息也会清空)
// 所以我们可以知道用户信息是为了下次打开弹框不会发生之前数据还显示出来的状况
// 而 currentUpdateId 则是当前更新 ID ,该值为 undefined 需要执行的是新增,如果当前拥有用户id,那么执行的就是更新
const resetForm = () => {
currentUpdateId.value = undefined
formData.username = ""
formData.password = ""
}
//#endregion
//#region 删
// row 是当前点击列表项的数据
const handleDelete = (row: any) => {
ElMessageBox.confirm(`正在删除用户:${row.username},确认删除?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
// deleteTableDataApi 是封装好的 request(ajax) 方法
// 我们可以看看作者是怎么封装数据请求的
deleteTableDataApi(row.id).then(() => {
ElMessage.success("删除成功")
getTableData()
})
})
}
//#endregion
//#region 改
// 这里的修改方法只是给到了用户数据以及 当前ID 值,当用户点击确认按钮的时候才会发出真在的数据请求,将该值给到服务端处理
// 所以这里只是一些简单的数据修改
const currentUpdateId = ref<undefined | string>(undefined)
const handleUpdate = (row: any) => {
currentUpdateId.value = row.id
formData.username = row.username
formData.password = row.password
dialogVisible.value = true
}
//#endregion
//#region 查
// 表格列表数据 -- 好的代码命名一看就知道是什么意思,是非常棒的
const tableData = ref<any[]>([])
// 输入框框实例
const searchFormRef = ref<FormInstance | null>(null)
// 输入框输入值
const searchData = reactive({
username: "",
phone: ""
})
// 获取所有列表数据进行页面的渲染
const getTableData = () => {
loading.value = true
// 获取列表数据的 API
getTableDataApi({
// 前俩个看命名也知道是分页相关值
currentPage: paginationData.currentPage,
size: paginationData.pageSize,
username: searchData.username || undefined,
phone: searchData.phone || undefined
})
.then((res) => {
// 总数以及列表数据
paginationData.total = res.data.total
tableData.value = res.data.list
})
.catch(() => {
// 如果数据请求发生错误,那么不显示数据列表
tableData.value = []
})
.finally(() => {
// 无论请求成功或者失败 不显示加载图标
loading.value = false
})
}
// 查询
const handleSearch = () => {
// 只有当数据处在第一页的时候才会刷新数据
if (paginationData.currentPage === 1) {
getTableData()
}
// 跳转到第一页
paginationData.currentPage = 1
}
// 重置
const resetSearch = () => {
// resetFields 重置该表单项,将其值重置为初始值,并移除校验结果,这也是 element-ui-plus 组件实例上的方法
searchFormRef.value?.resetFields()
if (paginationData.currentPage === 1) {
getTableData()
}
paginationData.currentPage = 1
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
//#endregion
/** 监听分页参数的变化 */
// 看到这里我们就明白为什么查询和重置页面为什么跳转到第一页就不管了,因为这里在监听着分页参数的变化,这样的完美代码看着是很爽的,为作者点一个大赞
watch([() => paginationData.currentPage, () => paginationData.pageSize], getTableData, { immediate: true })
</script>
<template>
<div class="app-container">
<el-card v-loading="loading" shadow="never" class="search-wrapper">
<el-form ref="searchFormRef" :inline="true" :model="searchData">
<el-form-item prop="username" label="用户名">
<el-input v-model="searchData.username" placeholder="请输入" />
</el-form-item>
<el-form-item prop="phone" label="手机号">
<el-input v-model="searchData.phone" placeholder="请输入" />
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
<el-button :icon="Refresh" @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card v-loading="loading" shadow="never">
<div class="toolbar-wrapper">
<div>
<el-button type="primary" :icon="CirclePlus" @click="dialogVisible = true">新增用户</el-button>
<el-button type="danger" :icon="Delete">批量删除</el-button>
</div>
<div>
<el-tooltip content="下载">
<el-button type="primary" :icon="Download" circle />
</el-tooltip>
<el-tooltip content="刷新表格">
<el-button type="primary" :icon="RefreshRight" circle @click="handleRefresh" />
</el-tooltip>
</div>
</div>
<div class="table-wrapper">
<el-table :data="tableData">
<el-table-column type="selection" width="50" align="center" />
<el-table-column prop="username" label="用户名" align="center" />
<el-table-column prop="roles" label="角色" align="center">
<template #default="scope">
<el-tag v-if="scope.row.roles === 'admin'" effect="plain">admin</el-tag>
<el-tag v-else type="warning" effect="plain">{{ scope.row.roles }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="phone" label="手机号" align="center" />
<el-table-column prop="email" label="邮箱" align="center" />
<el-table-column prop="status" label="状态" align="center">
<template #default="scope">
<el-tag v-if="scope.row.status" type="success" effect="plain">启用</el-tag>
<el-tag v-else type="danger" effect="plain">禁用</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" align="center" />
<el-table-column fixed="right" label="操作" width="150" align="center">
<template #default="scope">
<el-button type="primary" text bg size="small" @click="handleUpdate(scope.row)">修改</el-button>
<el-button type="danger" text bg size="small" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="pager-wrapper">
<el-pagination
background
:layout="paginationData.layout"
:page-sizes="paginationData.pageSizes"
:total="paginationData.total"
:page-size="paginationData.pageSize"
:currentPage="paginationData.currentPage"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 新增/修改 -->
<el-dialog
v-model="dialogVisible"
:title="currentUpdateId === undefined ? '新增用户' : '修改用户'"
@close="resetForm"
width="30%"
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" label-position="left">
<el-form-item prop="username" label="用户名">
<el-input v-model="formData.username" placeholder="请输入" />
</el-form-item>
<el-form-item prop="password" label="密码">
<el-input v-model="formData.password" placeholder="请输入" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleCreate">确认</el-button>
</template>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
.search-wrapper {
margin-bottom: 20px;
:deep(.el-card__body) {
padding-bottom: 2px;
}
}
.toolbar-wrapper {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.table-wrapper {
margin-bottom: 20px;
}
.pager-wrapper {
display: flex;
justify-content: flex-end;
}
</style>
这是分页数据接口规范以及方法定义导出
import { reactive } from "vue"
// ts 定义接口 -- 分页数据接口规范
interface IDefaultPaginationData {
total: number
currentPage: number
pageSizes: number[]
pageSize: number
layout: string
}
// ts 定义接口 -- 合并数据接口规范
interface IPaginationData {
total?: number
currentPage?: number
pageSizes?: number[]
pageSize?: number
layout?: string
}
/** 默认的分页参数 */
const defaultPaginationData: IDefaultPaginationData = {
total: 0,
currentPage: 1,
pageSizes: [10, 20, 50],
pageSize: 10,
layout: "total, sizes, prev, pager, next, jumper"
}
export function usePagination(_paginationData: IPaginationData = {}) {
/** 合并分页参数 */
// Object.assign()是对象的静态方法,可以用来复制对象的可枚举属性到目标对象,利用这个特性可以实现对象属性的合并
// 意思就是传过来的值有的话就覆盖,没有就使用默认分页数据,这个处理很完美
const paginationData = reactive(Object.assign({ ...defaultPaginationData }, _paginationData))
/** 改变当前页码 */
const handleCurrentChange = (value: number) => {
paginationData.currentPage = value
}
/** 改变每页显示数据数量 */
const handleSizeChange = (value: number) => {
paginationData.pageSize = value
}
return { paginationData, handleCurrentChange, handleSizeChange }
}
}
我们顺着上面发起请求的导出方法找到了这里(这里位于 src 下的 api 文件夹),这是一些简单的接口定义以及 api 接口的封装,等等,好像有一个奇怪的东西,在依赖包中使用的是 axios ,怎么出现了 request ,肯定还有一个整体封装层,并且应该在那里会有一个请求响应拦截器,我们去看看
import { request } from "@/utils/service"
interface ICreateTableRequestData {
username: string
password: string
}
interface IUpdateTableRequestData {
id: string
username: string
password?: string
}
interface IGetTableRequestData {
/** 当前页码 */
currentPage: number
/** 查询条数 */
size: number
/** 查询参数 */
username?: string
phone?: string
}
type GetTableResponseData = IApiResponseData<{
list: {
createTime: string
email: string
id: string
phone: string
roles: string
status: boolean
username: string
}[]
total: number
}>
/** 增 */
export function createTableDataApi(data: ICreateTableRequestData) {
return request({
url: "table",
method: "post",
data
})
}
/** 删 */
export function deleteTableDataApi(id: string) {
return request({
url: `table/${id}`,
method: "delete"
})
}
/** 改 */
export function updateTableDataApi(data: IUpdateTableRequestData) {
return request({
url: "table",
method: "put",
data
})
}
/** 查 */
export function getTableDataApi(params: IGetTableRequestData) {
return request<GetTableResponseData>({
url: "table",
method: "get",
params
})
}
果然是 😎
import axios, { type AxiosInstance, type AxiosRequestConfig } from "axios"
import { useUserStoreHook } from "@/store/modules/user"
import { ElMessage } from "element-plus"
import { get } from "lodash-es"
import { getToken } from "./cache/cookies"
/** 创建请求实例 */
function createService() {
// 创建一个 Axios 实例
const service = axios.create()
// 请求拦截
service.interceptors.request.use(
(config) => config,
// 发送失败
(error) => Promise.reject(error)
)
// 响应拦截(可根据具体业务作出相应的调整)
service.interceptors.response.use(
(response) => {
// apiData 是 API 返回的数据
const apiData = response.data as any
// 这个 Code 是和后端约定的业务 Code
const code = apiData.code
// 如果没有 Code, 代表这不是项目后端开发的 API
if (code === undefined) {
ElMessage.error("非本系统的接口")
return Promise.reject(new Error("非本系统的接口"))
} else {
switch (code) {
case 0:
// code === 0 代表没有错误
return apiData
default:
// 不是正确的 Code
ElMessage.error(apiData.message || "Error")
return Promise.reject(new Error("Error"))
}
}
},
(error) => {
// Status 是 HTTP 状态码
const status = get(error, "response.status")
switch (status) {
case 400:
error.message = "请求错误"
break
case 401:
// Token 过期时,直接退出登录并强制刷新页面(会重定向到登录页)
useUserStoreHook().logout()
location.reload()
break
case 403:
error.message = "拒绝访问"
break
case 404:
error.message = "请求地址出错"
break
case 408:
error.message = "请求超时"
break
case 500:
error.message = "服务器内部错误"
break
case 501:
error.message = "服务未实现"
break
case 502:
error.message = "网关错误"
break
case 503:
error.message = "服务不可用"
break
case 504:
error.message = "网关超时"
break
case 505:
error.message = "HTTP 版本不受支持"
break
default:
break
}
ElMessage.error(error.message)
return Promise.reject(error)
}
)
return service
}
/** 创建请求方法 */
function createRequestFunction(service: AxiosInstance) {
return function <T>(config: AxiosRequestConfig): Promise<T> {
const configDefault = {
headers: {
// 携带 Token
Authorization: "Bearer " + getToken(),
"Content-Type": get(config, "headers.Content-Type", "application/json")
},
timeout: 5000,
baseURL: import.meta.env.VITE_BASE_API,
data: {}
}
return service(Object.assign(configDefault, config))
}
}
/** 用于网络请求的实例 */
export const service = createService()
/** 用于网络请求的方法 */
export const request = createRequestFunction(service)
我正在使用i18n从头开始构建一个多语言网络应用程序,虽然我自己可以处理一大堆yml文件,但我说的语言(非常)有限,最终我想寻求外部帮助帮助。我想知道这里是否有人在使用UI插件/gem(与django上的django-rosetta不同)来处理多个翻译器,其中一些翻译器不愿意或无法处理存储库中的100多个文件,处理语言数据。谢谢&问候,安德拉斯(如果您已经在rubyonrails-talk上遇到了这个问题,我们深表歉意) 最佳答案 有一个rails3branchofthetolkgem在github上。您可以通过在Gemfi
如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby
我在我的Rails项目中使用Pow和powifygem。现在我尝试升级我的ruby版本(从1.9.3到2.0.0,我使用RVM)当我切换ruby版本、安装所有gem依赖项时,我通过运行railss并访问localhost:3000确保该应用程序正常运行以前,我通过使用pow访问http://my_app.dev来浏览我的应用程序。升级后,由于错误Bundler::RubyVersionMismatch:YourRubyversionis1.9.3,butyourGemfilespecified2.0.0,此url不起作用我尝试过的:重新创建pow应用程序重启pow服务器更新战俘
我已经像这样安装了一个新的Rails项目:$railsnewsite它执行并到达:bundleinstall但是当它似乎尝试安装依赖项时我得到了这个错误Gem::Ext::BuildError:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcheckingforlibkern/OSAtomic.h...yescreatingMakefilemake"DESTDIR="cleanmake"DESTDIR="
假设我有这个范围:("aaaaa".."zzzzz")如何在不事先/每次生成整个项目的情况下从范围中获取第N个项目? 最佳答案 一种快速简便的方法:("aaaaa".."zzzzz").first(42).last#==>"aaabp"如果出于某种原因你不得不一遍又一遍地这样做,或者如果你需要避免为前N个元素构建中间数组,你可以这样写:moduleEnumerabledefskip(n)returnto_enum:skip,nunlessblock_given?each_with_indexdo|item,index|yieldit
按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭10年前。问题1)我想知道rubyonrails是否有功能类似于primefaces的gem。我问的原因是如果您使用primefaces(http://www.primefaces.org/showcase-labs/ui/home.jsf),开发人员无需担心javascript或jquery的东西。据我所知,JSF是一个规范,基于规范的各种可用实现,prim
项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU
我正在尝试创建一个带有项目符号字符的Ruby1.9.3字符串。str="•"+"helloworld"但是,当我输入它时,我收到有关非ASCII字符的语法错误。我该怎么做? 最佳答案 你可以把Unicode字符放在那里。str="\u2022"+"helloworld" 关于ruby-如何在Ruby字符串中插入项目符号字符?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/1195
我的Rails站点使用了一个确实不是很好的gem。每次我需要做一些新的事情时,我最终不得不花费与向实际Rails项目添加代码一样多的时间来为gem添加功能。但我不介意,我将我的Gemfile设置为指向我的gem的GitHub分支(我尝试提交PR,但维护者似乎已经下台)。问题是我真的没有找到一种合理的方法来测试我添加到gem的新东西。在railsc中测试它会特别好,但我能想到的唯一方法是a)更改~/.rvm/gems/.../foo。rb,这看起来不对或者b)升级版本,推送到Github,然后运行bundleup,这除了耗时之外显然是一场灾难,因为我不确定我所做的promise是否正
我一直在尝试使用nanoc用于生成静态网站。我需要组织一个复杂的排列页面,我想让我的内容保持干燥。包含或合并的概念在nanoc系统中如何运作?我已阅读文档,但似乎找不到我想要的内容。例如:我如何获取两个部分内容项并将它们合并到一个新的内容项中。在staticmatic您可以在您的页面中执行以下操作。=partial('partials/shared/navigation')类似的约定在nanoc中如何运作? 最佳答案 这里是nanoc的作者。在nanoc中,部分是布局。因此,您可以拥有layouts/partials/shared/