abp.NetCore使用vue.js前端配置菜单路由、列表页、编辑页、ViewUI上传文件、ElementUI列表、富文本编辑器vue-quill-editor

配置路由:router/router.ts,在appRouters对象中copy修改就行了,注意所有路由包括父子路由的name都不能重复。
路由配置中permission是对应的后台权限名称字符串:permission:'Pages.Articles',meta》title对应的是多语言配置key:Framework.Core》Localization》Framework.xml
添加新页面:
1、添加store实体:store/entities

import Entity from './entity'
export default class Article extends Entity<number>{
    isActive:boolean;
    tenantId:number;
    title:string;
    author:number;
    contents:string;
    articleType:number;
    coverImg:string;
}

2、添加store/modules,其中引用刚刚添加的实体类型

import {Store,Module,ActionContext} from 'vuex'
import ListModule from './list-module'
import ListState from './list-state'
import Article from '../entities/article'
import Role from '../entities/role'
import Ajax from '../../lib/ajax'
import PageResult from '@/store/entities/page-result';
import ListMutations from './list-mutations'

interface ArticleState extends ListState<Article>{
    editArticle:Article
}
class ArticleMutations extends ListMutations<Article>{

}
class ArticleModule extends ListModule<ArticleState,any,Article>{
    state={
        totalCount:0,
        currentPage:1,
        pageSize:10,
        list: new Array<Article>(),
        loading:false,
        editArticle:new Article()
    }
    actions={
        async getAll(context:ActionContext<ArticleState,any>,payload:any){
            context.state.loading=true;
            let reponse=await Ajax.post('/api/services/app/Article/GetArticles',payload.data);//Ajax.get 方式,传参:{params:payload.data};Ajax.post 方式参数直接写 payload.data
            context.state.loading=false;
            let page=reponse.data.result as PageResult<Article>;
            context.state.totalCount=page.totalCount;
            context.state.list=page.items;
        },
        async create(context:ActionContext<ArticleState,any>,payload:any){
            await Ajax.post('/api/services/app/Article/Create',payload.data);
        },
        async update(context:ActionContext<ArticleState,any>,payload:any){
            await Ajax.put('/api/services/app/Article/Update',payload.data);
        },
        async delete(context:ActionContext<ArticleState,any>,payload:any){
            await Ajax.delete('/api/services/app/Article/Delete?Id='+payload.data.id);
        },
        async get(context:ActionContext<ArticleState,any>,payload:any){
            let reponse=await Ajax.get('/api/services/app/Article/Get?Id='+payload.id);
            return reponse.data.result as Article;
        },
    };
    mutations={
        setCurrentPage(state:ArticleState,page:number){
            state.currentPage=page;
        },
        setPageSize(state:ArticleState,pagesize:number){
            state.pageSize=pagesize;
        },
        edit(state:ArticleState,article:Article){
            state.editArticle=article;
        }
    }
}
const articleModule=new ArticleModule();
export default articleModule;

可以看到他都是用store来存储数据的。
3、添加页面views/article/index.vue,这用的是切换组件,不跳转,就不用改变路径,也不用再多配置路由

<template>
  <component :is="currentRouter" :operation='currentOpt' :mId="currentId" />
</template>
<script> 
// 列表页面
import list from '@/views/article/components/list'
// 编辑页面
import edit from '@/views/article/components/edit'

export default {
  name: 'articles',
  components: { list, edit },
  data() {
    return {
      // 当前加载的组件,默认为 list 组件(显示列表页面)
      currentRouter: "list",
      currentOpt: undefined,
      currentId: undefined
    }
  },
  created() {
    
  }
}
</script>

4、添加组件views/article/components/list.vue和edit.vue
list.vue

<template>
  <div>
    <Card dis-hover>
      <div class="page-body">
        <Form ref="queryForm" :label-width="80" label-position="left" inline>
          <Row :gutter="16">
            <Col span="6">
              <FormItem :label="L('Keyword') + ':'" style=" 100%">
                <Input
                  v-model="pagerequest.keyword"
                  :placeholder="L('ArticleTitle')"
                />
              </FormItem>
            </Col>
          </Row>
          <Row>
            <Button
              @click="create"
              icon="android-add"
              type="primary"
              size="large"
              >{{ L("Add") }}</Button
            >
            <Button
              icon="ios-search"
              type="primary"
              size="large"
              @click="getpage"
              class="toolbar-btn"
              >{{ L("Find") }}</Button
            >
          </Row>
        </Form>
        <div class="margin-top-10">
          <Table
            :loading="loading"
            :columns="columns"
            :no-data-text="L('NoDatas')"
            border
            :data="list"
          >
          </Table>
          <Page
            show-sizer
            class-name="fengpage"
            :total="totalCount"
            class="margin-top-10"
            @on-change="pageChange"
            @on-page-size-change="pagesizeChange"
            :page-size="pageSize"
            :current="currentPage"
          ></Page>
        </div>
      </div>
    </Card>
  </div>
</template>
<script lang='ts'>
import { Component, Vue, Inject, Prop, Watch } from "vue-property-decorator";
import Util from "@/lib/util";
import AbpBase from "@/lib/abpbase";
import PageRequest from "@/store/entities/page-request";

