表格表单封装
表格组件封装
src\components\admin\g_list.vue
javascript
<script setup lang='ts'>
import type { listResponse, paramsType, baseResponse,optionsType,optionsFunc } from "@/api/index";
import {defaultDeleteApi} from "@/api/index";
import { dateCurrentFormat, dateTemFormat } from "@/utils/date";
import { Message, type TableColumnData,type TableRowSelection,type TableData } from "@arco-design/web-vue";
import { reactive, ref } from "vue";
const loading = ref(false)
// 父组件传递给子组件的参数类型;
interface Props {
url: (params?: paramsType) => Promise<baseResponse<listResponse<any>>>, //请求路由,返回的是一个函数,和接口的函数类型保持一致
columns: TableColumnData[], //字段数据
rowKey?:string //(主要使用为默认删除的字段名称)
noDefaultDelete?: boolean // 不启用默认删除
searchPlaceholder?: string //搜索框提示,
addLabel?: string //添加按钮名称,
editLabel?: string //编辑按钮名称
deleteLabel?: string
limit?: number
noAdd?: boolean
noEdit?: boolean
noDelete?: boolean
noActionGroup?:boolean
noCheck?: boolean //是否打开多选
actionGroup?: actionGroupType[] //操作组
noBatchDelete?: boolean // 是否没有批量删除
filterGroup?: filterGroupType[] //过滤组
defaultParams?: Object
noPage?:boolean
}
// 定义父组件传递给子组件的参数
const props = defineProps<Props>()
const {
rowKey ='id',//如果rowkey没有传,就给默认值id,
noDefaultDelete = false,
searchPlaceholder = "搜索",//搜索框提示,默认是搜索
addLabel = "添加",
editLabel = "编辑",
deleteLabel = "删除",
limit = 10,
} = props
// 表格组件的数据类型
const data = reactive<listResponse<any>>({
list: [],
count: 0,
})
// 请求参数类型
const params = reactive<paramsType>({
limit: props.noPage ? -1 : limit,
})
// 获取数据的函数
async function getlist(newParams?: paramsType) {
// 判断有没有默认的params
if (props.defaultParams) {
Object.assign(params, props.defaultParams)
}
loading.value = true
if (newParams) {
Object.assign(params, newParams)
}
const res = await props.url(params)
loading.value = false
if (res.code) {
Message.error(res.msg)
return
}
data.list = res.data.list || []
data.count = res.data.count
}
// 打开多选的配置
const selectedKeys = ref([]);
const rowSelection = reactive<TableRowSelection>({
type: 'checkbox',
showCheckedAll: true,
onlyCurrent: false,
});
const actionValue = ref()
export interface actionGroupType {
label: string
value?: number
callback: (keyList: number[]) => void
}
const actionGroupOptions = ref<actionGroupType[]>([])
// 操作组函数:找到这个 actionGroupOptions 值,然后如果有的话就执行相对应的回调函数
function actionGroupAction() {
const action = actionGroupOptions.value.find((value) => value.value === actionValue.value)
if (action) {
action.callback(selectedKeys.value)
}
}
// 初始化操作组,在开启默认批量删除时,添加一个批量删除的action,并执行回调函数删除对应的keyList的数据
function initActionGroup() {
let index = 0
if (!props.noBatchDelete) {
actionGroupOptions.value.push({
label: "批量删除",
value: 1,
callback: (keyList: number[]) => {
baseDelete(keyList)
selectedKeys.value = []
}
})
index = 1
}
index++
const actionGroup = props.actionGroup || []
for (const action of actionGroup) {
actionGroupOptions.value.push({
label: action.label,
value: index,
callback: action.callback,
})
}
}
initActionGroup()
// 过滤组类型
export interface filterGroupType {
label: string
source: optionsType[] | optionsFunc // 既可以处理options类型,也可以处理函数类型
options?: optionsType[]
column?: string
params?: paramsType
callback?: (value: number | string) => void
width?: number
}
const filterGroups = ref<filterGroupType[]>([])
async function initFilterGroup() {
filterGroups.value = []
// 判断是否是函数类型,是的话,获取到数据,进行赋值
for (const f of props.filterGroup || []) {
if (typeof f.source === 'function') {
const res = await f.source(f.params)
if (res.code) {
Message.error(res.msg)
continue
}
f.options = res.data
} else {
f.options = f.source
}
if (!f.callback) {
// 如果没有callback,那就走默认行为
f.callback = (value) => {
if (f.column) {
const p: { [key: string]: any } = {}
p[f.column] = value
console.log(p)
getlist(p)
}
}
}
filterGroups.value.push(f)
}
}
initFilterGroup()
function edit(record: any) {
// if (props.formList?.length) {
// modalFormRef.value.setForm(record)
// visible.value = true
// return
// }
emits("edit", record)
}
function add() {
// if (props.formList?.length) {
// visible.value = true
// return
// }
emits("add")
}
function rowClick(record: TableData, ev: Event) {
emits("row-click", record)
}
// 抛给父组件的时间
const emits = defineEmits<{
(e: 'delete', keyList: number[] | string[]): void
(e: 'add'): void
(e: 'edit', record: any): void
(e: "row-click", record: any): void //这个是table组件默认自带的点击行的触发函数,将其保留传到子组件中
}>()
// 删除逻辑进行封装
async function baseDelete(keyList: number[]) {
if (noDefaultDelete) {
// 不启用默认删除,将删除时间抛给父组件
emits("delete", keyList)
return
}
// 拿到传递接口的路由,拿到路由路径
const array = /\"(.*?)\"/.exec(props.url.toString())
if (array?.length !== 2) {
return
}
const url = array[1]
const res = await defaultDeleteApi(url, keyList)
if (res.code) {
Message.error(res.msg)
return
}
getlist()
Message.success(res.msg)
}
// 刷新
function refresh() {
getlist()
Message.success("刷新成功")
}
async function remove(record: any) {
const key = record[rowKey]
baseDelete([key])
}
// 页数发生变化时
function pageChange(){
getlist()
}
// 搜索
function search(){
getlist()
}
getlist()
// 将父组件的数据函数抛出
defineExpose({
getlist,
data,
})
</script>
<template>
<div class="g_list_com">
<div class="g_list_head">
<div class="action_create" v-if="!noAdd">
<a-button type="primary" @click="add">{{ addLabel }}</a-button>
</div>
<div class="action_group" v-if="!noActionGroup">
<a-select style="width: 140px;" placeholder="操作" v-model="actionValue"
:options="actionGroupOptions"></a-select>
<a-button type="primary" status="danger" @click="actionGroupAction" v-if="actionValue">执行</a-button>
</div>
<div class="actions_search">
<a-input-search :placeholder="searchPlaceholder" v-model="params.key" @search="search"></a-input-search>
</div>
<div class="action_search_slot">
<slot name="search_other"></slot>
</div>
<div class="action_filter">
<a-select v-for="item in filterGroups" :style="{width: item.width + 'px'}" allow-clear @change="item.callback" :placeholder="item.label" :options="item.options" ></a-select>
</div>
<div class="action_refresh" @click="refresh">
<icon-refresh></icon-refresh>
</div>
</div>
<div class="g_list_body">
<a-spin :loading="loading" tip="加载中...">
<div class="g_list_table">
<a-table @row-click="rowClick" :data="data.list" :row-key="rowKey" v-model:selectedKeys="selectedKeys"
:row-selection="props.noCheck ? undefined : rowSelection " >
<!-- 通过 #columns 插槽和 <a-table-column> 组件可以使用模板的方法自定义列渲染 -->
<template #columns>
<template v-for="col in props.columns">
<a-table-column v-if="col.dataIndex" v-bind="col"> </a-table-column>
<a-table-column v-else-if="col.slotName" v-bind="col">
<template #cell="data">
<!-- 如果是slotName是actin的话,显示操作按钮 -->
<div class="col_actions" v-if="col.slotName ==='action'">
<!-- action_left 预留在左边的按钮-->
<slot v-bind="data" name="action_left"></slot>
<!-- 编辑按钮 -->
<a-button type="primary" @click="edit(data.record)" v-if="!noEdit">{{ editLabel }}</a-button>
<!-- 删除按钮 -->
<a-popconfirm @ok="remove(data.record)" content="你确定要删除该记录吗?">
<a-button type="primary" status="danger" v-if="!noDelete">{{ deleteLabel }}</a-button>
</a-popconfirm>
<!-- action_left 预留在右边边的按钮-->
<slot v-bind="data" name="action_rigth"></slot>
</div>
<!-- 如果字段中有dateFormat就使用工具中的函数处理时间格式 -->
<div v-if="col.dateFormat">
{{ dateTemFormat(data.record[col.slotName],col.dateFormat) }}
</div>
<!-- 使用卡槽将传递给子组件,实现自定义 -->
<slot v-else :name="col.slotName" v-bind="data"></slot>
</template>
</a-table-column>
</template>
</template>
</a-table>
</div>
<div class="g_list_page" v-if="!noPage">
<!-- 分页 -->
<a-pagination show-total @change="pageChange" :total="data.count" v-model:current="params.page" :page-size="params.limit"></a-pagination>
</div>
</a-spin>
</div>
</div>
</template>
<style lang='less'>
.g_list_com {
.g_list_head {
padding: 20px 20px 10px 20px;
border-bottom: @g_border;
display: flex;
align-items: center;
position: relative;
.action_create,
.action_group,
.action_search,
.action_search_slot {
margin-right: 10px;
}
.action_group {
display: flex;
align-items: center;
button {
margin-left: 10px;
}
}
.action_filter {
.arco-select {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
}
.action_refresh {
position: absolute;
right: 20px;
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--color-fill-2);
border-radius: 5px;
cursor: pointer;
}
}
.g_list_body {
padding: 10px 20px 20px 20px;
>.arco-spin {
width: 100%;
}
.g_list_page {
display: flex;
justify-content: center;
margin-top: 10px;
}
.col_actions {
button {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
}
}
}
</style>
src\views\admin\user_manage\index.vue
javascript
<script setup lang='ts'>
import G_list from '@/components/admin/g_list.vue';
import { type filterGroupType} from '@/components/admin/g_list.vue';
import {userListApi,type userListType} from "@/api/user_api"
import { articleCategoryOptionsApi } from '@/api';
import { ref } from 'vue';
const fListRef = ref()
const columns = [
{
title:"ID",dataIndex:"id"
},
{
title:"昵称",dataIndex:"nickname"
},
{
title:"头像",slotName:"avatar"
},
{
title:"角色",dataIndex:"role"
},
{
title:"时间",slotName:"created_at",dateFormat:"current"
},
{
title:"最后登录时间",slotName:"last_login_date",dateFormat:"date"
}
,
{
title:"操作",slotName:"action"
},
]
// 当启用默认删除时的删除函数
function remove(keyList:number[]|string[]){
console.log(keyList)
}
// 另外添加操作组,添加label 然后执行回调函数,会传递到父组件添加到actionGroupOptions中去
const actionGroup = [
{
label:"批量打印",
callback: function(keyList:number[]){
console.log(keyList)
}
}
]
// 过滤组的配置
const filters:filterGroupType[]=[
{
label:"过滤用户",
source:[
{label:"管理员","value":1},
{label:"用户","value":2},
{label:"游客","value":3},
],
column:"role",
width:140
},
{
// 通过回调函数+父组件抛出的getlist进行时间过滤
label:"过滤用户2",
source:[
{label:"管理员","value":1},
{label:"用户","value":2},
{label:"游客","value":3},
],
column:"role",
width:140,
callback:(value:number|string)=>{
console.log(fListRef.value.data) //验证抛出的data能否获取到
fListRef.value.getlist({role:value}) //验证抛出的getlist能否正常使用
},
},
{
label:"过滤ip",
source:[
{label:"xxxx2","value":22},
{label:"xxxx3","value":33},
],
column:"ip",
width:140
},
{
label:"过滤分类",
source:articleCategoryOptionsApi,
column:"ip",
width:140
}
]
// 点击行获取其中数据
function rowClick(record:any){
console.log(record)
}
</script>
<template>
<div>
<G_list ref="fListRef" :url="userListApi" :actionGroup="actionGroup":columns="columns" noBatchDelete :filterGroup="filters" @row-click="rowClick">
<template #avatar="{record}":{record:userListType}>
<a-avatar :image-url="record.avatar"></a-avatar>
</template>
<template #search_other>
</template>
</G_list>
</div>
</template>
<style lang="less">
</style>
src\utils\date.ts
javascript
// npm install dayjs
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime"
import "dayjs/locale/zh-cn"
dayjs.extend(relativeTime)
dayjs.locale('zh-cn')
export type dateTemType = "datetime" | "date" | "time" | "current" | undefined
//年月入显示时分秒
export function dateTimeFormat(date: string): string {
return dayjs(date).format("YYYY-MM-DD HH:mm:ss")
}
// 显示年月日
export function dateFormat(date: string): string {
return dayjs(date).format("YYYY-MM-DD")
}
// 显示时分秒
export function timeFormat(date: string): string {
return dayjs(date).format("HH:mm:ss")
}
// 显示相对时间
export function dateCurrentFormat(date: string):string{
return dayjs().to(dayjs(date))
}
// 时间格式类型函数封装
export function dateTemFormat(date: string, name?: dateTemType){
if (name === "date"){
return dateFormat(date)
}
if (name === "time"){
return timeFormat(date)
}
if (name === "current"){
return dateCurrentFormat(date)
}
return dateTimeFormat(date)
}
src\api\user_api.ts
javascript
import { type baseResponse,type paramsType, type listResponse,useAxios} from "@/api/index";
export interface pwdLoginRequest {
val :string
password:string
}
export function pwdLoginApi(data:pwdLoginRequest):Promise<baseResponse<string>>{
return useAxios.post("api/user/login",data)
}
export interface userInfoType {
"user_id": number
"code_age": number
"avatar": string
"nickname": string
"look_count": number
"article_count": number
"fans_count": number
"place": string
"open_collect": boolean,
"open_follow": boolean,
"open_fans": boolean,
"home_style_id": number
"relationship": 0| 1 | 2 | 3 | 4
}
export function userInfoApi(userID: number): Promise<baseResponse<userInfoType>> {
return useAxios.get("/api/user/base", {params: {id: userID}})
}
export function userLogoutApi(): Promise<baseResponse<string>> {
return useAxios.delete("/api/user/logout")
}
export interface userListType {
"id": number,
"nickname": string,
"username": string,
"avatar": string ,
"ip": string,
"addr": string,
"article_count": number,
"index_count": number,
"created_at": string,
"last_login_date": string,
"role": number
}
export function userListApi(params?: paramsType): Promise<baseResponse<listResponse<userListType>>> {
return useAxios.get("/api/user", {params})
}
表单组件封装
src\components\admin\g_modal_form.vue
javascript
<script setup lang="ts">
import type {FieldRule} from "@arco-design/web-vue";
import {reactive, ref} from "vue";
import type {optionsFunc, optionsType} from "@/api";
import {Message} from "@arco-design/web-vue";
export interface formListType {
label: string
field: string
type?: "input" | "textarea" | "select" | "switch" | "radio" | "password"
validateTrigger?: "focus" | "input" | "blur" | "change" | ("focus" | "input" | "blur" | "change")[];
rules?: FieldRule<any> | FieldRule<any>[]
source?: optionsType[] | optionsFunc
options?: optionsType[]
autoSize?: boolean | {
minRows?: number | undefined;
maxRows?: number | undefined;
}
multiple?: boolean
editDisable?: boolean // 是否编辑不可见
}
export type emitFnType = (val: boolean) => void
interface Props {
visible: boolean
formList: formListType[]
addLabel: string // 添加的时候,显示的名字
editLabel?: string // 编辑的时候,显示的名字,如果没有就用添加的
}
const props = defineProps<Props>()
const formList = ref<formListType[]>([])
async function initForm() {
formList.value = []
for (const f of props.formList) {
if (typeof f.source === 'function') {
const res = await f.source()
if (res.code) {
Message.error(res.msg)
continue
}
f.options = res.data
} else {
f.options = f.source
}
formList.value.push(f)
}
}
initForm()
const emits = defineEmits<{
(e: "update:visible", visible: boolean): void
(e: "create", form: object, fn?: emitFnType): void
(e: "update", form: object, fn?: emitFnType): void
}>()
function cancel() {
formRef.value.clearValidate()
formRef.value.resetFields()
isEdit.value = false
emits("update:visible", false)
}
type formType = {
[key: string]: any
}
const form = reactive<formType>({})
const formRef = ref()
async function beforeOk() {
const val = await formRef.value.validate()
if (val) return false
const emitFn = (val: boolean) => {
if (val) {
cancel()
return
}
}
if (isEdit.value) {
emits("update", form, emitFn)
} else {
emits("create", form, emitFn)
}
}
const isEdit = ref(false)
function setForm(formObj: any) {
isEdit.value = true
Object.assign(form, formObj)
}
defineExpose({
setForm
})
</script>
<template>
<a-modal :title="isEdit ? editLabel ? editLabel : addLabel : addLabel" :visible="props.visible" @cancel="cancel"
:on-before-ok="beforeOk">
<a-form ref="formRef" :model="form">
<template v-for="item in formList" >
<a-form-item v-if="!isEdit || (isEdit && !item.editDisable)" :field="item.field" :label="item.label" :rules="item.rules"
:validate-trigger="item.validateTrigger as 'blur'">
<template v-if="item.type === 'input' || item.type === 'password' ">
<a-input v-model="form[item.field]" :type="item.type === 'input' ? 'text' : 'password'" :placeholder="item.label"></a-input>
</template>
<template v-else-if="item.type === 'select'">
<a-select :multiple="item.multiple as boolean" v-model="form[item.field]" :placeholder="item.label"
:options="item.options as optionsType[]" allow-clear></a-select>
</template>
<template v-else-if="item.type === 'switch'">
<a-switch v-model="form[item.field]"></a-switch>
</template>
<template v-else-if="item.type === 'radio'">
<a-radio-group v-model="form[item.field]" :options="item.options as optionsType[]"></a-radio-group>
</template>
<template v-else-if="item.type === 'textarea'">
<a-textarea v-model="form[item.field]" :placeholder="item.label" allow-clear
:auto-size="item.autoSize as boolean"></a-textarea>
</template>
<template v-else>
<slot :name="item.field" :form="form"></slot>
</template>
<template #help>
<slot :name="`${item.field}_help`" :value="form[item.field]"></slot>
</template>
</a-form-item>
</template>
</a-form>
<template #footer>
<slot name="footer" :form="form"></slot>
</template>
</a-modal>
</template>
<style scoped>
</style>
src\views\admin\user_manage\index.vue【使用】
javascript
<script setup lang='ts'>
import G_list from '@/components/admin/g_list.vue';
import { type filterGroupType} from '@/components/admin/g_list.vue';
import {userListApi,type userListType} from "@/api/user_api"
import { articleCategoryOptionsApi } from '@/api';
import { ref,reactive } from 'vue';
import G_modal_form from '@/components/admin/g_modal_form.vue';
import { ValidateTrigger } from '@arco-design/web-vue';
const fListRef = ref()
const columns = [
{
title:"ID",dataIndex:"id"
},
{
title:"昵称",dataIndex:"nickname"
},
{
title:"头像",slotName:"avatar"
},
{
title:"角色",dataIndex:"role"
},
{
title:"时间",slotName:"created_at",dateFormat:"current"
},
{
title:"最后登录时间",slotName:"last_login_date",dateFormat:"date"
}
,
{
title:"操作",slotName:"action"
},
]
// 当启用默认删除时的删除函数
function remove(keyList:number[]|string[]){
console.log(keyList)
}
// 另外添加操作组,添加label 然后执行回调函数,会传递到父组件添加到actionGroupOptions中去
const actionGroup = [
{
label:"批量打印",
callback: function(keyList:number[]){
console.log(keyList)
}
}
]
// 过滤组的配置
const filters:filterGroupType[]=[
{
label:"过滤用户",
source:[
{label:"管理员","value":1},
{label:"用户","value":2},
{label:"游客","value":3},
],
column:"role",
width:140
},
{
// 通过回调函数+父组件抛出的getlist进行时间过滤
label:"过滤用户2",
source:[
{label:"管理员","value":1},
{label:"用户","value":2},
{label:"游客","value":3},
],
column:"role",
width:140,
callback:(value:number|string)=>{
console.log(fListRef.value.data) //验证抛出的data能否获取到
fListRef.value.getlist({role:value}) //验证抛出的getlist能否正常使用
},
},
{
label:"过滤ip",
source:[
{label:"xxxx2","value":22},
{label:"xxxx3","value":33},
],
column:"ip",
width:140
},
{
label:"过滤分类",
source:articleCategoryOptionsApi,
column:"ip",
width:140
}
]
// 点击行获取其中数据
function rowClick(record:any){
console.log(record)
}
// ==================================================================================
import type { formListType } from '@/components/admin/g_modal_form.vue';
const formList:formListType[] = [
{
label:"昵称",field:"nickname",type:"input",rules:{required:true,message:'昵称为必填字段!'},validateTrigger:"blur"
},
{
label:"角色",field:"role",type:"select",rules:{required:true,message:'昵称为必填字段!'},validateTrigger:"blur",source:[
{label:"管理员",value:1},
{label:"用户",value:2}
]
},
{
label:"角色",field:"role",type:"select",rules:{required:true,message:'昵称为必填字段!'},validateTrigger:"blur",source:articleCategoryOptionsApi
},
{
label:"角色",field:"role",type:"radio",rules:{required:true,message:'昵称为必填字段!'},validateTrigger:"blur",source:[
{label:"管理员",value:1},
{label:"用户",value:2}
]
},
{
label:"角色",field:"role",type:"switch",rules:{required:true,message:'昵称为必填字段!'},validateTrigger:"blur",source:[
{label:"管理员",value:1},
{label:"用户",value:2}
]
},
{
label:"角色",field:"role",type:"textarea",rules:{required:true,message:'昵称为必填字段!'},validateTrigger:"blur",source:[
{label:"管理员",value:1},
{label:"用户",value:2}
],
autoSize:{minRows:3,maxRows:5}
},
{
label:"角色",field:"role",type:"select",rules:{required:true,message:'昵称为必填字段!'},validateTrigger:"blur",source:[
{label:"管理员",value:1},
{label:"用户",value:2}
],
multiple:true
},
{
label:"角色",field:"role",rules:{required:true,message:'昵称为必填字段!'},validateTrigger:"blur",source:[
{label:"管理员",value:1},
{label:"用户",value:2}
],
multiple:true
},
]
function formOk(form:object,fnc?:(val:boolean)=>void){
console.log(form)
// fnc(true)
}
const modalFormRef = ref()
function edit(record:userListType){
modalFormRef.value.setForm(record)
visible.value = true
}
function create(form:object,fnc?:(val:boolean)=>void){
console.log("create",form)
}
function update(form:object,fnc?:(val:boolean)=>void){
console.log("update",form)
}
const form = reactive({})
const visible = ref(true)
</script>
<template>
<div>
<G_modal_form ref="modalFormRef" v-model:visible="visible" add-label="创建用户" edit-label="编辑用户" :form-list="formList" @create="create" @update="update">
<template #role="{form}">
自定义渲染例如
<a-select v-model="form['role']" placeholder="自定义role" :options="[{label:'超级管理员xxxx',value:1000}]"></a-select>
</template>
<!-- <template #footer="{form}">自定义的脚页 例如
<a-button type="primary" @click="formOk(form)">ok</a-button>
</template> -->
<template #nickname_help="{value}">
{{ value }}根据字段显示对应的help信息
</template>
</G_modal_form>
<G_list @edit="edit" @add="visible=true" ref="fListRef" :url="userListApi" :actionGroup="actionGroup":columns="columns" noBatchDelete :filterGroup="filters" @row-click="rowClick">
<template #avatar="{record}":{record:userListType}>
<a-avatar :image-url="record.avatar"></a-avatar>
</template>
<template #search_other>
</template>
</G_list>
</div>
</template>
<style lang="less">
</style>
表单组件合并到列表组件中
src\components\admin\g_list.vue【修改】
javascript
...
...
interface Props {
url: (params?: paramsType) => Promise<baseResponse<listResponse<any>>>, //请求路由,返回的是一个函数,和接口的函数类型保持一致
columns: TableColumnData[], //字段数据
rowKey?:string //(主要使用为默认删除的字段名称)
noDefaultDelete?: boolean // 不启用默认删除
searchPlaceholder?: string //搜索框提示,
addLabel?: string //添加按钮名称,
editLabel?: string //编辑按钮名称
deleteLabel?: string
limit?: number
noAdd?: boolean
noEdit?: boolean
noDelete?: boolean
noActionGroup?:boolean
noCheck?: boolean //是否打开多选
actionGroup?: actionGroupType[] //操作组
noBatchDelete?: boolean // 是否没有批量删除
filterGroup?: filterGroupType[] //过滤组
defaultParams?: Object
noPage?:boolean
// 表单组件相关
formList?:formListType[]
addFormLabel?:string
editFormLabel?:string
}
...
...
function edit(record: any) {
if (props.formList?.length) {
modalFormRef.value.setForm(record)
visible.value = true
return
}
emits("edit", record)
}
function add() {
if (props.formList?.length) {
visible.value = true
return
}
emits("add")
}
...
...
// ======================表单组件相关=====================
import G_modal_form from "./g_modal_form.vue";
const modalFormRef = ref()
const visible = ref(false)
</script>
...
...
<template>
<div class="g_list_com">
<!-- 将表格组件嵌套进来 -->
<g_modal_form
ref="modalFormRef"
v-if="props.formList?.length"
:add-label="props.addFormLabel"
:edit-label="props.editFormLabel"
v-model:visible="visible"
:form-list="props.formList"
></g_modal_form>
...
...
src\views\admin\user_manage\index.vue【使用情况发生变化,直接传参到list组件中】
javascript
...
...
<template>
<div>
<!-- <G_modal_form ref="modalFormRef" v-model:visible="visible" add-label="创建用户" edit-label="编辑用户" :form-list="formList" @create="create" @update="update">
<template #role="{form}">
自定义渲染例如
<a-select v-model="form['role']" placeholder="自定义role" :options="[{label:'超级管理员xxxx',value:1000}]"></a-select>
</template>
<template #footer="{form}">自定义的脚页 例如
<a-button type="primary" @click="formOk(form)">ok</a-button>
</template>
<template #nickname_help="{value}">
{{ value }}根据字段显示对应的help信息
</template>
</G_modal_form> -->
<G_list @edit="edit" @add="visible = true" ref="fListRef" :url="userListApi" :actionGroup="actionGroup"
:columns="columns" noBatchDelete :filterGroup="filters" @row-click="rowClick" add-form-label="创建用户" edit-form-label="编辑用户" :form-list="formList">
<template #avatar="{ record }" :{record:userListType}>
<a-avatar :image-url="record.avatar"></a-avatar>
</template>
<template #search_other>
</template>
</G_list>
</div>
</template>
...
...
表单组件的默认创建和默认更新
src\components\admin\g_list.vue【表格组件中添加默认删除和默认更新的钩子即函数】
javascript
....
....
// ======================表单组件相关=====================
import G_modal_form from "./g_modal_form.vue";
import {defaultDeleteApi, defaultPostApi, defaultPutApi} from "@/api";
import type { formListType,emitFnType } from "./g_modal_form.vue";
const modalFormRef = ref()
const visible = ref(false)
// 默认删除和更新
async function create(form: any, fn?: emitFnType) {
const array = /\"(.*?)\"/.exec(props.url.toString())
if (array?.length !== 2) {
return
}
const url = array[1]
const res = await defaultPostApi(url, form)
if (res.code) {
Message.error(res.msg)
return
}
getlist()
if (fn) {
fn(true)
}
}
async function update(form: any, fn?: emitFnType) {
const array = /\"(.*?)\"/.exec(props.url.toString())
if (array?.length !== 2) {
return
}
const url = array[1]
const res = await defaultPutApi(url, form)
if (res.code) {
Message.error(res.msg)
return
}
getlist()
if (fn) {
fn(true)
}
}
</script>
....
....
<template>
<div class="g_list_com">
<!-- 将表格组件嵌套进来 -->
<g_modal_form
ref="modalFormRef"
v-if="props.formList?.length"
:add-label="props.addFormLabel"
:edit-label="props.editFormLabel"
@create="create"
@update="update"
v-model:visible="visible"
:form-list="props.formList"
></g_modal_form>
src\mock\user_mock.ts【mock数据】
javascript
import {mock} from "mockjs";
import {type MockjsRequestOptions} from "mockjs"
export function userMock(){
mock(/.*?api\/user\/login/, function (options: MockjsRequestOptions){
// token 2028年十月过期
return {
code: 0,
data: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJkb3VibGUiLCJ1c2VyX2lkIjoxNSwicm9sZV9pZCI6MSwiZXhwIjoxODU0Njg5MzY3LCJpc3MiOiJkb3VibGVXZW4ifQ.c1kr2Nw8cRly-XvVBmwskbSb7OUnl0Q67DP1ujIRgt4",
msg: "成功"
}
})
mock(/.*?api\/user\/logout/, function (options: MockjsRequestOptions){
return {
code: 0,
data: "",
msg: "成功"
}
})
mock(/.*?api\/user\/base/, function (options: MockjsRequestOptions){
return mock({
"code": 0,
"data": {
"id": 15,
"created_at": "2026-01-12T16:24:53.826+08:00",
"username": "double",
"nickname": "mock用户",
"avatar": "@img(100x100)",
"abstract": "",
"register_source": 3,
"code_age": 1,
"role": 1,
"user_id": 15,
"like_tags": null,
"update_username_date": null,
"open_collect": true,
"open_follow": true,
"open_fans": true,
"home_style_id": 1,
"index_count": 0,
"email": "",
"usePassword": true
},
"msg": "成功"
})
}),
mock(/.*?\/api\/user/, function (){
return mock({
code: 0,
msg: "成功",
data: {
count: 4,
"list|3-10": [
{
"id": 14,
"nickname": "@cname",
"username": "@name",
"avatar": "@img(100x100)",
"ip": "",
"addr": "",
"article_count": 0,
"index_count": 0,
"created_at": "2026-01-12T16:24:30.842+08:00",
"last_login_date": "0001-01-01T00:00:00Z",
"role": 2
}
]
}
})
})
}
首页显示
data的 mock假数据
src\mock\data_mock.ts【data数据,实际根据后端调整】
javascript
import {mock, type MockjsRequestOptions} from "mockjs";
export function dataMock() {
mock(/.*?api\/data\/statistic/, function (options: MockjsRequestOptions) {
console.log("SSSS")
return mock({
code: 0,
data: [
{
label: "在线流量",
"value|10001-30000": 1,
},
{
label: "用户总数",
"value|201-400": 1,
},
{
label: "文章总数",
"value|121-220": 1,
},
{
label: "今日登陆",
"value|21-40": 1,
},
{
label: "今日注册",
"value|8-20": 1,
}
],
msg: "成功"
})
})
mock(/.*?api\/data\/weather/, function (){
return mock({
"code": 0,
"data": {
"province": "湖南|上海",
"city": "长沙市",
"adcode": "430100",
"weather": "晴",
"temperature": "34",
"winddirection": "西南",
"windpower": "≤3",
"humidity": "55",
"reporttime": "2024-05-26 14:01:47",
"temperature_float": "34.0",
"humidity_float": "55.0"
},
"msg": "成功"
})
})
}
src\mock\index.ts【添加进mock启动函数中】
javascript
import {userMock} from "@/mock/user_mock";
import { dataMock } from "./data_mock";
export function apiMock(){
// 拿到这个配置文件中的VITE_MOCK 如果是为true就启动mock,否则就不启动mock
const env = import.meta.env
if (env.VITE_MOCK !== "true"){
return
}
userMock()
dataMock()
}
src\api\data_api.ts【data接口】
javascript
import type {baseResponse, optionsType} from "@/api/index";
import {useAxios} from "@/api/index";
export interface weatherType {
"province": string
"city": string
"adcode": string
"weather": string // 天气
"temperature": string // 温度
"winddirection": string
"windpower": string
"humidity": string
"reporttime": string
"temperature_float": string
"humidity_float": string
}
export function dataStatistic(): Promise<baseResponse<optionsType[]>> {
return useAxios.get("/api/data/statistic")
}
welcom组件封装
src\components\admin\g_welcom.vue【组件封装】
javascript
<script setup lang="ts">
import {reactive, ref} from "vue";
import {dataStatistic,type weatherType} from "@/api/data_api";
import {userStorei} from "@/stores/user_store";
import {computed} from "vue";
import {type optionsType } from "@/api";
interface Props {
noWeather?: boolean
noHelp?: boolean
}
const props = defineProps<Props>()
const store = userStorei()
const data = ref<optionsType[]>([])
async function getData() {
const res =await dataStatistic()
console.log(res.data)
data.value = res.data
}
getData()
const weatherData = reactive<weatherType>({
"province": "湖南",
"city": "长沙市",
"adcode": "430100",
"weather": "晴",
"temperature": "3",
"winddirection": "西南",
"windpower": "≤3",
"humidity": "55",
"reporttime": "2024-05-26 14:01:47",
"temperature_float": "34.0",
"humidity_float": "55.0"
})
const temperatureLabel = computed(() => {
const num = Number(weatherData.temperature)
if (num > 40) {
return "天气炎热,注意避暑"
}
if (num > 30) {
return "天气较热,多在阴凉出休息"
}
if (num > 10) {
return "天气不错,适合工作"
}
if (num > 0) {
return "天气较冷,多穿点衣服"
}
return "天气很冷,注意保暖"
})
const welcomeTitle = computed(() => {
const now = new Date()
const h = now.getHours()
if (h < 9 && h >= 6) {
return "早安"
}
if (h >= 9 && h < 12) {
return "上午好"
}
if (h >= 12 && h < 14) {
return "中午好"
}
if (h >= 14 && h < 16) {
return "下午好"
}
if (h >= 16 && h < 20) {
return "傍晚好"
}
if (h >= 20 && h < 24) {
return "晚安"
}
return "早安"
})
// getData()
</script>
<template>
<div class="f_welcome">
<div class="title">{{ welcomeTitle }} {{ store.userInfo.nickName }},请开始一天的工作吧</div>
<div v-if="!props.noWeather" class="weather">
{{ weatherData.province }} · {{ weatherData.city }} 今日 {{ weatherData.weather }},{{
weatherData.temperature
}}℃,{{ temperatureLabel }}
</div>
<div class="statistics">
<a-statistic v-for="item in data" animation :title="item.label" :value="item.value" show-group-separator/>
</div>
<div class="extra" v-if="!props.noHelp">
欢迎使用AnternalAdmin后台系统,可查看 <a href="">系统帮助</a> 以便更好的使用本系统
</div>
</div>
</template>
<style lang="less">
.f_welcome {
width: 100%;
background-color: var(--color-bg-1);
border-radius: 5px;
margin-bottom: 20px;
background-image: url("@/assets/logo.svg");
background-size: 500px;
background-repeat: no-repeat;
background-position: right center;
padding: 40px 20px;
color: var(--color-text-2);
.title {
font-size: 22px;
color: var(--color-text-1);
}
.weather {
margin-top: 20px;
}
.statistics {
margin-top: 20px;
.arco-statistic {
margin-right: 30px;
&:last-child {
margin-right: 0;
}
}
}
.extra {
margin-top: 20px;
a {
text-decoration: none;
color: @primary-6;
}
}
}
</style>
home页面框架以及调用组件
src\views\admin\home\index.vue
javascript
<script setup lang='ts'>
import G_welcom from '@/components/admin/g_welcom.vue';
import G_card from '@/components/common/g_card.vue';
</script>
<template>
<div class="home_view">
<G_welcom></G_welcom>
<div class="bottom">
<div class="left">
<G_card title="快捷入口" class="quick_room"></G_card>
<G_card title="数据统计" class="statistics"></G_card>
</div>
<div class="rigth">
<G_card title="更新日志" class="version"></G_card>
</div>
</div>
</div>
</template>
<style lang='less'>
.home_view{
background-color: initial !important;
.bottom{
display: flex;
justify-content: space-between;
.left{
width: 70%;
.statistics{
margin-top: 20px;
}
}
.rigth{
width: 30%;
margin-left: 20px;
}
}
}
</style>
快捷入口组件
src\components\admin\g_quick_entrane.vue
javascript
<script setup lang="ts">
import type { Component } from "vue";
import router from "@/router";
import G_iconComponent from "../common/g_iconComponent.vue";
import G_card from "@/components/common/g_card.vue";
interface quickType {
label: string
icon: string | Component
name: string
}
const list: quickType[] = [
{ label: "用户列表", icon: "iconfont icon-list", name: "userList" },
{ label: "文章列表", icon: "iconfont icon-navicon-wzgl", name: "articleList" },
{ label: "网站设置", icon: "iconfont icon-wangzhan", name: "siteManageSite" },
{ label: "AI设置", icon: "iconfont icon-wuguan", name: "siteManageAi" },
{ label: "Banner设置", icon: "iconfont icon-banner", name: "bannerList" },
{ label: "日志列表", icon: "iconfont icon-xitongrizhi", name: "logList" },
]
function goRouter(item: quickType) {
router.push({ name: item.name })
}
</script>
<template>
<g_card title="快捷入口" class="g_quick_entrance">
<div class="item" v-for="item in list">
<div class="icon" @click="goRouter(item)">
<g_icon-component :is="item.icon"></g_icon-component>
</div>
<div class="label">{{ item.label }}</div>
</div>
</g_card>
</template>
<style lang="less">
.g_quick_entrance {
.body {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-row-gap: 20px;
.item {
display: flex;
flex-direction: column;
align-items: center;
&:last-child {
margin-right: 0;
}
.icon {
width: 60px;
height: 60px;
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--color-fill-2);
cursor: pointer;
transition: all .3s;
&:hover {
transform: translateY(-5px);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.08);
}
i {
font-size: 20px;
}
}
.label {
margin-top: 5px;
color: var(--color-text-2);
}
}
}
}
</style>
更新日志组件
src\components\common\g_version.vue
javascript
<script setup lang="ts">
import G_card from "@/components/common/g_card.vue";
import {dateCurrentFormat} from "../../utils/date";
interface versionType {
title: string
date: string
items?: string[]
}
const list: versionType[] = [
{
title: "版本1.3",
date: "2024-11-23",
items: [
"管理员可以切换运行模式,社区、博客模式",
"站内信",
"用户私信",
"收藏夹",
"文章AI",
"前端自动刷新",
"一键部署",
]
},
{
title: "版本1.2",
date: "2025-12-06",
items: [
"加了ai",
]
},
{
title: "版本1.1",
date: "2025-09-29",
items: [
"前端换成了ts"
]
},
{
title: "版本1.0",
date: "2025-02-28",
items: [
"加入了群聊",
"前端是js"
]
}
]
</script>
<template>
<G_card title="更新日志" class="g_version">
<div class="item" v-for="(item, index) in list">
<div class="version_head">
<span class="index">{{ index + 1 }}.</span>
<span class="label">{{ item.title}}</span>
<span class="date">{{ dateCurrentFormat(item.date) }}</span>
</div>
<ul class="version_items" v-if="item.items?.length">
<li v-for="li in item.items">{{ li }}</li>
</ul>
</div>
</G_card>
</template>
<style lang="less">
.g_version{
.body{
.item{
margin-bottom: 20px;
.version_head{
position: relative;
.index{
margin-right: 5px;
}
.date{
position: absolute;
right: 0;
color: var(--color-text-2);
}
}
.version_items{
color: var(--color-text-2);
margin-top: 10px;
line-height: 1.3rem;
}
}
}
}
</style>
echarts的使用
-
npm install echarts
-
echarts是必须放在onmount中使用
echats实例不能使用响应式即ref
src\components\common\echarts\user_login.vue【没有使用接口数据】
可以根据主题进行切换颜色
javascript
<script setup lang="ts">
import * as echarts from 'echarts';
import {onMounted, reactive, ref, watch} from "vue";
import {theme} from "@/components/common/g_theme";
type EChartsOption = echarts.EChartsOption;
let myChart: echarts.ECharts | null = null
function initEcharts() {
let option: EChartsOption;
const textColor = getComputedStyle(document.body).getPropertyValue("--color-text-1")
const lineColor = getComputedStyle(document.body).getPropertyValue("--color-neutral-2")
let themeColor = [
'#1c5ae0',
'#15c5be'
]
if (theme.value === "dark") {
themeColor = [
'#1c5ae0',
'#ffffff'
]
}
option = {
color: themeColor,
title: {
text: '用户登录数据',
textStyle: {
color: textColor
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
legend: {
data: ['登录', '注册',],
textStyle: {
color: textColor
},
top:40
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
}
],
yAxis: [
{
type: 'value',
splitLine: {
lineStyle: {
color: lineColor
}
}
}
],
series: [
{
name: '登录',
type: 'line',
// stack: 'Total',
areaStyle: {},
emphasis: {
focus: 'series'
},
data: [120, 132, 101, 134, 90, 230, 210],
smooth: true,
},
{
name: '注册',
type: 'line',
// stack: 'Total',
areaStyle: {},
emphasis: {
focus: 'series'
},
smooth: true,
data: [220, 182, 191, 234, 290, 330, 310],
},
]
};
option && myChart?.setOption(option);
}
onMounted(async () => {
const chartDom = document.querySelector('.user_login_echarts') as HTMLDivElement;
myChart = echarts.init(chartDom);
initEcharts()
})
watch(() => theme.value, () => {
initEcharts()
})
</script>
<template>
<div class="user_login_echarts"></div>
</template>
<style lang="less">
.user_login_echarts {
width: 100%;
height: 300px;
background-color: var(--color-bg-1);
border-radius: 5px;
}
</style>
总体的home页面代码
src\views\admin\home\index.vue
javascript
<script setup lang='ts'>
import G_quick_entrane from '@/components/admin/g_quick_entrane.vue';
import G_welcom from '@/components/admin/g_welcom.vue';
import User_login from '@/components/common/echarts/user_login.vue';
import G_version from '@/components/common/g_version.vue';
</script>
<template>
<div class="home_view">
<G_welcom></G_welcom>
<div class="bottom">
<div class="left">
<G_quick_entrane></G_quick_entrane>
<User_login></User_login>
</div>
<div class="rigth">
<G_version></G_version>
</div>
</div>
</div>
</template>
<style lang='less'>
.home_view {
background-color: initial !important;
.bottom {
display: flex;
justify-content: space-between;
.left {
width: 70%;
.statistics {
margin-top: 20px;
}
}
.rigth {
width: 30%;
margin-left: 20px;
}
}
}
</style>
404页面
src\views\web\404.vue
javascript
<script setup lang="ts">
import router from "@/router";
function goHome(){
router.push({name: "web"})
}
</script>
<template>
<div class="notfound_view">
<img src="@/assets/logo.svg" alt="">
<div class="msg">页面找不到了哟</div>
<div class="btn">
<a-button type="primary" @click="goHome">返回首页</a-button>
</div>
</div>
</template>
<style lang="less">
.notfound_view{
display: flex;
flex-direction: column;
align-items: center;
margin-top: 40px;
img{
width: 400px;
}
.btn{
margin-top: 10px;
}
.msg{
font-size: 20px;
color: var(--color-text-2);
}
}
</style>
src\router\index.ts【在路由的最后一层加上404页面路由】
javascript
...
...
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
name: "home",
path: "/",
redirect: "/admin"
},
{
name: "web",
path: "/web",
component: () => import("@/views/web/index.vue")
},
{
name: "login",
path: "/login",
component: () => import("@/views/login/index.vue")
},
{
name: "admin",
path: "/admin",
component: () => import("@/views/admin/index.vue"),
meta: {
title: "首页",
role: [1, 2, 3]
},
children: [
{
name: "home",
path: "",
component: () => import("@/views/admin/home/index.vue")
},
{
name: "userCenter",
path: "user_Center",
children: [
{
name: "userinfo",
path: "user_info",
component: () => import("@/views/admin/user_center/index.vue"),
meta: {
title: "用户信息"
}
}
],
meta: {
title: "用户中心"
}
},
{
name: "userManage",
path: "user_Manage",
children: [
{
name: "userlist",
path: "user_list",
component: () => import("@/views/admin/user_manage/index.vue"),
meta: {
title: "用户列表",
role: [1]
}
}
],
meta: { title: "用户管理" }
},
{
name: "systemCenter",
path: "system_Center",
children: [
{
name: "systeminfo",
path: "system_info",
component: () => import("@/views/admin/system_manage/index.vue"),
meta: {
title: "系统管理",
}
}
],
meta: {
title: "系统信息",
role: [1]
}
},
]
},
{
name: "notfound",
path: "/:match(.*)",
component: () => import("@/views/web/404.vue"),
}
],
}
...
...
前端动态修改网页title,图标
src\conf\global.ts【创建配置文件】
javascript
export const title = "通用后台管理系统"
export const slogan = "通用后台"
export const enSlogan = "GennalAdmin"
export const ico = "/favicon.ico"
export const beian = "沪ICP备202601008号-3"
// export const ico = "https://fanyi-cdn.cdn.bcebos.com/webStatic/translation/img/favicon/favicon-32x32.png"
src\App.vue【app.vue中进行初始化调用】
html
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import {userStorei} from "@/stores/user_store";
import { title,ico} from './conf/global';
const store = userStorei()
store.loadUserInfo()
if (title){
document.title= title
}
if(ico){
const link = document.querySelector('link[rel="icon"]')
if(link){
link.setAttribute("href",ico)
}
}
</script>
<template>
<RouterView />
</template>
web页面导航栏+脚页
导航栏
src\components\web\g_nav.vue【实现超过部分可以实现控制滚动,以及滚动的高配配置,当下拉到一定的高度就固定在一个位置,方便查看跳转的一个导航栏功能】
javascript
<script setup lang="ts">
import { reactive, ref } from "vue";
import { userStorei } from "@/stores/user_store";
import { title, slogan, enSlogan } from "@/conf/global";
import G_theme from "@/components/common/g_theme.vue";
const store = userStorei()
interface Props {
noScroll?: boolean
scrollTop?: number
}
const props = defineProps<Props>()
const { noScroll = false, scrollTop = 100 } = props
const isShow = ref(noScroll)
if (!noScroll) {
window.onscroll = function () {
const top = document.documentElement.scrollTop
if (top >= scrollTop) {
isShow.value = true
} else {
isShow.value = false
}
}
}
</script>
<template>
<div class="g_nav" :class="{ isShow: isShow }">
<div class="container">
<div class="slogan">
<span class="n1">{{ slogan }}</span>
<span class="n2">{{ enSlogan }}</span>
</div>
<div class="left">
<router-link class="history" :to="{ name: 'home' }">首页</router-link>
</div>
<div class="right">
<router-link class="history" :to="{ name: 'login' }">登录</router-link>
<G_theme></G_theme>
</div>
</div>
</div>
</template>
<style lang="less">
.g_nav {
width: 100vw;
height: 60px;
position: fixed;
top: 0;
z-index: 1000;
display: flex;
justify-content: center;
color: white;
transition: all .3s;
&.isShow {
box-shadow: 0 0 5px rgba(0, 0, 0, 0.06);
background-color: var(--color-bg-1);
color: var(--color-text-2);
.n1 {
color: var(--color-text-1);
}
.n2 {
color: var(--color-text-2);
}
.theme {
color: var(--color-text-2) !important;
}
}
.container {
width: 1200px;
display: flex;
align-items: center;
// justify-content: space-between;
.slogan {
width: 10%;
.n1 {
font-size: 20px;
color: var(--color-text-1);
}
.n2 {
font-size: 12px;
transform: scale(0.87);
transform-origin: left center;
color: var(--color-text-2);
}
}
.left {
width: 70%;
}
.right {
width: 30%;
display: flex;
align-items: center;
justify-content: end;
.theme {
width: 20%;
}
}
}
}
</style>
脚页
src\components\web\g_follter.vue【脚页可以显示一些基本网页信息,比如备案号,联系方式等信息】
javascript
<script setup lang="ts">
import {beian} from "@/conf/global";
</script>
<template>
<div class="g_footer">
<div class="g_beian" v-if="beian">
<img src="@/assets/logo.svg" alt="" :width="30">
<a href="https://beian.miit.gov.cn/" target="_blank">备案号:{{ beian }}</a>
</div>
</div>
</template>
<style lang="less">
.g_footer {
background-color: var(--color-bg-1);
display: flex;
justify-content: center;
.g_beian {
display: flex;
align-items: center;
padding: 20px 0;
a{
margin-left: 10px;
color: var(--color-text-2);
text-decoration: none;
}
}
}
</style>
web主页组件
src\components\web\g_main.vue【作为web页面的主入口。将代码写进这个里面】
javascript
<script setup lang="ts">
</script>
<template>
<div class="f_main_com">
<div class="f_container">
<slot></slot>
</div>
</div>
</template>
<style lang="less">
.f_main_com {
width: 100%;
display: flex;
justify-content: center;
margin-top: 60px;
background-color: var(--color-neutral-2);
> .f_container {
width: 1200px;
min-height: calc(100vh - 122px);
}
}
</style>
配置
src\views\web\index.vue【web的index作为路由进口】
javascript
<script setup lang='ts'>
</script>
<template>
<div>
<router-view></router-view>
</div>
</template>
<style scoped lang='scss'>
</style>
src\views\web\web_home.vue【以webhome为例,内容写进g_main组件中】
javascript
<script setup lang='ts'>
import G_follter from '@/components/web/g_follter.vue';
import G_main from '@/components/web/g_main.vue';
import G_nav from '@/components/web/g_nav.vue';
</script>
<template>
<div class="web_home_view">
<g_nav no-scroll></g_nav>
<g_main> 首页</g_main>
<g_follter></g_follter>
</div>
</template>
<style lang='less'></style>
src\router\index.ts【修改路由层级】
javascript
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
// {
// name: "home",
// path: "/",
// redirect: "/admin"
// },
{
name: "web",
path: "/",
component: () => import("@/views/web/index.vue"),children:[
{
name: "web_home",
path: "",
component: () => import("@/views/web/web_home.vue")
},
]
},
{
name: "login",
path: "/login",
component: () => import("@/views/login/index.vue")
},
{
name: "admin",
path: "/admin",
component: () => import("@/views/admin/index.vue"),
meta: {
title: "首页",
role: [1, 2, 3]
},
children: [
{
name: "home",
path: "",
component: () => import("@/views/admin/home/index.vue")
},
{
name: "userCenter",
path: "user_Center",
children: [
{
name: "userinfo",
path: "user_info",
component: () => import("@/views/admin/user_center/index.vue"),
meta: {
title: "用户信息"
}
}
],
meta: {
title: "用户中心"
}
},
{
name: "userManage",
path: "user_Manage",
children: [
{
name: "userlist",
path: "user_list",
component: () => import("@/views/admin/user_manage/index.vue"),
meta: {
title: "用户列表",
role: [1]
}
}
],
meta: { title: "用户管理" }
},
{
name: "systemCenter",
path: "system_Center",
children: [
{
name: "systeminfo",
path: "system_info",
component: () => import("@/views/admin/system_manage/index.vue"),
meta: {
title: "系统管理",
}
}
],
meta: {
title: "系统信息",
role: [1]
}
},
]
},
{
name: "notfound",
path: "/:match(.*)",
component: () => import("@/views/web/404.vue"),
}
],
})
修改
修改1
src\assets\theme.css【在这个文件中添加了关于登录的背景色的变量】
然后在main.ts中引入
javascript
:root {
--login_bg: rgba(255,255,255,0.8)
}
[arco-theme="dark"]{
--login_bg:rgba(0,0,0,0.8)
}
src\views\login\index.vue【在login的页面中使用,区分在黑夜模式或者白天模式下可以清晰看到登录页面】
javascript
...
...
<style lang='less'>
.login_view{
background: url(https://ts1.tc.mm.bing.net/th/id/OIP-C.IfUgoN1SdEsFH5xpjuOG1QHaNL?w=160&h=211&c=8&rs=1&qlt=90&o=6&pid=3.1&rm=2) 50% /cover no-repeat;
position: relative;
height: 100vh;
.login_mask{
width: 400px;
height: 100vh;
background-color: var(--login_bg);
position: absolute;
right: 0;
top: 0;
display: flex;
justify-content: center;
align-items: center;
.arco-form{
padding: 40px;
}
.title{
font-size: 26px;
font-weight: 600;
text-align: center;
color: @primary-6;
margin-bottom: 10px;
}
}
}
</style>
...
...
修改2
src\App.vue
在App.vue中执行loadThme函数去掉在theme.ts中的执行oadThme,这样在程序入口就执行主题的切换,这样在登录或者其他页面都显示缓存过后的主题
javascript
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import {userStorei} from "@/stores/user_store";
import { title,ico} from './conf/global';
import { loadTheme } from './components/common/g_theme';
const store = userStorei()
store.loadUserInfo()
if (title){
document.title= title
}
if(ico){
const link = document.querySelector('link[rel="icon"]')
if(link){
link.setAttribute("href",ico)
}
}
loadTheme() //在app中导入,就不用在theme中导入,且全局可用
</script>
<template>
<RouterView />
</template>