GeneralAdmin-3

表格表单封装

表格组件封装

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>
相关推荐
Charlie_lll2 小时前
学习Three.js–太阳系星球自转公转
前端·three.js
json{shen:"jing"}2 小时前
10_自定义事件组件交互
开发语言·前端·javascript
Jinuss2 小时前
源码分析之React中scheduleUpdateOnFiber调度更新解析
前端·javascript·react.js
一位搞嵌入式的 genius2 小时前
深入理解 JavaScript 异步编程:从 Event Loop 到 Promise
开发语言·前端·javascript
m0_564914922 小时前
Altium Designer,AD如何修改原理图右下角图纸标题栏?如何自定义标题栏?自定义原理图模版的使用方法
java·服务器·前端
brevity_souls2 小时前
SQL Server 窗口函数简介
开发语言·javascript·数据库
方安乐2 小时前
react笔记之useCallback
前端·笔记·react.js
小二·3 小时前
Python Web 开发进阶实战:AI 伦理审计平台 —— 在 Flask + Vue 中构建算法偏见检测与公平性评估系统
前端·人工智能·python
走粥3 小时前
选项式API与组合式API的区别
开发语言·前端·javascript·vue.js·前端框架