class PageModelRequest extends PageRequest {
  keyword: string;
  isActive: boolean = null; //nullable
}

@Component({
  // components: { CreateUser, EditUser },
})
export default class AbpArticles extends AbpBase {
  // createModalShow: boolean = false;
  // editModalShow: boolean = false;
  pagerequest: PageModelRequest = new PageModelRequest();
  // creationTime: Date[] = [];

  edit() {
    // this.editModalShow=true;
  }
  get list() {
    return this.$store.state.article.list;
  }
  get loading() {
    return this.$store.state.article.loading;
  }
  create() {
    this.$parent.currentOpt = "create";
    this.$parent.currentRouter = "edit";
  }
  isActiveChange(val: string) {
    console.log(val);
    if (val === "Actived") {
      this.pagerequest.isActive = true;
    } else if (val === "NoActive") {
      this.pagerequest.isActive = false;
    } else {
      this.pagerequest.isActive = null;
    }
  }
  pageChange(page: number) {
    this.$store.commit("article/setCurrentPage", page);
    this.getpage();
  }
  pagesizeChange(pagesize: number) {
    this.$store.commit("article/setPageSize", pagesize);
    this.getpage();
  }
  async getpage() {
    this.pagerequest.maxResultCount = this.pageSize;
    this.pagerequest.skipCount = (this.currentPage - 1) * this.pageSize;
    //filters

    // if (this.creationTime.length > 0) {
    //   this.pagerequest.from = this.creationTime[0];
    // }
    // if (this.creationTime.length > 1) {
    //   this.pagerequest.to = this.creationTime[1];
    // }

    await this.$store.dispatch({
      type: "article/getAll",
      data: this.pagerequest,
    });
  }
  get pageSize() {
    return this.$store.state.article.pageSize;
  }
  get totalCount() {
    return this.$store.state.article.totalCount;
  }
  get currentPage() {
    return this.$store.state.article.currentPage;
  }

  columns = [
    {
      title: this.L("ArticleTitle"),
      key: "title",
    },
    {
      title: this.L("ArticleAuthor"),
      key: "author",
    },
    {
      title: this.L("IsActive"),
      render: (h: any, params: any) => {
        return h("span", params.row.isActive ? this.L("Yes") : this.L("No"));
      },
    },
    {
      title: this.L("Actions"),
      key: "Actions",
       150,
      render: (h: any, params: any) => {
        return h("div", [
          h(
            "Button",
            {
              props: {
                type: "primary",
                size: "small",
              },
              style: {
                marginRight: "5px",
              },
              on: {
                click: () => {
                  this.$store.commit("article/edit", params.row);
                  this.$parent.currentOpt = "edit";
                  this.$parent.currentRouter = "edit";
                  this.$parent.currentId = params.row.id;
                },
              },
            },
            this.L("Edit")
          ),
          h(
            "Button",
            {
              props: {
                type: "error",
                size: "small",
              },
              on: {
                click: async () => {
                  this.$Modal.confirm({
                    title: this.L("Tips"),
                    content: this.L("DeleteUserConfirm"),
                    okText: this.L("Yes"),
                    cancelText: this.L("No"),
                    onOk: async () => {
                      await this.$store.dispatch({
                        type: "article/delete",
                        data: params.row,
                      });
                      await this.getpage();
                    },
                  });
                },
              },
            },
            this.L("Delete")
          ),
        ]);
      },
    },
  ];
  async created() {
    this.getpage();
  }
}
</script>
View Code

edit.vue

