0

0

教你基于Laravel+Vue组件实现文章发布、编辑和浏览功能

藏色散人

藏色散人

发布时间:2020-11-13 14:08:50

|

3309人浏览过

|

来源于xueyuanjun

转载


下面由laravel教程栏目给大家介绍基于laravel+vue组件实现文章发布、编辑和浏览功能,希望对需要的朋友有所帮助!

教你基于Laravel+Vue组件实现文章发布、编辑和浏览功能

我们将基于 Laravel 提供后端接口,基于 Vue.js 作为前端 JavaScript 组件开发框架,基于 Bootstrap 作为 CSS 框架。

Laravel 后端接口

首先,我们基于上篇教程创建的资源控制器 PostController 快速编写后端增删改查接口实现代码:

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Exception;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;

class PostController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth')->except('index', 'all', 'show', 'data');
    }

    /**
     * Display a listing of the resource.
     *
     * @return Application|Factory|View|Response|\Illuminate\View\View
     */
    public function index()
    {
        return view('posts.index', ['pageTitle' => '文章列表页']);
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return Application|Factory|View|Response|\Illuminate\View\View
     */
    public function create()
    {
        return view('posts.create', ['pageTitle' => '发布新文章']);
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param Request $request
     * @return array
     */
    public function store(Request $request)
    {
        $data = $request->validate([
            'title' => 'required|max:128',
            'content' => 'required'
        ]);

        $post = new Post($data);
        $post->status = 1;
        $post->user_id = Auth::user()->id;
        if ($post->save()) {
            return ['success' => true, 'message' => '文章发布成功'];
        }
        return ['success' => false, 'message' => '保存文章数据失败'];
    }

    /**
     * Display the specified resource.
     *
     * @param Post $post
     * @return Application|Factory|View|Response|\Illuminate\View\View
     */
    public function show(Post $post)
    {
        return view('posts.show', ['id' => $post->id, 'pageTitle' => $post->title]);
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param Post $post
     * @return Application|Factory|View|Response|\Illuminate\View\View
     */
    public function edit(Post $post)
    {
        return view('posts.edit', ['pageTitle' => '编辑文章', 'id' => $post->id]);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param Request $request
     * @param Post $post
     * @return array
     */
    public function update(Request $request, Post $post)
    {
        $data = $request->validate([
            'title' => 'required|max:128',
            'content' => 'required'
        ]);

        $post->fill($data);
        $post->status = 1;
        if ($post->save()) {
            return ['success' => true, 'message' => '文章更新成功'];
        }
        return ['success' => false, 'message' => '更新文章数据失败!'];
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param Post $post
     * @return array
     * @throws Exception
     */
    public function destroy(Post $post)
    {
        if ($post->delete()) {
            return ['success' => true, 'message' => '文章删除成功'];
        }
        return ['success' => false, 'message' => '删除文章失败'];
    }

    /**
     * 获取所有文章数据
     *
     * @return Collection
     */
    public function all()
    {
        return Post::orderByDesc('created_at')->get();
    }

    /**
     * 获取单个文章数据
     *
     * @param Post $post
     * @return Post
     */
    public function data(Post $post)
    {
        $post->author_name = $post->author->name;
        return $post;
    }
}

除了 Laravel 资源控制器自带的方法之外,我们额外提供了 alldata 两个方法,分别用于在 Vue 组件中通过 AJAX 请求获取文章列表数据和文章详情数据。因此,需要在路由文件 routes/web.php 中注册资源路由之前添加这两个方法对应的路由:

立即学习前端免费学习笔记(深入)”;

use App\Http\Controllers\PostController;

Route::get('posts/all', [PostController::class, 'all']);
Route::get('posts/{post}/data', [PostController::class, 'data']);
Route::resource('posts', PostController::class);

注意这里我们使用了 Laravel 路由提供的隐式模型绑定功能快速获取模型实例。此外,相应的视图模板路径也做了调整,我们马上会介绍这些视图模板文件。

通过填充器填充测试数据

如果你在上篇教程填充的测试数据基础上新增过其他数据,可以运行 php artisan migrate:refresh 命令重建数据表快速清空已有数据并重新填充。

如果你不想查看返回实例数据格式的细节,可以在自带填充器 database/seeders/DatabaseSeeder.php 中定义填充代码:

<?php

namespace Database\Seeders;

use App\Models\Post;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        // \App\Models\User::factory(10)->create();
        Post::factory(10)->create();
    }
}

然后运行  php artisan migrate:refresh --seed 命令即可一步到位完成数据表重建、测试数据清空和重新填充:

-w847

-w1316

通过模板继承重构视图模板

由于我们使用的是 Laravel 提供的 laravel/ui 扩展包提供的 Bootstrap 和 Vue 前端脚手架代码,该扩展包还提供了用户认证相关脚手架代码实现,并且提供了一个视图模板布局文件 resources/views/layouts/app.blade.php,我们将通过模板继承基于这个布局文件来重构文章列表、表单、详情页相关视图模板文件,让整体 UI 统一。

不同页面设置不同标题

我们前面在 PostController 中,为所有 GET 路由渲染的视图文件传递了 pageTitle  值作为不同页面的标题,要实现该功能,需要修改 resources/views/layouts/app.blade.php 布局文件中 title 标签对应的标签文本值:

<title>{{ $pageTitle ?? config('app.name', 'Laravel') }}</title>

文章列表视图

接下来,将原来的文章相关视图文件都移动到 resources/views/posts 目录下,改写文章列表视图文件模板代码如下(将原来的 posts.blade.php 重命名为 index.blade.php):

@extends('layouts.app')

@section('content')
    <p class="container">
        <post-list></post-list>
    </p>
@endsection

文章发布视图

将原来的 form.blade.php 重命名为 create.blade.php,并编写文章发布表单页面视图文件模板代码如下:

@extends('layouts.app')

@section('content')
    <p class="container">
        <p class="row justify-content-center">
            <post-form title="发布新文章" action="create" url="{{ route('posts.store') }}">
            </post-form>
        </p>
    </p>
@endsection

由于文章发布和编辑表单共用一个 Vue 表单组件,所以我们这里额外传递了一些 props 属性到组件模板,包括表单标题(title)、操作类型(action)、表单提交 URL(url),后面马上会介绍表单组件的调整。

文章编辑视图

resources/views/posts 目录下新建一个 edit.blade.php 作为文件编辑页面视图文件,并编写模板代码如下:

@extends('layouts.app')

@section('content')
    <p class="container">
        <p class="row justify-content-center">
            <post-form title="编辑文章" action="update" id="{{ $id }}" url="{{ route('posts.update', ['post' => $id]) }}">
            </post-form>
        </p>
    </p>
@endsection

同样也使用 post-form 模板渲染文章编辑表单,只不过额外传递了一个 id 属性,用于在表单组件初始化待编辑的文章数据。

文章详情页视图后面单独介绍。

重构 Vue 表单组件代码

为了适配文章编辑表单,以及后端接口返回数据格式的调整,我们需要修改 Vue 表单组件实现代码:

<template>
    <FormSection @store="store">
        <template slot="title">{{ title }}</template>
        <template slot="input-group">
            <p class="form-group">
                <Label name="title" label="标题"></Label>
                <InputText name="title" v-model="form.title" @keyup="clear('title')"></InputText>
                <ErrorMsg :error="form.errors.get('title')"></ErrorMsg>
            </p>

            <p class="form-group">
                <Label name="content" label="内容"></Label>
                <TextArea name="content" v-model="form.content" @keyup="clear('content')"></TextArea>
                <ErrorMsg :error="form.errors.get('content')"></ErrorMsg>
            </p>
        </template>
        <template slot="action">
            <Button type="submit">立即发布</Button>
        </template>
        <template slot="toast">
            <ToastMsg :success="form.success" :validated="form.validated">
                {{ form.message }}
            </ToastMsg>
        </template>
    </FormSection>
</template>

<script>
import FormSection from './form/FormSection';
import InputText from './form/InputText';
import TextArea from './form/TextArea';
import Button from './form/Button';
import ToastMsg from './form/ToastMsg';
import Label from "./form/Label";
import ErrorMsg from "./form/ErrorMsg";

export default {

    components: {FormSection, InputText, TextArea, Label, ErrorMsg, Button, ToastMsg},

    props: ['title', 'url', 'action', 'id'],

    data() {
        return {
            form: new Form({
                title: '',
                content: ''
            })
        }
    },

    mounted() {
        let post_id = Number(this.id);
        if (this.action === 'update' && post_id > 0) {
            this.load(post_id);
        }
    },

    methods: {
        load(id) {
            this.form.title = '加载中...';
            this.form.content = '加载中...';
            let url = '/posts/' + id + '/data';
            axios.get(url).then(resp => {
                this.form.title = resp.data.title;
                this.form.content = resp.data.content;
            }).catch(error => {
                alert('从服务端初始化表单数据失败');
            });
        },
        store() {
            if (this.action === 'create') {
                this.form.post(this.url)
                    .then(data => {
                        // 发布成功后跳转到列表页
                        window.location.href = '/posts';
                    })
                    .catch(data => console.log(data)); // 自定义表单提交失败处理逻辑
            } else {
                this.form.put(this.url)
                    .then(data => {
                        // 更新成功后跳转到详情页
                        window.location.href = '/posts/' + this.id;
                    })
                    .catch(data => console.log(data)); // 自定义表单提交失败处理逻辑
            }
        },
        clear(field) {
            this.form.errors.clear(field);
        }
    }
}
</script>

文章发布和编辑页面需要通过标题予以区分,所以我们通过 title 属性从父级作用域传递该标题值。

对于文章编辑表单,首先,我们会根据父级作用域传递的 id 属性值在 mounted 钩子函数中调用新增的 load 方法从后端接口 /posts/{post}/data 加载对应文章数据填充表单。

现在后端接口可以自动获取当前认证用户的 ID,所以 author 字段就没有必要填写了,直接将其移除。

文章创建和编辑对应的请求方式是不一样的,操作成功后处理逻辑也是不一样的(前者重定向到列表页,后者重定向到详情页),所以根据 action 属性值分别进行了处理。

此外,由于后端对表单数据进行验证后,保存数据阶段依然可能失败,所以前端提交表单后返回的响应状态码为 200 并不表示表单提交处理成功,还需要借助响应实体(JSON 格式)中的 success 字段进一步判断,进而通过 ToastMsg 子组件渲染成功或失败的提示文本。

ToastMsg 是从之前的 SuccessMsg 组件升级而来,直接将 SuccessMsg 组件重命名为 ToastMsg 并改写组件代码如下:

<style scoped>
.alert {
    margin-top: 10px;
}
</style>

<template>
    <p class="alert" :class="{'alert-success': success, 'alert-danger': !success}" role="alert" v-show="validated">
        <slot></slot>
    </p>
</template>

<script>
export default {
    props: ['validated', 'success']
}
</script>

可以看到,如果表单提交处理成功(依然基于父级作用域传递的 form.success 属性)则显示成功提示样式及文案,否则显示失败提示样式和文案,而是否渲染该组件取决于表单验证是否成功,该字段基于父级作用域传递的 form.validated 属性,之前是没有这个属性的,所以我们需要额外添加,在 resources/js/form.js 中,调整相关代码实现如下:

class Form {
    constructor(data) {
        ...
        this.validated = false;
    }

    ...
    
    /**
     * 表单提交处理
     *
     * @param {string} url
     * @param {string} method
     */
    submit(url, method) {
        return new Promise((resolve, reject) => {
            axios[method](url, this.data())
                .then(response => {
                    this.onSuccess(response.data);
                    this.validated = true;
                    if (this.success === true) {
                        resolve(response.data);
                    } else {
                        reject(response.data);
                    }
                })
                .catch(error => {
                    this.onFail(error.response.data.errors);
                    reject(error.response.data);
                });
        });
    }


    /**
     * 处理表单提交成功
     *
     * @param {object} data
     */
    onSuccess(data) {
        this.success = data.success;
        this.message = data.message;
        this.reset();
    }
    
    ...

}

这样一来,文章发布和编辑共用的 Vue 表单组件就重构好了。

阿里云AI平台
阿里云AI平台

阿里云AI平台

下载

文章详情页视图和 Vue 组件实现

我们接着来实现文章详情页。

PostDetail 组件

component-practice/resources/js/components 目录下新建一个 PostDetail.vue 文件作为渲染文章详情的 Vue 单文件组件,并编写组件代码如下:

<style scoped>
.post-detail {
    width: 100%;
}
.post-title {
    margin-bottom: .25rem;
    font-size: 2.5rem;
}
.post-meta {
    margin-bottom: 1.25rem;
    color: #999;
}
.post-content {
    font-size: 1.1rem;
    font-weight: 400;
    line-height: 1.5;
    color: #212529;
}
</style>

<template>
    <p class="spinner-border" role="status" v-if="!loaded">
        <span class="sr-only">Loading...</span>
    </p>
    <p class="post-detail" v-else>
        <h2 class="post-title">{{ title }}</h2>
        <p class="post-meta">
            Created at {{ created_at | diff_for_human }} by <a href="#">{{ author_name }}</a>,
            Status: {{ status | post_status_readable }},
            Action: <a :href="'/posts/' + id + '/edit'">编辑</a>
        </p>
        <p class="post-content">
            {{ content }}
        </p>
    </p>
</template>

<script>
export default {
    props: ['post_id'],
    data() {
        return {
            id: this.post_id,
            title: '',
            content: '',
            status: '',
            author_name: '',
            created_at: '',
            loaded: false
        }
    },

    mounted() {
        if (!this.loaded) {
            this.load(Number(this.id));
        }
    },

    methods: {
        load(id) {
            axios.get('/posts/' + this.id + '/data').then(resp => {
                this.title = resp.data.title;
                this.content = resp.data.content;
                this.status = resp.data.status;
                this.author_name = resp.data.author_name;
                this.created_at = resp.data.created_at;
                this.loaded = true;
            }).catch(err => {
                alert('加载文章数据失败');
            });
        }
    }
}
</script>

这个组件功能比较简单,在 mounted 钩子函数中通过父级作用域传递的 id 属性值调用 load 函数加载后端接口返回的文章数据,并通过数据绑定将其渲染到模板代码中,在加载过程中,会有一个动态的加载状态提示用户文章数据正在加载。

这里我们还使用了过滤器对数据进行格式化,日期过滤器已经是全局的了,状态过滤器之前是本地的,这里我们将其从文章列表卡片组件 CardItem 中将其迁移到 app.js 中作为全局过滤器:

Vue.filter('post_status_readable', status => {
    switch(status) {
        case 0:
            return '草稿';
        case 1:
            return '已发布';
        default:
            return '未知状态';
    }
});

然后就可以在任何 Vue 组件中调用它了(CardItem 中过滤器调用代码做一下相应调整)。

app.js 中注册这个组件:

Vue.component('post-detail', require('./components/PostDetail.vue').default);

文章详情页视图文件

再到 component-practice/resources/views/posts 目录下新建 show.blade.php 视图文件引用 post-detail 组件即可:

@extends('layouts.app')

@section('content')
<p class="container">
    <post-detail post_id="{{ $id }}"></post-detail>
</p>
@endsection

优化文章列表组件

最后,我们到文章列表组件中新增一个发布文章入口。

打开子组件 ListSection,在视图模式切换按钮右侧新增一个插槽,用于从父级作用域传递更多额外操作按钮:

<style scoped>
.card-header h5 {
    margin-top: 0.5em;
    display: inline-block;
}
.card-header .float-right {
    float: right;
}
</style>

<template>
<p class="card">
    <p class="card-header">
        <h5><slot name="title"></slot></h5>
        <p class="float-right">
            <button class="btn btn-success view-mode" @click.prevent="switch_view_mode">
                {{ view.switch_to }}
            </button>
            <slot name="more-actions"></slot>
        </p>
    </p>
    
    ...

然后在 PostList 中将发布文章按钮放到这个插槽中(样式代码也做了微调):

<style scoped>
.post-list {
    width: 100%;
}
</style>

<template>
    <p class="post-list">
    <ListSection :view_mode="view_mode" @view-mode-changed="change_view_mode">
        <template #title>文章列表</template>
        <template #more-actions>
            <a href="/posts/create" class="btn btn-primary">新文章</a>
        </template>
        <template v-if="view_mode === 'list'">
            <ListItem v-for="post in posts" :key="post.id" :url="'/posts/' + post.id">
                {{ post.title }}
            </ListItem>
        </template>
    ...

顺便也为文章列表所有文章设置详情页链接,ListItem 链接是从 PostList 通过 props 属性传递的,CardItem 需要去子组件中设置:

<a :href="'/posts/' + post.id" class="btn btn-primary"><slot name="action-label"></slot></a>

至此,我们就完成了文章列表、发布、编辑和详情页的所有前后端功能代码编写。

整体测试

如果你已经在本地运行了 npm run watch 并且通过 php arstisan serve 启动 PHP 内置 Web 服务器的话,就可以在浏览器通过 http://127.0.0.1:3002/posts (启用了 BrowserSync 代理)访问新的文章列表页了:

-w1161

点击任意文章链接,即可进入文章详情页,加载数据成功之前,会有如下动态加载效果:

-w1122

-w1131

你可以点击「编辑」链接对这篇文章进行编辑:

-w804

更新成功后,会跳转到文章详情页,对应字段均已更新,并且状态也从草稿变成了已发布:

-w1135

当然,文章发布和编辑功能需要用户处于已登录状态(目前未做权限验证),如果未登录的话,点击编辑和新文章按钮会先跳转到登录页面(该功能由 PostController 控制器构造函数中定义的中间件方法实现),我们在已登录情况下在文章列表页点击右上角的「新文章」按钮进入文章发布页面:

-w801

发布成功后,页面会跳转到文章列表页,并在列表中出现刚刚创建的文章:

-w1159

增删改查还剩下一个「删」,下篇教程,就来给大家演示文章删除功能实现,为什么单独介绍呢,因为我想结合删除功能演示基于 Vue 组件的模态框、对话框以及过渡效果的实现。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

26

2026.03.13

Python异步编程与Asyncio高并发应用实践
Python异步编程与Asyncio高并发应用实践

本专题围绕 Python 异步编程模型展开,深入讲解 Asyncio 框架的核心原理与应用实践。内容包括事件循环机制、协程任务调度、异步 IO 处理以及并发任务管理策略。通过构建高并发网络请求与异步数据处理案例,帮助开发者掌握 Python 在高并发场景中的高效开发方法,并提升系统资源利用率与整体运行性能。

46

2026.03.12

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

178

2026.03.11

Go高并发任务调度与Goroutine池化实践
Go高并发任务调度与Goroutine池化实践

本专题围绕 Go 语言在高并发任务处理场景中的实践展开,系统讲解 Goroutine 调度模型、Channel 通信机制以及并发控制策略。内容包括任务队列设计、Goroutine 池化管理、资源限制控制以及并发任务的性能优化方法。通过实际案例演示,帮助开发者构建稳定高效的 Go 并发任务处理系统,提高系统在高负载环境下的处理能力与稳定性。

51

2026.03.10

Kotlin Android模块化架构与组件化开发实践
Kotlin Android模块化架构与组件化开发实践

本专题围绕 Kotlin 在 Android 应用开发中的架构实践展开,重点讲解模块化设计与组件化开发的实现思路。内容包括项目模块拆分策略、公共组件封装、依赖管理优化、路由通信机制以及大型项目的工程化管理方法。通过真实项目案例分析,帮助开发者构建结构清晰、易扩展且维护成本低的 Android 应用架构体系,提升团队协作效率与项目迭代速度。

92

2026.03.09

JavaScript浏览器渲染机制与前端性能优化实践
JavaScript浏览器渲染机制与前端性能优化实践

本专题围绕 JavaScript 在浏览器中的执行与渲染机制展开,系统讲解 DOM 构建、CSSOM 解析、重排与重绘原理,以及关键渲染路径优化方法。内容涵盖事件循环机制、异步任务调度、资源加载优化、代码拆分与懒加载等性能优化策略。通过真实前端项目案例,帮助开发者理解浏览器底层工作原理,并掌握提升网页加载速度与交互体验的实用技巧。

102

2026.03.06

Rust内存安全机制与所有权模型深度实践
Rust内存安全机制与所有权模型深度实践

本专题围绕 Rust 语言核心特性展开,深入讲解所有权机制、借用规则、生命周期管理以及智能指针等关键概念。通过系统级开发案例,分析内存安全保障原理与零成本抽象优势,并结合并发场景讲解 Send 与 Sync 特性实现机制。帮助开发者真正理解 Rust 的设计哲学,掌握在高性能与安全性并重场景中的工程实践能力。

227

2026.03.05

PHP高性能API设计与Laravel服务架构实践
PHP高性能API设计与Laravel服务架构实践

本专题围绕 PHP 在现代 Web 后端开发中的高性能实践展开,重点讲解基于 Laravel 框架构建可扩展 API 服务的核心方法。内容涵盖路由与中间件机制、服务容器与依赖注入、接口版本管理、缓存策略设计以及队列异步处理方案。同时结合高并发场景,深入分析性能瓶颈定位与优化思路,帮助开发者构建稳定、高效、易维护的 PHP 后端服务体系。

532

2026.03.04

AI安装教程大全
AI安装教程大全

2026最全AI工具安装教程专题:包含各版本AI绘图、AI视频、智能办公软件的本地化部署手册。全篇零基础友好,附带最新模型下载地址、一键安装脚本及常见报错修复方案。每日更新,收藏这一篇就够了,让AI安装不再报错!

171

2026.03.04

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Laravel---API接口
Laravel---API接口

共7课时 | 0.7万人学习

PHP自制框架
PHP自制框架

共8课时 | 0.6万人学习

PHP面向对象基础课程(更新中)
PHP面向对象基础课程(更新中)

共12课时 | 0.7万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号