-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(websites): 完成分类站点 RestFul 风格 CURD 接口
- Loading branch information
Showing
10 changed files
with
750 additions
and
3 deletions.
There are no files selected for viewing
68 changes: 68 additions & 0 deletions
68
src/pages/admin/_components/websites/components/DynamicTag.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
<template> | ||
<el-space wrap> | ||
<el-tag | ||
v-for="tag in dynamicTags" | ||
:key="tag" | ||
closable | ||
:disable-transitions="false" | ||
@close="handleClose(tag)" | ||
> | ||
{{ tag }} | ||
</el-tag> | ||
<el-input | ||
v-if="inputVisible" | ||
ref="InputRef" | ||
v-model="inputValue" | ||
class="w-20" | ||
size="small" | ||
@keyup.enter="handleInputConfirm" | ||
@blur="handleInputConfirm" | ||
/> | ||
<el-button v-else class="button-new-tag" size="small" @click="showInput"> + New Tag </el-button> | ||
</el-space> | ||
</template> | ||
|
||
<script lang="ts" setup> | ||
import { nextTick, ref } from 'vue' | ||
import { ElInput } from 'element-plus' | ||
// 定义props和emits | ||
const props = defineProps<{ | ||
modelValue: string[] | ||
}>() | ||
const emits = defineEmits(['update:modelValue']) | ||
const inputValue = ref('') | ||
const inputVisible = ref(false) | ||
const InputRef = ref<InstanceType<typeof ElInput>>() | ||
const dynamicTags = ref(props.modelValue) | ||
const handleClose = (tag: string) => { | ||
dynamicTags.value.splice(dynamicTags.value.indexOf(tag), 1) | ||
} | ||
const showInput = () => { | ||
inputVisible.value = true | ||
nextTick(() => { | ||
InputRef.value!.input!.focus() | ||
}) | ||
} | ||
const handleInputConfirm = () => { | ||
if (inputValue.value) { | ||
dynamicTags.value.push(inputValue.value) | ||
// 触发更新事件通知父组件 | ||
emits('update:modelValue', dynamicTags.value) | ||
} | ||
inputVisible.value = false | ||
inputValue.value = '' | ||
} | ||
watch( | ||
() => props.modelValue, | ||
(newVal) => { | ||
dynamicTags.value = newVal // 确保 dynamicTags 总是与 modelValue 同步 | ||
}, | ||
{ immediate: true } // 添加 immediate: true 以在初始渲染时立即同步 | ||
) | ||
</script> |
165 changes: 165 additions & 0 deletions
165
src/pages/admin/_components/websites/components/EditModal.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
<!-- | ||
* @Author: 白雾茫茫丶<baiwumm.com> | ||
* @Date: 2024-06-05 10:47:28 | ||
* @LastEditors: 白雾茫茫丶<baiwumm.com> | ||
* @LastEditTime: 2024-06-17 14:32:12 | ||
* @Description: 新增/编辑弹窗 | ||
--> | ||
<template> | ||
<el-dialog :model-value="open" width="500" @close="handleClose"> | ||
<template #header> | ||
{{ websiteId ? `编辑站点: ` : '新增站点' }} | ||
<el-tag v-if="name" type="primary"> | ||
{{ name }} | ||
</el-tag> | ||
</template> | ||
<template #footer> | ||
<el-button type="primary" :loading="confirmLoading" @click="handleConfirm(ruleFormRef)"> | ||
确定 | ||
</el-button> | ||
</template> | ||
<el-form ref="ruleFormRef" :model="form" label-width="auto" :rules="rules"> | ||
<el-form-item label="所属分类" prop="category_id"> | ||
<el-select v-model="form.category_id" clearable filterable placeholder="请选择所属分类"> | ||
<el-option | ||
v-for="item in categoryList" | ||
:key="item.id" | ||
:label="item.name" | ||
:value="item.id" | ||
/> | ||
</el-select> | ||
</el-form-item> | ||
<el-form-item label="网站名称" prop="name"> | ||
<el-input v-model="form.name" maxlength="12" show-word-limit type="text" /> | ||
</el-form-item> | ||
<el-form-item label="网站链接" prop="url"> | ||
<el-input v-model="form.url" type="text" /> | ||
</el-form-item> | ||
<el-form-item label="Logo" prop="logo"> | ||
<el-input v-model="form.logo" type="text" /> | ||
</el-form-item> | ||
<el-form-item label="站点标签" prop="tags"> | ||
<dynamic-tag v-model="form.tags" /> | ||
</el-form-item> | ||
<el-form-item label="分类描述" prop="desc"> | ||
<el-input v-model="form.desc" type="textarea" :rows="3" maxlength="100" show-word-limit /> | ||
</el-form-item> | ||
<el-form-item label="排序" prop="sort"> | ||
<el-input-number v-model="form.sort" :min="1" :max="99" :style="{ width: '100%' }" /> | ||
</el-form-item> | ||
</el-form> | ||
</el-dialog> | ||
</template> | ||
<script setup lang="ts"> | ||
import type { WebsiteEdit, CategoryList } from '~/types' | ||
import type { FormInstance, FormRules } from 'element-plus' | ||
import DynamicTag from './DynamicTag.vue' | ||
import { RESPONSE_STATUS_CODE } from '~/enum' | ||
|
||
const open = ref(false) // 是否显示弹窗 | ||
const name = ref('') // 当前数据 | ||
const confirmLoading = ref(false) | ||
const websiteId = ref() | ||
|
||
const emit = defineEmits(['refresh']) | ||
|
||
// 父组件传递参数 | ||
defineProps<{ | ||
categoryList: CategoryList[] | ||
}>() | ||
|
||
const form = reactive<WebsiteEdit>({ | ||
category_id: '', | ||
name: '', | ||
url: '', | ||
logo: '', | ||
tags: [], | ||
desc: undefined, | ||
sort: 1 | ||
}) | ||
|
||
const ruleFormRef = ref<FormInstance>() | ||
// 校验 logo url | ||
const validatorLogo = ( | ||
_: any, | ||
value: any, | ||
callback: (error?: string | Error | undefined) => void | ||
) => { | ||
if (!value) { | ||
callback(new Error('请输入站点logo')) | ||
} else { | ||
const reg = /^https:\/\/.*\.(jpg|jpeg|png|gif|bmp|svg)(\?|$)/i | ||
if (reg.test(value)) { | ||
callback() | ||
} else { | ||
callback(new Error('请输入正确的url')) | ||
} | ||
} | ||
} | ||
// 校验网站 url | ||
const validatorUrl = ( | ||
_: any, | ||
value: any, | ||
callback: (error?: string | Error | undefined) => void | ||
) => { | ||
if (!value) { | ||
callback(new Error('请输入网站链接')) | ||
} else { | ||
const reg = | ||
/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/ | ||
if (reg.test(value)) { | ||
callback() | ||
} else { | ||
callback(new Error('请输入正确的url')) | ||
} | ||
} | ||
} | ||
// 表单规则校验 | ||
const rules = reactive<FormRules<WebsiteEdit>>({ | ||
category_id: [{ required: true, message: '请选择所属分类', trigger: 'change' }], | ||
name: [ | ||
{ required: true, message: '请输入站点名称', trigger: 'blur' }, | ||
{ min: 1, max: 12, message: '长度1-12个字符', trigger: 'blur' } | ||
], | ||
url: [{ required: true, validator: validatorUrl, trigger: 'blur' }], | ||
logo: [{ required: true, validator: validatorLogo, trigger: 'blur' }], | ||
tags: [{ required: true, message: '请输入站点标签', trigger: 'blur' }] | ||
}) | ||
|
||
// 暴露方法 | ||
defineExpose({ open, name, form, websiteId }) | ||
|
||
// 关闭回调 | ||
const handleClose = async () => { | ||
open.value = false | ||
name.value = '' | ||
websiteId.value = undefined | ||
ruleFormRef.value?.resetFields() | ||
} | ||
|
||
// 确定回调 | ||
const handleConfirm = async (formEl: FormInstance | undefined) => { | ||
if (!formEl) return | ||
await formEl.validate(async (valid) => { | ||
if (valid) { | ||
confirmLoading.value = true | ||
await $fetch('/api/websites', { | ||
method: websiteId.value ? 'put' : 'post', | ||
body: Object.assign(form, { id: websiteId.value }) | ||
}) | ||
.then(({ code, msg }) => { | ||
if (code === RESPONSE_STATUS_CODE.SUCCESS) { | ||
ElMessage.success('操作成功') | ||
handleClose() | ||
emit('refresh') | ||
} else { | ||
ElMessage.error(msg) | ||
} | ||
}) | ||
.finally(() => { | ||
confirmLoading.value = false | ||
}) | ||
} | ||
}) | ||
} | ||
</script> |
91 changes: 91 additions & 0 deletions
91
src/pages/admin/_components/websites/components/TableTemplate.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
<template> | ||
<el-table v-loading="pending" :data="dataSource" border stripe table-layou="auto"> | ||
<el-table-column type="index" label="#" width="50" align="center" /> | ||
<el-table-column | ||
prop="name" | ||
label="站点名称" | ||
align="center" | ||
show-overflow-tooltip | ||
min-width="160" | ||
> | ||
<template #default="{ row }"> | ||
<el-space wrap> | ||
<el-tag type="primary">{{ row.name }}</el-tag> | ||
<el-link :href="row.url" target="_blank"> | ||
<Icon name="ri:share-box-fill" class="h-5 w-5" /> | ||
</el-link> | ||
</el-space> | ||
</template> | ||
</el-table-column> | ||
<el-table-column prop="logo" label="Logo" align="center" min-width="100"> | ||
<template #default="{ row }"> | ||
<NuxtImg :src="row.logo" alt="logo" class="w-10 h-10 m-auto" /> | ||
</template> | ||
</el-table-column> | ||
<el-table-column | ||
prop="tags" | ||
label="站点标签" | ||
align="center" | ||
show-overflow-tooltip | ||
min-width="200" | ||
> | ||
<template #default="{ row }"> | ||
<el-space wrap> | ||
<el-tag v-for="(tag, index) in row.tags" :key="index" type="info">{{ tag }}</el-tag> | ||
</el-space> | ||
</template> | ||
</el-table-column> | ||
<el-table-column | ||
prop="desc" | ||
label="分类描述" | ||
align="center" | ||
show-overflow-tooltip | ||
min-width="100" | ||
> | ||
<template #default="{ row }"> | ||
{{ row.desc || '--' }} | ||
</template> | ||
</el-table-column> | ||
<el-table-column | ||
prop="desc" | ||
label="所属分类" | ||
align="center" | ||
show-overflow-tooltip | ||
min-width="100" | ||
> | ||
<template #default="{ row }"> | ||
<el-tag type="success">{{ row.categorys.name }}</el-tag> | ||
</template> | ||
</el-table-column> | ||
<el-table-column prop="sort" label="排序" align="center" sortable min-width="100" /> | ||
<el-table-column prop="created_at" label="创建时间" align="center" width="180" sortable> | ||
<template #default="{ row }"> | ||
<div>{{ formatDateTime(row.created_at) }}</div> | ||
</template> | ||
</el-table-column> | ||
<el-table-column prop="updated_at" label="更新时间" align="center" width="180" sortable> | ||
<template #default="{ row }"> | ||
<div>{{ formatDateTime(row.updated_at) }}</div> | ||
</template> | ||
</el-table-column> | ||
<el-table-column label="操作" width="140" align="center" fixed="right"> | ||
<template #default="{ row }"> | ||
<el-button size="small" @click="emit('handleEdit', row)"> 编辑 </el-button> | ||
<el-button size="small" type="danger" @click="emit('handleDelete', row)"> 删除 </el-button> | ||
</template> | ||
</el-table-column> | ||
</el-table> | ||
</template> | ||
<script setup lang="ts"> | ||
import { formatDateTime } from '@/utils' | ||
import type { CategoryList } from '~/types' | ||
// 父组件传递参数 | ||
defineProps<{ | ||
pending: boolean // 数据加载 loading | ||
dataSource: CategoryList[] // 表格数据 | ||
}>() | ||
// 父组件方法 | ||
const emit = defineEmits(['handleEdit', 'handleDelete']) | ||
</script> |
Oops, something went wrong.