<template>
  <div>
    <Form ref="subForm" label-position="top" :rules="rules" :model="formModel">
      <Tabs value="detail">
        <TabPane :label="L('ArticleDetails')" name="detail">
          <FormItem :label="L('ArticleTitle')" prop="title">
            <Input
              v-model="formModel.title"
              :maxlength="32"
              :minlength="2"
            ></Input>
          </FormItem>
          <FormItem :label="L('ArticleType')" prop="articleType">
            <Select
              :placeholder="L('ArticleType')"
              v-model="formModel.articleType"
            >
              <Option
                v-for="item in articleTypeDataItems"
                :key="item.id"
                :label="item.name"
                :value="item.id"
                >{{ item.name }}</Option
              >
            </Select>
          </FormItem>
          <FormItem :label="L('ArticleCoverImg')" prop="coverImg">
            <Input v-model="formModel.coverImg"></Input>
            <Upload
              name="coverImg"
              ref="upload"
              :show-upload-list="false"
              :format="['jpg', 'jpeg', 'png']"
              :max-size="2048"
              :on-format-error="handleFormatError"
              :on-exceeded-size="handleMaxSize"
              :before-upload="handleBeforeUpload"
              :on-success="handleSuccess"
              :multiple="false"
              type="drag"
              action="http://localhost:21021/api/FileCommon/UploadFile"
            >
              <Button icon="ios-cloud-upload-outline">{{
                L("UploadFiles")
              }}</Button>
            </Upload>
          </FormItem>
          <FormItem :label="L('ArticleAuthor')" prop="author">
            <Input v-model="formModel.author" :maxlength="32"></Input>
          </FormItem>
          <FormItem>
            <Checkbox v-model="formModel.isActive">{{
              L("IsActive")
            }}</Checkbox>
          </FormItem>
          <FormItem :label="L('ArticleContents')" prop="contents">
            <quill-editor
              v-model="formModel.contents"
              ref="myQuillEditor"
              style="height: 500px;margin-bottom:30px;"
              :options="editorOption"
            >
              <!-- 自定义toolar -->
              <div id="toolbar" slot="toolbar">
                <!-- Add a bold button -->
                <button class="ql-bold" title="加粗">Bold</button>
                <button class="ql-italic" title="斜体">Italic</button>
                <button class="ql-underline" title="下划线">underline</button>
                <button class="ql-strike" title="删除线">strike</button>
                <button class="ql-blockquote" title="引用"></button>
                <button class="ql-code-block" title="代码"></button>
                <button class="ql-header" value="1" title="标题1"></button>
                <button class="ql-header" value="2" title="标题2"></button>
                <!--Add list -->
                <button
                  class="ql-list"
                  value="ordered"
                  title="有序列表"
                ></button>
                <button
                  class="ql-list"
                  value="bullet"
                  title="无序列表"
                ></button>
                <!-- Add font size dropdown -->
                <select class="ql-header" title="段落格式">
                  <option selected>段落</option>
                  <option value="1">标题1</option>
                  <option value="2">标题2</option>
                  <option value="3">标题3</option>
                  <option value="4">标题4</option>
                  <option value="5">标题5</option>
                  <option value="6">标题6</option>
                </select>
                <select class="ql-size" title="字体大小">
                  <option value="10px">10px</option>
                  <option value="12px">12px</option>
                  <option value="14px">14px</option>
                  <option value="16px" selected>16px</option>
                  <option value="18px">18px</option>
                  <option value="20px">20px</option>
                </select>
                <select class="ql-font" title="字体">
                  <option value="SimSun">宋体</option>
                  <option value="SimHei">黑体</option>
                  <option value="Microsoft-YaHei">微软雅黑</option>
                  <option value="KaiTi">楷体</option>
                  <option value="FangSong">仿宋</option>
                  <option value="Arial">Arial</option>
                </select>
                <!-- Add subscript and superscript buttons -->
                <select
                  class="ql-color"
                  value="color"
                  title="字体颜色"
                ></select>
                <select
                  class="ql-background"
                  value="background"
                  title="背景颜色"
                ></select>
                <select class="ql-align" value="align" title="对齐"></select>
                <button class="ql-clean" title="还原"></button>
                <!-- You can also add your own -->
              </div>
            </quill-editor>
          </FormItem>
        </TabPane>
      </Tabs>
    </Form>
    <div slot="footer" >
      <Button @click="cancel">{{ L("Cancel") }}</Button>
      &nbsp;&nbsp;&nbsp;
      <Button @click="save" type="primary">{{ L("OK") }}</Button>
    </div>
  </div>
</template>
<script lang="ts">
import { Quill, quillEditor } from "vue-quill-editor";
import "quill/dist/quill.core.css";
import "quill/dist/quill.snow.css";
import "quill/dist/quill.bubble.css";
//引入font.css
import "@/assets/css/font.css";

// 自定义字体大小
let Size = Quill.import("attributors/style/size");
Size.whitelist = ["10px", "12px", "14px", "16px", "18px", "20px"];
Quill.register(Size, true);

// 自定义字体类型
var fonts = [
  "SimSun",
  "SimHei",
  "Microsoft-YaHei",
  "KaiTi",
  "FangSong",
  "Arial",
  "Times-New-Roman",
  "sans-serif",
  "宋体",
  "黑体",
];
var Font = Quill.import("formats/font");
Font.whitelist = fonts;
Quill.register(Font, true);

import { ArticleTypeDataItems } from "@/lib/constData";
import { Component, Vue, Inject, Prop, Watch } from "vue-property-decorator";
import Util from "../../../lib/util";
import AbpBase from "../../../lib/abpbase";
import Article from "@/store/entities/article";

@Component({
  components: { quillEditor },
})
export default class EditArticle extends AbpBase {
  @Prop({ type: Boolean, default: false }) value: boolean;
  formModel: Article = new Article();
  articleTypeDataItems = ArticleTypeDataItems;
  uploadFormat = [".jpg", ".png", ".jpeg"];
  editorOption = {
    placeholder: "请输入",
    theme: "snow", // or 'bubble'
    modules: {
      toolbar: {
        container: "#toolbar",
      },
    },
  };
  handleSuccess(res, file) {
    console.log(res.data);
    file.url = res.data.fileUrl;
    file.name = res.data.fileUrl;
    this.formModel.coverImg = res.data.fileUrl;
  }
  handleFormatError(file) {
    this.$Notice.warning({
      title: "The file format is incorrect",
      desc:
        "File format of " +
        file.name +
        " is incorrect, please select jpg or png.",
    });
  }
  handleMaxSize(file) {
    this.$Notice.warning({
      title: "Exceeding file size limit",
      desc: "File  " + file.name + " is too large, no more than 2M.",
    });
  }
  handleBeforeUpload() {
    //上传文件之前的逻辑处理,return false 阻止上传
    const check = false;
    if (!check) {
      this.$Notice.warning({
        title: "error,can't upload",
      });
    }
    return check;
  }
  created() {
    let editModel = null;
    if (this.$store.state.article.editArticle) {
      editModel = this.$store.state.article.editArticle;
    }
    this.formModel = Util.extend(true, {}, editModel);
  }
  // articleTypeChange(val: string) {
  //   this.formModel.articleType = parseInt(val);
  // }
  save() {
    (this.$refs.subForm as any).validate(async (valid: boolean) => {
      if (valid) {
        let typeName = "article/create";
        if (this && this.formModel && this.formModel.id) {
          typeName = "article/update";
        }
        await this.$store.dispatch({
          type: typeName,
          data: this.formModel,
        });
        (this.$refs.subForm as any).resetFields();
        this.$emit("save-success");
        this.$emit("input", false);
        this.$Notice.success({
          title: "tips",
          desc: "success",
        });
      }
    });
  }
  cancel() {
    this.$parent.currentOpt = "list";
    this.$parent.currentRouter = "list";
    this.$parent.currentId = 0;
  }
  rules = {
    title: [
      {
        required: true,
        message: this.L("FieldIsRequired", undefined, this.L("ArticleTitle")),
        trigger: "blur",
      },
    ],
    author: [
      {
        required: true,
        message: this.L("FieldIsRequired", undefined, this.L("ArticleAuthor")),
        trigger: "blur",
      },
    ],
    contents: [
      {
        required: true,
        message: this.L(
          "FieldIsRequired",
          undefined,
          this.L("ArticleContents")
        ),
        trigger: "blur",
      },
    ],
    articleType: [
      {
        required: true,
        message: this.L("FieldIsRequired", undefined, this.L("ArticleType")),
        trigger: "change",
        type: "number",
      },
    ],
  };
}
</script>
View Code

list.vue使用ElementUI版本,main.ts中增加引用和基本样式:import ElementUI from 'element-ui';import 'element-ui/lib/theme-chalk/index.css';这个样式路径下面还有其他相关样式,需要可以自己引用一下。

<template>
  <div>
    <div class="app-container">
      <div class="filter-container">
        <el-row :gutter="15">
          <el-col :md="6">
            <el-input
              v-model="pagerequest.keyword"
              :placeholder="L('ArticleTitle')"
              class="filter-item"
              size="mini"
            />
          </el-col>
          <el-col :md="6">
            <el-button
              @click="create"
              icon="android-add"
              type="primary"
              size="mini"
              >{{ L("Add") }}</el-button
            >
            <el-button
              class="filter-item"
              type="primary"
              icon="el-icon-search"
              @click="getpage"
              size="mini"
              >{{ L("Find") }}</el-button
            >
          </el-col>
        </el-row>
      </div>

      <el-table
        v-loading="loading"
        :data="list"
        fit
        highlight-current-row
        style=" 100%"
        class="table-scroll-x"
      >
        <el-table-column label="ID" prop="id"></el-table-column>
        <el-table-column label="标题" prop="title"></el-table-column>
        <el-table-column label="作者" prop="author"></el-table-column>
        <el-table-column label="操作" class-name="small-padding">
          <template slot-scope="{ row }">
            <span class="link-type" @click="handleEdit(row)">编辑</span>
            <span class="link-type" @click="handleDelete(row)">删除</span>
          </template>
        </el-table-column>
      </el-table>

      <pagination
        v-show="totalCount > 0"
        :total="totalCount"
        :page="currentPage"
        :limit="pageSize"
        @pagination="pageChange"
      />
    </div>
  </div>
</template>
<script lang='ts'>
import { Component, Vue, Inject, Prop, Watch } from "vue-property-decorator";
import Pagination from "@/components/Pagination/index";
import Util from "@/lib/util";
import AbpBase from "@/lib/abpbase";
import PageRequest from "@/store/entities/page-request";

class PageModelRequest extends PageRequest {
  keyword: string;
  isActive: boolean = null; //nullable
}

@Component({
  components: { Pagination },
})
export default class AbpArticles extends AbpBase {
  // createModalShow: boolean = false;
  // editModalShow: boolean = false;
  pagerequest: PageModelRequest = new PageModelRequest();
  // creationTime: Date[] = [];
  handleEdit(row) {
    this.$store.commit("article/edit", row);
    this.$parent.currentOpt = "edit";
    this.$parent.currentRouter = "edit";
    this.$parent.currentId = row.id;
  }
  handleDelete(row) {
    this.$Modal.confirm({
      title: this.L("Tips"),
      content: this.L("DeleteUserConfirm"),
      okText: this.L("Yes"),
      cancelText: this.L("No"),
      onOk: async () => {
        await this.$store.dispatch({
          type: "article/delete",
          data: row,
        });
        await this.getpage();
      },
    });
  }
  edit() {
    // this.editModalShow=true;
  }
  get list() {
    return this.$store.state.article.list;
  }
  get loading() {
    return this.$store.state.article.loading;
  }
  create() {
    this.$parent.currentOpt = "create";
    this.$parent.currentRouter = "edit";
  }
  isActiveChange(val: string) {
    if (val === "Actived") {
      this.pagerequest.isActive = true;
    } else if (val === "NoActive") {
      this.pagerequest.isActive = false;
    } else {
      this.pagerequest.isActive = null;
    }
  }
  pageChange(page: number, pagesize: number) {
    this.$store.commit("article/setCurrentPage", page);
    this.$store.commit("article/setPageSize", pagesize);
    this.getpage();
  }
  pagesizeChange(pagesize: number) {
    this.$store.commit("article/setPageSize", pagesize);
    this.getpage();
  }
  async getpage() {
    this.pagerequest.maxResultCount = this.pageSize;
    this.pagerequest.skipCount = (this.currentPage - 1) * this.pageSize;
    //filters

    // if (this.creationTime.length > 0) {
    //   this.pagerequest.from = this.creationTime[0];
    // }
    // if (this.creationTime.length > 1) {
    //   this.pagerequest.to = this.creationTime[1];
    // }

    await this.$store.dispatch({
      type: "article/getAll",
      data: this.pagerequest,
    });
  }
  get pageSize() {
    return this.$store.state.article.pageSize;
  }
  get totalCount() {
    return this.$store.state.article.totalCount;
  }
  get currentPage() {
    return this.$store.state.article.currentPage;
  }

  columns = [
    {
      title: this.L("ArticleTitle"),
      key: "title",
    },
    {
      title: this.L("ArticleAuthor"),
      key: "author",
    },
    {
      title: this.L("IsActive"),
      render: (h: any, params: any) => {
        return h("span", params.row.isActive ? this.L("Yes") : this.L("No"));
      },
    },
    {
      title: this.L("Actions"),
      key: "Actions",
       150,
      render: (h: any, params: any) => {
        return h("div", [
          h(
            "Button",
            {
              props: {
                type: "primary",
                size: "small",
              },
              style: {
                marginRight: "5px",
              },
              on: {
                click: () => {
                  this.$store.commit("article/edit", params.row);
                  this.$parent.currentOpt = "edit";
                  this.$parent.currentRouter = "edit";
                  this.$parent.currentId = params.row.id;
                },
              },
            },
            this.L("Edit")
          ),
          h(
            "Button",
            {
              props: {
                type: "error",
                size: "small",
              },
              on: {
                click: async () => {
                  this.$Modal.confirm({
                    title: this.L("Tips"),
                    content: this.L("DeleteUserConfirm"),
                    okText: this.L("Yes"),
                    cancelText: this.L("No"),
                    onOk: async () => {
                      await this.$store.dispatch({
                        type: "article/delete",
                        data: params.row,
                      });
                      await this.getpage();
                    },
                  });
                },
              },
            },
            this.L("Delete")
          ),
        ]);
      },
    },
  ];
  async created() {
    this.getpage();
  }
}
</script>
View Code

edit.vue使用ElementUI版本

<template>
  <div class="app-container">
    <el-form
      :model="formModel"
      :rules="rules"
      ref="subForm"
      label-width="150px"
      class="form-container"
    >
      <div class="createPost-main-container">
        <!-- <el-row :gutter="rowGutter">
            <el-divider content-position="left">
              <span style="color: rgba(41, 155, 255, 0.67);">{{L('ArticleDetails')}}</span>
            </el-divider>
          </el-row> -->
        <el-row :gutter="rowGutter">
          <el-col :span="8">
            <el-form-item
              :label="L('ArticleTitle')"
              prop="title"
              class="postInfo-container-item"
            >
              <el-input
                :maxlength="32"
                :minlength="2"
                v-model="formModel.title"
              />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item :label="L('ArticleType')" prop="articleType">
              <el-select
                size="mini"
                v-model="formModel.articleType"
                clearable
                class="filter-item"
              >
                <el-option
                  v-for="item in articleTypeDataItems"
                  :key="item.id"
                  :label="item.name"
                  :value="item.id"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="rowGutter">
          <el-col :span="8">
            <el-form-item :label="L('ArticleCoverImg')" prop="coverImg">
              <el-input v-model="formModel.coverImg" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item>
              <el-button type="primary" class="filter-item" size="mini">
                <label for="file-upload">
                  {{ L("UploadFiles") }}
                  <input
                    type="file"
                    id="file-upload"
                    style="display: none"
                    accept=".png, .jpg, .jpeg"
                    @change="uploadFiles"
                  />
                </label>
              </el-button>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="rowGutter">
          <el-col :span="8">
            <el-form-item :label="L('ArticleAuthor')" prop="author">
              <el-input :maxlength="32" v-model="formModel.author" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item prop="isActive">
              <el-checkbox v-model="formModel.isActive">{{
                L("IsActive")
              }}</el-checkbox>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="rowGutter">
          <el-col :span="24">
            <el-form-item :label="L('ArticleContents')" prop="contents">
              <quill-editor
                v-model="formModel.contents"
                ref="myQuillEditor"
                style="height: 500px; margin-bottom: 30px"
                :options="editorOption"
              >
                <!-- 自定义toolar -->
                <div id="toolbar" slot="toolbar">
                  <!-- Add a bold button -->
                  <button class="ql-bold" title="加粗">Bold</button>
                  <button class="ql-italic" title="斜体">Italic</button>
                  <button class="ql-underline" title="下划线">underline</button>
                  <button class="ql-strike" title="删除线">strike</button>
                  <button class="ql-blockquote" title="引用"></button>
                  <button class="ql-code-block" title="代码"></button>
                  <button class="ql-header" value="1" title="标题1"></button>
                  <button class="ql-header" value="2" title="标题2"></button>
                  <!--Add list -->
                  <button
                    class="ql-list"
                    value="ordered"
                    title="有序列表"
                  ></button>
                  <button
                    class="ql-list"
                    value="bullet"
                    title="无序列表"
                  ></button>
                  <!-- Add font size dropdown -->
                  <select class="ql-header" title="段落格式">
                    <option selected>段落</option>
                    <option value="1">标题1</option>
                    <option value="2">标题2</option>
                    <option value="3">标题3</option>
                    <option value="4">标题4</option>
                    <option value="5">标题5</option>
                    <option value="6">标题6</option>
                  </select>
                  <select class="ql-size" title="字体大小">
                    <option value="10px">10px</option>
                    <option value="12px">12px</option>
                    <option value="14px">14px</option>
                    <option value="16px" selected>16px</option>
                    <option value="18px">18px</option>
                    <option value="20px">20px</option>
                  </select>
                  <select class="ql-font" title="字体">
                    <option value="SimSun">宋体</option>
                    <option value="SimHei">黑体</option>
                    <option value="Microsoft-YaHei">微软雅黑</option>
                    <option value="KaiTi">楷体</option>
                    <option value="FangSong">仿宋</option>
                    <option value="Arial">Arial</option>
                  </select>
                  <!-- Add subscript and superscript buttons -->
                  <select
                    class="ql-color"
                    value="color"
                    title="字体颜色"
                  ></select>
                  <select
                    class="ql-background"
                    value="background"
                    title="背景颜色"
                  ></select>
                  <select class="ql-align" value="align" title="对齐"></select>
                  <button class="ql-clean" title="还原"></button>
                  <!-- You can also add your own -->
                </div>
              </quill-editor>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="rowGutter">
          <el-col :span="8">
            <el-form-item>
              <el-button @click="cancel">{{ L("Back") }}</el-button>
              &nbsp;&nbsp;&nbsp;
              <el-button @click="save" type="primary">{{ L("OK") }}</el-button>
            </el-form-item>
          </el-col>
        </el-row>
      </div>
    </el-form>
  </div>
</template>
<script lang="ts">
import Axios from "axios";
import { Quill, quillEditor } from "vue-quill-editor";
import "quill/dist/quill.core.css";
import "quill/dist/quill.snow.css";
import "quill/dist/quill.bubble.css";
//引入font.css
import "@/assets/css/font.css";
import appconst from "@/lib/appconst";

// 自定义字体大小
let Size = Quill.import("attributors/style/size");
Size.whitelist = ["10px", "12px", "14px", "16px", "18px", "20px"];
Quill.register(Size, true);

// 自定义字体类型
var fonts = [
  "SimSun",
  "SimHei",
  "Microsoft-YaHei",
  "KaiTi",
  "FangSong",
  "Arial",
  "Times-New-Roman",
  "sans-serif",
  "宋体",
  "黑体",
];
var Font = Quill.import("formats/font");
Font.whitelist = fonts;
Quill.register(Font, true);

import { ArticleTypeDataItems } from "@/lib/constData";
import { Component, Vue, Inject, Prop, Watch } from "vue-property-decorator";
import Util from "../../../lib/util";
import AbpBase from "../../../lib/abpbase";
import Article from "@/store/entities/article";

@Component({
  components: { quillEditor },
})
export default class EditArticle extends AbpBase {
  @Prop({ type: Boolean, default: false }) value: boolean;
  activeName = "first";
  rowGutter = 30;
  formModel: Article = new Article();
  articleTypeDataItems = ArticleTypeDataItems;
  uploadFormat = [".jpg", ".png", ".jpeg"];
  editorOption = {
    placeholder: "请输入",
    theme: "snow", // or 'bubble'
    modules: {
      toolbar: {
        container: "#toolbar",
      },
    },
  };
  uploadFiles(e) {
    let file = e.target.files[0];
    /* eslint-disable no-undef */
    let param = new FormData(); // 创建form对象
    param.append("file", file); // 通过append向form对象添加数据
    // param.append("zoneId", zid);
    // console.log(param.get("file")); // FormData私有类对象,访问不到,可以通过get判断值是否传进去
    let config = {
      headers: { "Content-Type": "multipart/form-data" },
    };
    // 添加请求头
    Axios.post(
      appconst.remoteServiceBaseUrl + "/api/FileCommon/UploadFile",
      param,
      config
    ).then((res) => {
      this.formModel.coverImg = res.data.result.fileUrl;
    });
  }
  handleSuccess(res, file) {
    console.log(res.data);
    file.url = res.data.fileUrl;
    file.name = res.data.fileUrl;
    this.formModel.coverImg = res.data.fileUrl;
  }
  handleFormatError(file) {
    this.$Notice.warning({
      title: "The file format is incorrect",
      desc:
        "File format of " +
        file.name +
        " is incorrect, please select jpg or png.",
    });
  }
  handleMaxSize(file) {
    this.$Notice.warning({
      title: "Exceeding file size limit",
      desc: "File  " + file.name + " is too large, no more than 2M.",
    });
  }
  handleBeforeUpload() {
    //上传文件之前的逻辑处理,return false 阻止上传
    const check = false;
    if (!check) {
      this.$Notice.warning({
        title: "error,can't upload",
      });
    }
    return check;
  }
  created() {
    let editModel = null;
    if (this.$store.state.article.editArticle) {
      editModel = this.$store.state.article.editArticle;
    }
    this.formModel = Util.extend(true, {}, editModel);
  }
  // articleTypeChange(val: string) {
  //   this.formModel.articleType = parseInt(val);
  // }
  save() {
    (this.$refs.subForm as any).validate(async (valid: boolean) => {
      if (valid) {
        let typeName = "article/create";
        if (this && this.formModel && this.formModel.id) {
          typeName = "article/update";
        }
        await this.$store.dispatch({
          type: typeName,
          data: this.formModel,
        });
        (this.$refs.subForm as any).resetFields();
        this.$emit("save-success");
        this.$emit("input", false);
        this.$Notice.success({
          title: "tips",
          desc: "success",
        });
      }
    });
  }
  cancel() {
    this.$parent.currentOpt = "list";
    this.$parent.currentRouter = "list";
    this.$parent.currentId = 0;
  }
  rules = {
    title: [
      {
        required: true,
        message: this.L("FieldIsRequired", undefined, this.L("ArticleTitle")),
        trigger: "blur",
      },
    ],
    author: [
      {
        required: true,
        message: this.L("FieldIsRequired", undefined, this.L("ArticleAuthor")),
        trigger: "blur",
      },
    ],
    contents: [
      {
        required: true,
        message: this.L(
          "FieldIsRequired",
          undefined,
          this.L("ArticleContents")
        ),
        trigger: "blur",
      },
    ],
    articleType: [
      {
        required: true,
        message: this.L("FieldIsRequired", undefined, this.L("ArticleType")),
        trigger: "change",
        type: "number",
      },
    ],
  };
}
</script>
View Code

5、分页组件src》components》Pagination》index.vue:

<template>
  <div :class="{ hidden: hidden }" class="pagination-container">
    <el-pagination
      :background="background"
      :current-page.sync="currentPage"
      :page-size.sync="pageSize"
      :layout="layout"
      :page-sizes="pageSizes"
      :total="total"
      v-bind="$attrs"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />
  </div>
</template>

<script>
import { scrollTo } from "@/lib/scroll-to";

export default {
  name: "Pagination",
  props: {
    total: {
      required: true,
      type: Number,
    },
    page: {
      type: Number,
      default: 1,
    },
    limit: {
      type: Number,
      default: 20,
    },
    pageSizes: {
      type: Array,
      default() {
        return [10, 20, 30, 50];
      },
    },
    layout: {
      type: String,
      default: "total, sizes, prev, pager, next, jumper",
    },
    background: {
      type: Boolean,
      default: true,
    },
    autoScroll: {
      type: Boolean,
      default: true,
    },
    hidden: {
      type: Boolean,
      default: false,
    },
  },
  computed: {
    currentPage: {
      get() {
        return this.page;
      },
      set(val) {
        this.$emit("update:page", val);
      },
    },
    pageSize: {
      get() {
        return this.limit;
      },
      set(val) {
        this.$emit("update:limit", val);
      },
    },
  },
  methods: {
    handleSizeChange(val) {
      this.$emit("pagination", this.currentPage, val);
      if (this.autoScroll) {
        scrollTo(0, 800);
      }
    },
    handleCurrentChange(val) {
      this.$emit("pagination", val, this.pageSize);
      if (this.autoScroll) {
        scrollTo(0, 800);
      }
    },
  },
};
</script>

<style scoped>
.pagination-container {
  background: #fff;
  padding: 0;
}
.pagination-container.hidden {
  display: none;
}
</style>
View Code

6、富文本编辑器我使用的是 npm install vue-quill-editor -save,参考文档:https://www.cnblogs.com/seven077/p/11313137.html

7、使用ViewUI的Upload组件上传文件,这个类写在Framework.Web.Core项目中的Controller文件夹下,这个项目下面可以使用 HttpContext.Request.Form.Files 来获取上传的文件列表,

如果写在Application这个项目里,方法要使用参数IFormFile,这个我没弄成功。

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Abp.UI;
using System.IO;
using Framework.Common;
using Microsoft.AspNetCore.Http;

namespace Framework.Controllers
{
    [Route("api/[controller]/[action]")]
    public class FileCommonController : FrameworkControllerBase
    {
        public FileCommonController() { }
        /// <summary>
        /// 上传一个文件,并返回文件上传成功后的信息
        /// </summary>
        /// <param name="file">要上传的文件实体</param>
        /// <returns>文件上传成功后返回的文件相关信息</returns>
        [HttpPost]
        public async Task<FileUploadOutputDto> UploadFile()
        {
            try
            {
                var file = HttpContext.Request.Form.Files[0];

                //文件的原始名称
                string fileOriginName = file.FileName;

                //读取文件保存的根目录
                string fileSaveRootDir = Utils.GetAppSetting("App", "FileRootPath");
                //读取办公管理文件保存的模块的根目录
                //string fileSaveDir = Utils.GetAppSetting("App", "OAFiles");
                //文件保存的相对文件夹(保存到wwwroot目录下)
                string absoluteFileDir = fileSaveRootDir;

                //文件保存的路径(应用的工作目录+文件夹相对路径);
                string fileSavePath = Environment.CurrentDirectory + "/wwwroot/" + absoluteFileDir;
                if (!Directory.Exists(fileSavePath))
                {
                    Directory.CreateDirectory(fileSavePath);
                }

                //生成文件的名称
                string extensionName = Path.GetExtension(fileOriginName);//获取文件的源后缀
                if (string.IsNullOrEmpty(extensionName))
                {
                    throw new UserFriendlyException("文件上传的原始名称好像不对哦,没有找到文件后缀");
                }
                string fileName = Guid.NewGuid().ToString() + extensionName;//通过uuid和原始后缀生成新的文件名

                //最终生成的文件的相对路径(xxx/xxx/xx.xx)
                string finalyFilePath = fileSavePath + "/" + fileName;

                FileUploadOutputDto result = new FileUploadOutputDto();

                //打开上传文件的输入流
                using (Stream stream = file.OpenReadStream())
                {
                    //创建输入流的reader
                    //var fileType = stream.GetFileType();
                    //文件大小
                    result.FileLength = stream.Length;
                    result.FileName = fileOriginName;
                    result.FileType = extensionName.Substring(1);
                    result.FileUrl = absoluteFileDir + "/" + fileName;

                    //开始保存拷贝文件
                    using (FileStream targetFileStream = new FileStream(finalyFilePath, FileMode.OpenOrCreate))
                    {
                        await stream.CopyToAsync(targetFileStream);
                    }
                }
                return result;
            }
            catch (Exception ex)
            {
                throw new UserFriendlyException("文件上传失败,原因" + ex.Message);
            }
        }
    }
}

public class FileUploadOutputDto
{
public long FileLength { get; set; }
public string FileName { get; set; }
public string FileType { get; set; }
public string FileUrl { get; set; }
}

Framework.Web.Host》appSettings.json加配置,或者自己随便写也行。

using Abp.Localization;
using Framework.Configuration;
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using System.Reflection;

namespace Framework.Common
{
    public class Utils
    {
        public static ILocalizableString L(string name)
        {
            return new LocalizableString(name, FrameworkConsts.LocalizationSourceName);
        }
        /// <summary>
        /// 获取物理路径
        /// </summary>
        /// <param name="seedFile">/floder1/floder2/</param>
        /// <returns></returns>
        public static string MapPath(string seedFile)
        {
            var absolutePath = new Uri(Assembly.GetExecutingAssembly().Location).AbsolutePath.Replace("/bin/Debug", "").Replace("net5.0", "");//CodeBase
            var directoryName = Path.GetDirectoryName(absolutePath);
            var path = directoryName + seedFile.Replace('/', '\');
            return path;
        }

        private static IConfigurationRoot _appConfiguration = AppConfigurations.Get(System.Environment.CurrentDirectory);

        //用法1(有嵌套):GetAppSetting("Authentication", "JwtBearer:SecurityKey")
        //用法2:GetAppSetting("App", "ServerRootAddress")
        public static string GetAppSetting(string section, string key)
        {
            return _appConfiguration.GetSection(section)[key];
        }

        public static string GetConnectionString(string key)
        {
            return _appConfiguration.GetConnectionString(key);
        }

    }
}

完了之后运行,接口地址就是 /api/FileCommon/UploadFile
abp ViewUI使用Upload组件的代码,一些回调函数我没做,可以自己看着文档写一下。

<Upload
              name="coverImg"
              ref="upload"
              :show-upload-list="false"
              :format="['jpg', 'jpeg', 'png']"
              :max-size="2048"
              multiple
              type="drag"
              action="http://localhost:21021/api/FileCommon/UploadFile"
            >
              <Button icon="ios-cloud-upload-outline">{{
                L("UploadFiles")
              }}</Button>
            </Upload>

这样就可以上传了,参考文章:建议都看一看
http://v1.iviewui.com/components/upload
https://www.cnblogs.com/yanan7890/p/12944523.html
https://blog.csdn.net/war3ismylove/article/details/98201270
https://blog.csdn.net/weixin_34114823/article/details/92453634

原文地址:https://www.cnblogs.com/xsj1989/p/14411681.html