『VUE3后台—大事件管理系统(已完结)』

项目地址:https://gitee.com/csheng-gitee/vue3-big-event-admin
技术栈:VUE3 + Pinia + Pnpm(本项目暂不用 typescript)


一、前期准备工作

1、创建项目

npm install -g pnpm
pnpm create vue

在这里插入图片描述


2、ESLint 配置

  • (1) 禁用 prettier 插件,下载 ESLint 插件;
  • (2) 在 vscode 的 setting.json 设置自动修复:
  // 开启 eslint 自动修复
  "editor.codeActionsOnSave": {
    "source.fixAll": true
  },
  // 关闭保存自动格式化
  "editor.formatOnSave": false,
  • (3) 在项目根目录的 .gitignore.cjs 配置 ESLint 规则:
  rules: {
    'prettier/prettier': [
      'warn',
      {
        singleQuote: true, // 单引号
        semi: false, // 无分号
        printWidth: 80, // 每行宽度至多80字符
        trailingComma: 'none', // 不加对象|数组最后逗号
        endOfLine: 'auto' // 换行符号不限制(win mac 不一致)
      }
    ],
    'vue/multi-word-component-names': [
      'warn',
      {
        ignores: ['index'] // vue组件名称多单词组成(忽略index.vue)
      }
    ],
    'vue/no-setup-props-destructure': ['off'], // 关闭 props 解构的校验
    // 添加未定义变量错误提示,create-vue@3.6.3 关闭,这里加上是为了支持下一个章节演示。
    'no-undef': 'error'
  }

3、husky 代码检查

  • (1) husky 配置:
    此操作须在 bash 窗口输入(&&符号)
pnpm dlx husky-init && pnpm install

修改 .husky/pre-commit 文件

pnpm lint

缺点:默认进行的是全量检查,耗时问题,历史问题。继续步骤2

  • (2) lint-staged 配置:
pnpm i lint-staged -D

配置 package.json

{ 
  "scripts": {
    ....
    "lint-staged": "lint-staged"
  },
  ....
  "lint-staged": {
    "*.{js,ts,vue}": [
      "eslint --fix"
    ]
  }
}

修改 .husky/pre-commit 文件

pnpm lint-staged

4、目录调整

(1)删除 目录下的所有文件:

在这里插入图片描述

(2)修改:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

(3)新增 目录结构

在这里插入图片描述

(4)拷贝全局样式和图片,并且安装预处理器支持

在这里插入图片描述

在这里插入图片描述

pnpm add sass -D

5、VueRouter4 路由语法(可忽略)

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

// createWebHashHistory:hash 模式 / createWebHistory:history 模式
// import.meta.env.BASE_URL :vite.config.js 中的 base 基地址配置
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: []
})

export default router
// vite.config.js
export default defineConfig({
  ....
  base: '/', // 基地址
  ....
})

6、按需引入 Element-Plus

安装 Element-Plus:

pnpm install element-plus

按需导入:

pnpm add -D unplugin-vue-components unplugin-auto-import
// vite.config.js
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
...

export default defineConfig({
  plugins: [
    ...
    AutoImport({
      resolvers: [ElementPlusResolver()]
    }),
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ],
  ....
})

引入组件:

<!-- App.vue -->
<el-button type="primary">Primary</el-button>
<el-button type="success">Success</el-button>

彩蛋:components 文件夹下的组件会自动注册:

<!-- components/TestDemo.vue -->
<test-demo />

7、Pinia 构建用户仓库和持久化

  • (1) 构建用户仓库
// stores/user.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore('big-user', () => {
  const token = ref('')
  const setToken = (newToken) => (token.value = newToken)

  return { token, setToken }
})
<!-- App.vue -->
<script setup>
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
</script>

<template>
  <div>
    <p>token:{{ userStore.token }}</p>
    <el-button type="primary" @click="userStore.setToken('Bearer csheng')"
      >登录</el-button
    >
    <el-button type="success" @click="userStore.setToken('')">退出</el-button>
  </div>
</template>

<style scoped></style>
  • (2) 持久化
pnpm add pinia-plugin-persistedstate -D
// main.js
....
import persist from 'pinia-plugin-persistedstate'
....
app.use(createPinia().use(persist))
...
// stores/user.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore(
  'big-user',
  () => {
    const token = ref('')
    const setToken = (newToken) => (token.value = newToken)

    return { token, setToken }
  },
  {
    persist: true
  }
)
  • (3) 配置仓库统一管理
// stores/index.js
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(persist)

export default pinia

export * from './modules/user'
// stores/modules/user.js
原代码不变,仅路径更改了
// main.js
....
import pinia from '@/stores'

const app = createApp(App)

app.use(pinia)
app.use(router)
// App.vue
import { useUserStore } from '@/stores'

8、数据交互 - 请求工具设计

pnpm add axios

新建 utils/request.js

import axios from 'axios'

const baseURL = 'http://big-event-vue-api-t.itheima.net'

const instance = axios.create({
  // TODO 1. 基础地址,超时时间
})

instance.interceptors.request.use(
  (config) => {
    // TODO 2. 携带token
    return config
  },
  (err) => Promise.reject(err)
)

instance.interceptors.response.use(
  (res) => {
    // TODO 3. 处理业务失败
    // TODO 4. 摘取核心响应数据
    return res
  },
  (err) => {
    // TODO 5. 处理401错误
    return Promise.reject(err)
  }
)

export default instance

完整版:

import axios from 'axios'
import { useUserStore } from '@/stores'
import { ElMessage } from 'element-plus'
import router from '@/router'

const baseURL = 'http://big-event-vue-api-t.itheima.net'

const instance = axios.create({
  // TODO 1. 基础地址,超时时间
  baseURL,
  timeout: 10000
})

// 请求拦截器
instance.interceptors.request.use(
  (config) => {
    // TODO 2. 携带token
    const userStore = useUserStore()
    if (userStore.token) {
      config.headers.Authorization = userStore.token
    }
    return config
  },
  (err) => Promise.reject(err)
)

// 响应拦截器
instance.interceptors.response.use(
  (res) => {
    // TODO 4. 摘取核心响应数据
    if (res.data.code === 0) {
      return res
    }
    // TODO 3. 处理业务失败(错误提示,抛出错误)
    ElMessage.error(res.data.message || '服务异常')
    return Promise.reject(res.data)
  },
  (err) => {
    // TODO 5. 处理401错误(权限不足 或 token 过期)
    if (err.response?.status === 401) {
      router.push('/login')
    }

    // 错误的默认情况(只要给提示)
    ElMessage.error(err.response.data.message || '服务异常')
    return Promise.reject(err)
  }
)

export default instance
export { baseURL }

9、整体路由设计

path文件功能路由级别
/loginviews/login/LoginPage.vue登录&注册一级路由
/views/layout/LayoutContainer.vue布局架子一级路由
├─ /article/manageviews/article/ArticleManage.vue文章管理2级路由
├─ /article/channelviews/article/ArticleChannel.vue频道管理2级路由
├─ /user/profileviews/user/UserProfile.vue个人详情2级路由
├─ /user/avatarviews/user/UserAvatar.vue更换头像2级路由
├─ /user/passwordviews/user/UserPassword.vue重置密码2级路由
  • (1) 新建文件:

在这里插入图片描述

  • (2) 配置路由规则:
// router/index.js
routes: [
  {
    path: '/login',
    component: () => import('@/views/login/LoginPage.vue')
  },
  {
    path: '/',
    component: () => import('@/views/layout/LayoutContainer.vue'),
    redirect: '/article/manage',
    children: [
      {
        path: '/article/manage',
        component: () => import('@/views/article/ArticleManage.vue')
      },
      {
        path: '/article/channel',
        component: () => import('@/views/article/ArticleChannel.vue')
      },
      {
        path: '/user/profile',
        component: () => import('@/views/user/UserProfile.vue')
      },
      {
        path: '/user/avatar',
        component: () => import('@/views/user/UserAvatar.vue')
      },
      {
        path: '/user/password',
        component: () => import('@/views/user/UserPassword.vue')
      }
    ]
  }
]
<!-- App.vue -->
<script setup></script>

<template>
  <router-view></router-view>
</template>

<style scoped></style>
<!-- layout/LayoutContainer -->
<template>
  <div>
    <p>布局架子</p>
    <router-view></router-view>
  </div>
</template>

二、登录注册&layout架子

1、登录&注册

静态结构&基本切换

安装 element-plus 图标库:

pnpm install @element-plus/icons-vue

静态结构准备:

<!-- login/LoginPage.vue -->
<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'

const isRegister = ref(true) // 是否注册
</script>

<template>
  <el-row class="login-page">
    <el-col :span="12" class="bg"></el-col>
    <el-col :span="6" :offset="3" class="form">
      <!-- 注册表单 -->
      <el-form ref="form" size="large" autocomplete="off" v-if="isRegister">
        <el-form-item>
          <h1>注册</h1>
        </el-form-item>
        <el-form-item>
          <el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
        </el-form-item>
        <el-form-item>
          <el-input
            :prefix-icon="Lock"
            type="password"
            placeholder="请输入密码"
          ></el-input>
        </el-form-item>
        <el-form-item>
          <el-input
            :prefix-icon="Lock"
            type="password"
            placeholder="请输入再次密码"
          ></el-input>
        </el-form-item>
        <el-form-item>
          <el-button class="button" type="primary" auto-insert-space>
            注册
          </el-button>
        </el-form-item>
        <el-form-item class="flex">
          <el-link type="info" :underline="false" @click="isRegister = false">
            ← 返回
          </el-link>
        </el-form-item>
      </el-form>
      <!-- 登录表单 -->
      <el-form ref="form" size="large" autocomplete="off" v-else>
        <el-form-item>
          <h1>登录</h1>
        </el-form-item>
        <el-form-item>
          <el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
        </el-form-item>
        <el-form-item>
          <el-input
            name="password"
            :prefix-icon="Lock"
            type="password"
            placeholder="请输入密码"
          ></el-input>
        </el-form-item>
        <el-form-item class="flex">
          <div class="flex">
            <el-checkbox>记住我</el-checkbox>
            <el-link type="primary" :underline="false">忘记密码?</el-link>
          </div>
        </el-form-item>
        <el-form-item>
          <el-button class="button" type="primary" auto-insert-space
            >登录</el-button
          >
        </el-form-item>
        <el-form-item class="flex">
          <el-link type="info" :underline="false" @click="isRegister = true">
            注册 →
          </el-link>
        </el-form-item>
      </el-form>
    </el-col>
  </el-row>
</template>

<style lang="scss" scoped>
.login-page {
  height: 100vh;
  background-color: #fff;
  .bg {
    background:
      url('@/assets/logo2.png') no-repeat 60% center / 240px auto,
      url('@/assets/login_bg.jpg') no-repeat center / cover;
    border-radius: 0 20px 20px 0;
  }
  .form {
    display: flex;
    flex-direction: column;
    justify-content: center;
    user-select: none;
    .title {
      margin: 0 auto;
    }
    .button {
      width: 100%;
    }
    .flex {
      width: 100%;
      display: flex;
      justify-content: space-between;
    }
  }
}
</style>

表单校验

<!-- login/LoginPage.vue -->
<script setup>
...
// 注册Form信息
const formModel = ref({
  username: '',
  password: '',
  repassword: ''
})
// rules规则
const rules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 5, max: 10, message: '用户名必须是5-10位的字符', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    {
      pattern: /^\S{6,15}$/,
      message: '密码必须是6-15位的非空字符',
      trigger: 'blur'
    }
  ],
  repassword: [
    { required: true, message: '请再次输入密码', trigger: 'blur' },
    {
      pattern: /^\S{6,15}$/,
      message: '密码必须是6-15的非空字符',
      trigger: 'blur'
    },
    {
      validator: (rule, value, callback) => {
        if (value !== formModel.value.password) {
          callback(new Error('两次输入密码不一致!'))
        } else {
          callback()
        }
      },
      trigger: 'blur'
    }
  ]
}
...
</script>

<template>
	<el-form :model="formModel" :rules="rules" ....>
	   <el-form-item prop="username">
          <el-input v-model="formModel.username"></el-input>
       </el-form-item>
       ....
    </el-form>
</template>

注册功能

// api/user.js
import request from '@/utils/request'

// 注册
export const userRegisterService = ({ username, password, repassword }) =>
  request.post('/api/reg', { username, password, repassword })
// login/LoginPage.vue
import { userRegisterService } from '@/api/user'
....
const form = ref() // form表单实例
....
// 注册
const register = async () => {
  await form.value.validate()
  await userRegisterService(formModel.value)
  ElMessage.success('注册成功')
  isRegister.value = false
}

/************************************************/

<el-button @click="register" ....>注册</el-button>
// .eslintrc.cjs
module.exports = {
  ....
  globals: {
    ElMessage: 'readonly',
    ElMessageBox: 'readonly',
    ElLoading: 'readonly'
  }
}

登录功能

// api/user.js
// 登录
export const userLoginService = ({ username, password }) =>
  request.post('/api/login', { username, password })
// login/LoginPage.vue
....
// isRegister 每次变化都清空表单
watch(isRegister, () => {
  formModel.value = {
    username: '',
    password: '',
    repassword: ''
  }
})

// 登录
const userStore = useUserStore()
const router = useRouter()
const login = async () => {
  await form.value.validate()
  
  // 加载效果
  const loading = ElLoading.service({
    lock: true,
    text: '登录中',
    background: 'rgba(0, 0, 0, 0.7)'
  })
  const res = await userLoginService(formModel.value)
  loading.close() // 关闭加载效果
  
  userStore.setToken(res.data.token)
  ElMessage.success('登录成功')
  router.push('/')
}

/************************************************/

<el-button @click="login" ....>登录</el-button>

登录访问拦截

// router/index.js
// 登录访问拦截
router.beforeEach((to) => {
  const userStore = useUserStore()
  if (!userStore.token && to.path !== '/login') return '/login'
})

记住账号密码(补充)

pnpm add js-base64
pnpm add js-cookie
import { Base64 } from 'js-base64'
import Cookies from 'js-cookie'

const isRemember = ref(false) // 是否记住账号密码

// 登录
const login = async () => {
  await form.value.validate()
  
  const loading = ElLoading.service({
    lock: true,
    text: '登录中',
    background: 'rgba(0, 0, 0, 0.7)'
  })
  const res = await userLoginService(formModel.value)
  loading.close()

  // 如果记住密码
  if (isRemember.value) {
    // 转码
    const { username, password } = formModel.value
    const localForm = {
      username: Base64.encode(username),
      password: Base64.encode(password)
    }
    // 存到 Cookies
    Cookies.set('LOCAL_KEY', JSON.stringify(localForm))
  } else {
    Cookies.remove('LOCAL_KEY')
  }

  userStore.setToken(res.data.token)
  ElMessage.success('登录成功')
  router.push('/')
}

// 查询是否存了用户名和密码
onMounted(() => {
  const localForm = Cookies.get('LOCAL_KEY')
  if (localForm) {
    isRemember.value = true
    // 解码回填
    try {
      const { username, password } = JSON.parse(localForm)
      formModel.value.username = Base64.decode(username)
      formModel.value.password = Base64.decode(password)
    } catch (error) {
      console.error('本地数据解析失败~', error)
    }
  } else {
    isRemember.value = false
  }
})

/*---------------------------------------------------*/

<el-checkbox v-model="isRemember">记住我</el-checkbox>

2、layout 架子

<!-- layout/LayoutContainer.vue -->
<script setup>
import {
  Management,
  Promotion,
  UserFilled,
  User,
  Crop,
  EditPen,
  SwitchButton,
  CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'
</script>

<template>
  <!-- 容器 -->
  <el-container class="layout-container">
    <!-- 左侧 -->
    <el-aside width="200px">
      <!-- logo -->
      <div class="el-aside__logo"></div>
      <!-- 菜单项 -->
      <el-menu
        active-text-color="#ffd04b"
        background-color="#232323"
        :default-active="$route.path"
        text-color="#fff"
        router
      >
        <!-- 一级菜单 -->
        <el-menu-item index="/article/channel">
          <el-icon><Management /></el-icon>
          <span>文章分类</span>
        </el-menu-item>
        <el-menu-item index="/article/manage">
          <el-icon><Promotion /></el-icon>
          <span>文章管理</span>
        </el-menu-item>
        <!-- 带有二级菜单的一级菜单 -->
        <el-sub-menu index="/user">
          <!-- 一级 -->
          <template #title>
            <el-icon><UserFilled /></el-icon>
            <span>个人中心</span>
          </template>
          <!-- 二级 -->
          <el-menu-item index="/user/profile">
            <el-icon><User /></el-icon>
            <span>基本资料</span>
          </el-menu-item>
          <!-- 二级 -->
          <el-menu-item index="/user/avatar">
            <el-icon><Crop /></el-icon>
            <span>更换头像</span>
          </el-menu-item>
          <!-- 二级 -->
          <el-menu-item index="/user/password">
            <el-icon><EditPen /></el-icon>
            <span>重置密码</span>
          </el-menu-item>
        </el-sub-menu>
      </el-menu>
    </el-aside>
    <!-- 右侧 -->
    <el-container>
      <!-- 头部 -->
      <el-header>
        <!-- 最左 -->
        <div>当前用户:<strong>陈升</strong></div>
        <!-- 最右 -->
        <el-dropdown placement="bottom-end">
          <span class="el-dropdown__box">
            <el-avatar :src="avatar" />
            <el-icon><CaretBottom /></el-icon>
          </span>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item command="profile" :icon="User"
                >基本资料</el-dropdown-item
              >
              <el-dropdown-item command="avatar" :icon="Crop"
                >更换头像</el-dropdown-item
              >
              <el-dropdown-item command="password" :icon="EditPen"
                >重置密码</el-dropdown-item
              >
              <el-dropdown-item command="logout" :icon="SwitchButton"
                >退出登录</el-dropdown-item
              >
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </el-header>
      <!-- 主要内容 -->
      <el-main>
        <router-view></router-view>
      </el-main>
      <el-footer>大事件 ©2023 Created by 陈升</el-footer>
    </el-container>
  </el-container>
</template>

<style lang="scss" scoped>
.layout-container {
  height: 100vh;
  .el-aside {
    background-color: #232323;
    &__logo {
      height: 120px;
      background: url('@/assets/logo.png') no-repeat center / 120px auto;
    }
    .el-menu {
      border-right: none;
    }
  }
  .el-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background-color: #fff;
    .el-dropdown__box {
      display: flex;
      align-items: center;
      .el-icon {
        color: #999;
        margin-left: 10px;
      }
      &:active,
      &:focus {
        outline: none;
      }
    }
  }
  .el-footer {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    color: #666;
  }
}
</style>

3、用户基本信息获取&渲染

// api/user.js
// 获取用户基本信息
export const userGetInfoService = () => request.get('/my/userinfo')
// stores/modules/user.js
import { userGetInfoService } from '@/api/user'
....
const user = ref({})
const getUser = async () => {
  const res = await userGetInfoService()
  user.value = res.data.data
}
// layout/LayoutContainer.vue
import { useUserStore } from '@/stores'
import { onMounted } from 'vue'

const userStore = useUserStore()
onMounted(() => {
  userStore.getUser()
})

<div>当前用户:<strong>{{userStore.user.nickname || userStore.user.username}}</strong></div>
<el-avatar :src="userStore.user.user_pic || avatar" />

4、退出登录

// stores/modules/user.js
const setUser = (newUser) => (user.value = newUser)
<!-- layout/LayoutContainer -->
<script setup>
....
// 点击下拉项
const router = useRouter()
const onCommand = async (command) => {
  if (command === 'logout') {
    await ElMessageBox.confirm('你确认退出大事件吗?', '温馨提示', {
      type: 'warning',
      confirmButtonText: '确认',
      cancelButtonText: '取消'
    })
    userStore.setToken('')
    userStore.setUser({})
    router.push('/login')
  } else {
    router.push(`/user/${command}`)
  }
}
</script>

<el-dropdown @command="onCommand">....

三、文章分类

1、PageContainer 封装

<!-- components/PageContainer.vue -->
<script setup>
defineProps({
  title: { required: true, type: String }
})
</script>

<template>
  <el-card class="page-container">
    <template #header>
      <div class="header">
        <span>{{ title }}</span>
        <div class="extra">
          <!-- 具名插槽 -->
          <slot name="extra"></slot>
        </div>
      </div>
    </template>
    <!-- 默认插槽 -->
    <slot></slot>
  </el-card>
</template>

<style lang="scss" scoped>
.page-container {
  min-height: 100%;
  box-sizing: border-box;
  .header {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
}
</style>
<!-- article/ArticleChannel.vue -->
<script setup></script>

<template>
  <PageContainer title="文章分类">
    <template #extra>
      <el-button type="primary">添加分类</el-button>
    </template>

    主体部分
  </PageContainer>
</template>
<!-- article/ArticleManage.vue -->
<script setup></script>

<template>
  <PageContainer title="文章管理">
    <template #extra>
      <el-button type="primary">添加文章</el-button>
    </template>

    主体部分
  </PageContainer>
</template>

2、文章分类渲染

// api/article.js
import request from '@/utils/request'

// 获取文章分类信息
export const artGetChannelsService = () => request.get('/my/cate/list')
<!-- article/ArticleChannel.vue -->
<script setup>
import { artGetChannelsService } from '@/api/article'
import { ref } from 'vue'

// 获取文章分类数据
const loading = ref(false)
const channelList = ref([])
const getChannelList = async () => {
  loading.value = true
  const res = await artGetChannelsService()
  channelList.value = res.data.data
  loading.value = false
}
getChannelList()

// 编辑
const onEditChannel = (row) => {
  console.log(row)
}
// 删除
const onDelChannel = (row) => {
  console.log(row)
}
</script>

<template>
  <PageContainer title="文章分类">
    ....

    <el-table v-loading="loading" :data="channelList" style="width: 100%">
      <el-table-column label="序号" width="100" type="index"> </el-table-column>
      <el-table-column label="分类名称" prop="cate_name"></el-table-column>
      <el-table-column label="分类别名" prop="cate_alias"></el-table-column>
      <el-table-column label="操作" width="100">
        <template #default="{ row }">
          <el-button
            :icon="Edit"
            circle
            plain
            type="primary"
            @click="onEditChannel(row)"
          ></el-button>
          <el-button
            :icon="Delete"
            circle
            plain
            type="danger"
            @click="onDelChannel(row)"
          ></el-button>
        </template>
      </el-table-column>
      <template #empty>
        <el-empty description="没有数据" />
      </template>
    </el-table>
  </PageContainer>
</template>

3、文章分类添加/编辑/删除

// api/article.js
// 添加文章分类
export const artAddChannelService = (data) => request.post('/my/cate/add', data)
// 编辑文章分类
export const artEditChannelService = (data) =>
  request.put('/my/cate/info', data)
// 删除文章分类
export const artDelChannelService = (id) =>
  request.delete('/my/cate/del', { params: { id } })

四、文章管理

1、文章列表渲染

<!-- article/ArticleManage.vue -->
<script setup>
import { Delete, Edit } from '@element-plus/icons-vue'
import { ref } from 'vue'

// 暂时使用的假数据
const articleList = ref([
  {
    id: 5961,
    title: '新的文章啊',
    pub_date: '2022-07-10 14:53:52.604',
    state: '已发布',
    cate_name: '体育'
  },
  {
    id: 5962,
    title: '新的文章啊',
    pub_date: '2022-07-10 14:54:30.904',
    state: null,
    cate_name: '体育'
  }
])

// 编辑文章
const onEditArticle = (row) => {
  console.log(row)
}
// 删除文章
const onDeleteArticle = (row) => {
  console.log(row)
}
</script>

<template>
  <PageContainer title="文章管理">
    <template #extra>
      <el-button type="primary">添加文章</el-button>
    </template>

    <!-- 筛选项 -->
    <el-form inline>
      <el-form-item label="文章分类:">
        <el-select>
          <el-option label="新闻" value="111"></el-option>
          <el-option label="体育" value="222"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="发布状态:">
        <el-select>
          <el-option label="已发布" value="已发布"></el-option>
          <el-option label="草稿" value="草稿"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary">搜索</el-button>
        <el-button>重置</el-button>
      </el-form-item>
    </el-form>

    <!-- 表格区域 -->
    <el-table :data="articleList" style="width: 100%">
      <el-table-column label="文章标题" width="400">
        <template #default="{ row }">
          <el-link type="primary" :underline="false">{{ row.title }}</el-link>
        </template>
      </el-table-column>
      <el-table-column label="分类" prop="cate_name"></el-table-column>
      <el-table-column label="发布时间" prop="pub_date"></el-table-column>
      <el-table-column label="状态" prop="state"></el-table-column>
      <el-table-column label="操作" width="100">
        <template #default="{ row }">
          <el-button
            :icon="Edit"
            circle
            plain
            type="primary"
            @click="onEditArticle(row)"
          ></el-button>
          <el-button
            :icon="Delete"
            circle
            plain
            type="danger"
            @click="onDeleteArticle(row)"
          ></el-button>
        </template>
      </el-table-column>
      <template #empty>
        <el-empty description="没有数据" />
      </template>
    </el-table>
  </PageContainer>
</template>

2、中英国际化处理

<!-- App.vue -->
<script setup>
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
</script>

<template>
  <el-config-provider :locale="zhCn">
    <router-view></router-view>
  </el-config-provider>
</template>

<style scoped></style>

3、文章分类选择

<!-- article/components/ChannelSelect.vue -->
<script setup>
import { artGetChannelsService } from '@/api/article'
import { ref } from 'vue'

// 双向绑定
defineProps({
  modelValue: {
    type: [Number, String]
  },
  width: {
    type: String
  }
})
const emit = defineEmits(['update:modelValue'])

// 获取文章分类数据
const channelList = ref([])
const getChannelList = async () => {
  const res = await artGetChannelsService()
  channelList.value = res.data.data
}
getChannelList()
</script>

<template>
  <el-select
    :modelValue="modelValue"
    :style="{ width }"
    @update:modelValue="emit('update:modelValue', $event)"
  >
    <el-option
      v-for="item in channelList"
      :key="item.id"
      :label="item.cate_name"
      :value="item.id"
    ></el-option>
  </el-select>
</template>

<style lang="scss" scoped></style>
<!-- article/components/ArticleManage.vue -->
<script setup>
....
import ChannelSelect from './components/ChannelSelect.vue'

// 列表请求参数
const params = ref({
  pagenum: 1,
  pagesize: 5,
  cate_id: '',
  state: ''
})
....
</script>

<template>
	<channel-select v-model="params.cate_id"></channel-select>
	....
    <el-form-item label="发布状态:">
       <el-select v-model="params.state">...</el-select>
    </el-form-item>
</template>

4、渲染文章列表

// api/article.js
// 获取文章列表
export const artGetListService = (params) =>
  request.get('/my/article/list', { params })
// utils/format.js
import { dayjs } from 'element-plus'

export const formatTime = (time) => dayjs(time).format('YYYY年MM月DD日')
// article/components/ArticleManage.vue
import { artGetListService } from '@/api/article'
import { formatTime } from '@/utils/format'

....
const articleList = ref([]) // 文章列表
const total = ref(0) // 总条数

// 获取用户列表
const getArticleList = async () => {
  const res = await artGetListService(params.value)
  articleList.value = res.data.data
  total.value = res.data.total
}
getArticleList()

/*---------------------------------------------*/

<el-table-column label="发布时间">
   <template #default="{ row }">
     {{ formatTime(row.pub_date) }}
   </template>
</el-table-column>

5、分页

const loading = ref(false)

// 获取用户列表
const getArticleList = async () => {
  loading.value = true
  const res = await artGetListService(params.value)
  loading.value = false
  ....
}
// 分页
const onSizeChange = (size) => {
  params.value.pagenum = 1
  params.value.pagesize = size
  getArticleList()
}
const onCurrentChange = (page) => {
  params.value.pagenum = page
  getArticleList()
}

/*---------------------------------------------*/

<el-table v-loading="loading" ....

<el-pagination
  v-model:current-page="params.pagenum"
  v-model:page-size="params.pagesize"
  :page-sizes="[2, 3, 4, 5, 10]"
  layout="jumper, total, sizes, prev, pager, next"
  background
  :total="total"
  @size-change="onSizeChange"
  @current-change="onCurrentChange"
  style="margin-top: 20px; justify-content: flex-end"
/>

6、搜索&重置

// 搜索和重置
const onSearch = () => {
  params.value.pagenum = 1
  getArticleList()
}
const onReset = () => {
  params.value.pagenum = 1
  params.value.cate_id = ''
  params.value.state = ''
  getArticleList()
}

/*---------------------------------------------------------*/

<el-button @click="onSearch" type="primary">搜索</el-button>
<el-button @click="onReset">重置</el-button>

7、新增/编辑打开抽屉

新建 article/components/ArticleEdit.vue

<script setup>
import { ref } from 'vue'
import ChannelSelect from './ChannelSelect.vue'

// 默认表单(初始化)
const defaultForm = {
  title: '',
  cate_id: '',
  cover_img: '',
  content: '',
  state: ''
}
// 抽屉表单(备份)
const formModel = ref({
  title: '',
  cate_id: '',
  cover_img: '',
  content: '',
  state: ''
})

// 抽屉
const visibleDrawer = ref(false)
const open = async (row) => {
  visibleDrawer.value = true
  if (row?.id) {
    console.log('编辑回显')
  } else {
    console.log('新增回显')
    formModel.value = { ...defaultForm.value }
  }
}

defineExpose({ open })
</script>

<template>
  <!-- 抽屉 -->`
  <el-drawer
    v-model="visibleDrawer"
    :title="formModel.id ? '编辑文章' : '添加文章'"
    direction="rtl"
    size="35%"
  >
    <!-- 发表文章表单 -->
    <el-form ref="formRef" :model="formModel" label-width="100px">
      <el-form-item label="文章标题" prop="title">
        <el-input v-model="formModel.title" placeholder="请输入标题"></el-input>
      </el-form-item>
      <el-form-item label="文章分类" prop="cate_id">
        <channel-select v-model="formModel.cate_id" width="100%" />
      </el-form-item>
      <el-form-item label="文章封面" prop="cover_img">文件上传</el-form-item>
      <el-form-item label="文章内容" prop="content">
        <div class="editor">富文本编辑器</div>
      </el-form-item>
      <el-form-item>
        <el-button type="primary">发布</el-button>
        <el-button type="info">草稿</el-button>
      </el-form-item>
    </el-form>
  </el-drawer>
</template>

<style lang="scss" scoped></style>

article/ArticleMange.vue

<script setup>
const articleEditRef = ref() // 抽屉
// 新增文章
const onAddArticle = () => {
  articleEditRef.value.open()
}
// 编辑文章
const onEditArticle = (row) => {
  articleEditRef.value.open(row)
}
</script>

<template>
  ...
  <!-- 抽屉 -->
  <article-edit ref="articleEditRef"></article-edit>
</template>

8、文件上传

article/components/ArticleEdit.vue

<script setup>
import { Plus } from '@element-plus/icons-vue'
....
// 文件上传
const imgUrl = ref('')
const onUploadFile = (uploadFile) => {
  // 外观
  imgUrl.value = URL.createObjectURL(uploadFile.raw)
  // 实际赋值位置
  formModel.value.cover_img = uploadFile.raw
}
</script>

<template>
  ....
  <el-upload
    class="avatar-uploader"
    :auto-upload="false"
    :show-file-list="false"
    :on-change="onUploadFile"
  >
    <img v-if="imgUrl" :src="imgUrl" class="avatar" />
    <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
  </el-upload>
  ....
</template>

<style lang="scss" scoped>
.avatar-uploader {
  :deep() {
    .avatar {
      width: 178px;
      height: 178px;
      display: block;
    }
    .el-upload {
      border: 1px dashed var(--el-border-color);
      border-radius: 6px;
      cursor: pointer;
      position: relative;
      overflow: hidden;
      transition: var(--el-transition-duration-fast);
    }
    .el-upload:hover {
      border-color: var(--el-color-primary);
    }
    .el-icon.avatar-uploader-icon {
      font-size: 28px;
      color: #8c939d;
      width: 178px;
      height: 178px;
      text-align: center;
    }
  }
}
</style>

9、富文本编辑器 vue-quill

pnpm add @vueup/vue-quill@latest

article/components/ArticleEdit.vue

<script setup>
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
....
</script>

<template>
        ....
        <div class="editor">
          <quill-editor
            theme="snow"
            v-model:content="formModel.content"
            contentType="html"
          />
        </div>
</template>

<style lang="scss" scoped>
....
.editor {
  width: 100%;
  :deep(.ql-editor) {
    min-height: 200px;
  }
}
</style>

10、添加/编辑文章

api/article.js

// 添加文章
export const artPublishService = (data) => request.post('/my/article/add', data)

// 获取文章详情
export const artGetDetailService = (id) =>
  request.get('my/article/info', { params: { id } })

// 编辑文章
export const artEditService = (data) => request.put('my/article/info', data)

article/ArticleManage.vue

<script setup>
// 添加/修改 成功后的操作
const onSuccess = (type) => {
  if (type === 'add') {
    // 添加完毕,更新跳转到渲染最后一页;编辑完毕,渲染当前页
    const lastPage = Math.ceil((total.value + 1) / params.value.pagesize)
    params.value.pagenum = lastPage
  }
  getArticleList()
}
</script>

<template>
  <article-edit ref="articleEditRef" @success="onSuccess"></article-edit>
</template>

article/ArticleEdit.vue

<script setup>
import {
  artPublishService,
  artGetDetailService,
  artEditService
} from '@/api/article'
import { ElMessage } from 'element-plus'
import { baseURL } from '@/utils/request'
import axios from 'axios'
import { nextTick } from 'vue'

const editorRef = ref()
const open = async (row) => {
  visibleDrawer.value = true
  if (row?.id) {
    const res = await artGetDetailService(row.id)
    formModel.value = res.data.data
    imgUrl.value = baseURL + formModel.value.cover_img
    // 将 在线地址 转换成 file 格式
    formModel.value.cover_img = await imageUrlToFile(
      imgUrl.value,
      formModel.value.cover_img
    )
  } else {
    formModel.value = { ...defaultForm.value }
    imgUrl.value = ''
    nextTick(() => {
      editorRef.value.setHTML('')
    })
  }
}
// 将网络图片地址转换为File对象
async function imageUrlToFile(url, fileName) {
  try {
    // 第一步:使用axios获取网络图片数据
    const response = await axios.get(url, { responseType: 'arraybuffer' })
    const imageData = response.data

    // 第二步:将图片数据转换为Blob对象
    const blob = new Blob([imageData], {
      type: response.headers['content-type']
    })

    // 第三步:创建一个新的File对象
    const file = new File([blob], fileName, { type: blob.type })

    return file
  } catch (error) {
    console.error('将图片转换为File对象时发生错误:', error)
    throw error
  }
}

// 添加/修改 成功后的操作
const emit = defineEmits(['success'])
// 发布/草稿
const onPublish = async (state) => {
  formModel.value.state = state

  // 因为参数带有文件上传,所以要转换为 FormData 格式
  const fd = new FormData()
  for (let key in formModel.value) {
    fd.append(key, formModel.value[key])
  }

  if (formModel.value.id) {
    await artEditService(fd)
    ElMessage.success('编辑成功')
    visibleDrawer.value = false
    emit('success', 'edit')
  } else {
    await artPublishService(fd)
    ElMessage.success('添加成功')
    visibleDrawer.value = false
    emit('success', 'add')
  }
}
</script>

<template>
	<quill-editor ref="editorRef" />
	<el-button @click="onPublish('已发布')" type="primary">发布</el-button>
	<el-button @click="onPublish('草稿')" type="info">草稿</el-button>
</template>

11、删除文章

api/article.js

export const artDelService = (id) =>
  request.delete('my/article/info', { params: { id } })

article/ArticleManage.vue

import { ElMessageBox } from 'element-plus'
import { artDelService } from '@/api/article'

// 删除文章
const onDeleteArticle = async (row) => {
  await ElMessageBox.confirm('你确认删除该文章信息吗?', '温馨提示', {
    type: 'warning',
    confirmButtonText: '确认',
    cancelButtonText: '取消'
  })
  await artDelService(row.id)
  ElMessage.success('删除成功')
  getArticleList()
}

五、个人中心

1、基本资料

api/user.js

// 更新用户基本信息
export const userUpdateInfoService = ({ id, nickname, email }) =>
  request.put('/my/userinfo', { id, nickname, email })

user/UserProfile.vue

<script setup>
import PageContainer from '@/components/PageContainer.vue'
import { ref } from 'vue'
import { userUpdateInfoService } from '@/api/user'
import { useUserStore } from '@/stores'

// pinia 用户数据
const {
  user: { nickname, email, username, id },
  getUser
} = useUserStore()
// 表单数据
const userInfo = ref({
  username,
  nickname,
  email,
  id
})
// 表单校验规则
const rules = {
  nickname: [
    { required: true, message: '请输入用户昵称', trigger: 'blur' },
    {
      pattern: /^\S{2,10}$/,
      message: '昵称必须是2-10位的非空字符',
      trigger: 'blur'
    }
  ],
  email: [
    { required: true, message: '请输入用户邮箱', trigger: 'blur' },
    {
      type: 'email',
      message: '请输入正确的邮箱地址',
      trigger: 'blur'
    }
  ]
}

const formRef = ref()
const onSumbit = async () => {
  const valid = await formRef.value.validate()
  if (valid) {
    await userUpdateInfoService(userInfo.value)
    await getUser()
    ElMessage.success('修改成功')
  }
}
</script>

<template>
  <page-container title="基本资料">
    <el-row>
      <el-col :span="12">
        <el-form
          ref="formRef"
          :model="userInfo"
          :rules="rules"
          label-width="100px"
          size="large"
        >
          <el-form-item label="登录名称">
            <el-input v-model="userInfo.username" disabled></el-input>
          </el-form-item>
          <el-form-item label="用户昵称" prop="nickname">
            <el-input v-model="userInfo.nickname"></el-input>
          </el-form-item>
          <el-form-item label="用户邮箱" prop="email">
            <el-input v-model="userInfo.email"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="onSumbit">提交修改</el-button>
          </el-form-item>
        </el-form>
      </el-col>
    </el-row>
  </page-container>
</template>

2、更换头像

api/user.js

// 上传头像
export const userUploadAvatarService = (avatar) =>
  request.patch('/my/update/avatar', { avatar })

user/UserAvatar.vue

<script setup>
import PageContainer from '@/components/PageContainer.vue'
import { Plus, Upload } from '@element-plus/icons-vue'
import { ref } from 'vue'
import { useUserStore } from '@/stores'
import { userUploadAvatarService } from '@/api/user'

const userStore = useUserStore()

const uploadRef = ref()
const imgUrl = ref(userStore.user.user_pic)

// 选择图片
const onUploadFile = (file) => {
  // 基于 FileReader 读取图片做预览
  const reader = new FileReader()
  reader.readAsDataURL(file.raw)
  reader.onload = () => {
    imgUrl.value = reader.result
  }
}
// 上传图片
const onUpdateAvatar = async () => {
  await userUploadAvatarService(imgUrl.value)
  await userStore.getUser()
  ElMessage.success('更换头像成功')
}
</script>

<template>
  <page-container title="更换头像">
    <el-row>
      <el-col :span="12">
        <el-upload
          ref="uploadRef"
          class="avatar-uploader"
          :auto-upload="false"
          :show-file-list="false"
          :on-change="onUploadFile"
        >
          <img v-if="imgUrl" :src="imgUrl" class="avatar" />
          <img v-else src="@/assets/avatar.jpg" width="278" />
        </el-upload>
        <br />
        <el-button
          @click="uploadRef.$el.querySelector('input').click()"
          type="primary"
          :icon="Plus"
          size="large"
        >
          选择图片
        </el-button>
        <el-button
          type="success"
          :icon="Upload"
          size="large"
          @click="onUpdateAvatar"
        >
          上传头像
        </el-button>
      </el-col>
    </el-row>
  </page-container>
</template>

<style lang="scss" scoped>
.avatar-uploader {
  :deep() {
    .avatar {
      width: 278px;
      height: 278px;
      display: block;
    }
    .el-upload {
      border: 1px dashed var(--el-border-color);
      border-radius: 6px;
      cursor: pointer;
      position: relative;
      overflow: hidden;
      transition: var(--el-transition-duration-fast);
    }
    .el-upload:hover {
      border-color: var(--el-color-primary);
    }
    .el-icon.avatar-uploader-icon {
      font-size: 28px;
      color: #8c939d;
      width: 278px;
      height: 278px;
      text-align: center;
    }
  }
}
</style>

3、重置密码

api/user.js

// 更新密码
export const userUpdatePassService = ({ old_pwd, new_pwd, re_pwd }) =>
  request.patch('/my/updatepwd', { old_pwd, new_pwd, re_pwd })

user/UserPassword.vue

<script setup>
import PageContainer from '@/components/PageContainer.vue'
import { ref } from 'vue'
import { userUpdatePassService } from '@/api/user'
import { useUserStore } from '@/stores'
import { useRouter } from 'vue-router'

const formRef = ref()
const pwdForm = ref({
  old_pwd: '',
  new_pwd: '',
  re_pwd: ''
})
const checkOldSame = (rule, value, callback) => {
  if (value === pwdForm.value.old_pwd) {
    callback(new Error('新密码不能与原密码相同'))
  } else {
    callback()
  }
}
const checkNewSame = (rule, value, callback) => {
  if (value !== pwdForm.value.new_pwd) {
    callback(new Error('确认密码与新密码不一致'))
  } else {
    callback()
  }
}
const rules = {
  old_pwd: [
    { required: true, message: '请输入原密码', trigger: 'blur' },
    {
      pattern: /^\S{6,15}$/,
      message: '密码长度必须是6-15位的非空字符串',
      trigger: 'blur'
    }
  ],
  new_pwd: [
    { required: true, message: '请输入新密码', trigger: 'blur' },
    {
      pattern: /^\S{6,15}$/,
      message: '密码长度必须是6-15位的非空字符串',
      trigger: 'blur'
    },
    { validator: checkOldSame, trigger: 'blur' }
  ],
  re_pwd: [
    { required: true, message: '请再次输入密码', trigger: 'blur' },
    {
      pattern: /^\S{6,15}$/,
      message: '密码长度必须是6-15位的非空字符串',
      trigger: 'blur'
    },
    { validator: checkNewSame, trigger: 'blur' }
  ]
}

// 修改密码
const userStore = useUserStore()
const router = useRouter()
const onSubmit = async () => {
  const valid = await formRef.value.validate()
  if (valid) {
    await userUpdatePassService(pwdForm.value)
    ElMessage.success('更换密码成功')
    // 清空token和user信息,并且返回登录页
    userStore.setToken('')
    userStore.setUser({})
    router.push('/login')
  }
}
// 重置密码
const onReset = () => {
  formRef.value.resetFields()
}
</script>

<template>
  <page-container title="重置密码">
    <el-row>
      <el-col :span="12">
        <el-form
          ref="formRef"
          :model="pwdForm"
          :rules="rules"
          label-width="100px"
          size="large"
        >
          <el-form-item label="原密码" prop="old_pwd">
            <el-input v-model="pwdForm.old_pwd" type="password"></el-input>
          </el-form-item>
          <el-form-item label="新密码" prop="new_pwd">
            <el-input v-model="pwdForm.new_pwd" type="password"></el-input>
          </el-form-item>
          <el-form-item label="确认新密码" prop="re_pwd">
            <el-input v-model="pwdForm.re_pwd" type="password"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="onSubmit">修改密码</el-button>
            <el-button @click="onReset">重置</el-button>
          </el-form-item>
        </el-form>
      </el-col>
    </el-row>
  </page-container>
</template>
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
2014年09月12日 V2.85更新包(推荐升级) 1.[*修复]修复后台文件出现任意下载漏洞【感谢乌云[haitaowuyun]发现】 2.[*修复]修复前台存在的SQL注入风险漏洞【感谢乌云[语邑尘]发现】 3.[*修复]修复前台存在的xss跨站脚本攻击漏洞【感谢乌云[evil]发现】 4.[修复]修复内容页投票区域部分浏览器不会居中BUG 5.加强前台提交表单信息的安全过滤 6.修复几个细节BUG 2014年04月30日 V2.84更新包(推荐升级) 1.修复安装向导配置时对数据库字符过滤不严存在入侵漏洞 2.修复后台模板样式文件字符过滤不严存在入侵漏洞 3.修复后台登录验证没有清空验证码信息,导致增加暴力解密风险 4.修复通过后台文章编辑附件栏自定义内容可能导致删掉网站任意文件的BUG 5.修复后台服务器文件管理文件总大小超过几G会出现页面程序错误BUG 6.完善后台JS复制事件 7.修复其他BUG (1~4更新点感谢白帽:seraph1984,通过乌云平台告知我们网钛。这些入侵漏洞只要有根据程序后台引导页提示对后台文件夹进行重命名以及重命名或删掉安装向导文件夹均不会受影响) 2014年04月10日 V2.83更新包 1.[纠正]纠正后台 会员参数设置 有时自适应高度不对的BUG 2.[纠正]纠正后台如果是用chrome核心浏览器,会造成底部一大截空白的BUG 3.[纠正]纠正部分浏览器后台导航菜单间间隔背景图会重复显示多次BUG 4.[纠正]纠正前台导航下拉二级菜单多次经过会出现重复伸缩问题 5.[纠正]选择服务器文件页面没有滚动条的BUG 2014年03月07日 V2.82更新包 1.[新增]会员投稿管理也加入复制功能 2.[改进]改进后台文章管理 缩略图示意图,图片图标代表本地缩略图,电脑图标代表远程缩略图 3.[纠正]纠正外部调用代码会显示出未审核状态和隐藏状态的文章 4.[纠正]纠正外部调用代码对采用外部链接的文章路径没法显示正确 2013年10月23日 V2.81更新包 1.[新增]文章管理 增加 复制 按钮,免去写形似文章的工作量,直接在现有的修改下即可成新文章 2.[改进]网站参数设置 增加 清除JS内容 按钮 3.修复其他几个小细节 2013年09月07日 V2.80更新包 1.[改进]去掉旧版AJAXRequest换成jQuery中的AJAX 2.[改进]为了系统安全和稳定去掉宇初验证码和印象码,加入4种数字验证码、1种中文验证码、1种计算题验证码供用户选择 3.[改进]为了后台系统安全,后台登录页不用判断是否已登录状态并自动跳转到管理页 4.[改进]后台编辑器CKEditor V3.6.1升级到CKEditor V3.6.6 5.[改进]前台编辑器KindEditor V3.5.5升级到KindEditor V3.5.6 6.[纠正]纠正栏目管理中非顶级分类,次页显示模式选择[分类列表2]没提示不允许选择的提示 7.[纠正]纠正部分浏览器后台列表管理的复选框全选失效的BUG 8.[纠正]纠正部分浏览器后台列表管理的多选处理按钮和下拉菜单失效的BUG 9.[改变]后台兼容模式提升到IE8模式 10.修复其他诸多小细节 2013年07月19日 V2.74更新包 1.[新增]后台引导页加入非IE浏览器提示,后台部分功能在非IE浏览器下可能没法使用 2.[修复]【紧急】纠正后台设皮肤目录存在入侵挂马BUG 3.[修复]纠正服务器时间跟保存到数据库里格式不同造成登录有时出现时间不匹配问题 4.[纠正]生成sitemap 纠正隐藏状态文章和未审核文章也会显示出来的BUG 5.[改变]后台 新增栏目 次页显示模式默认为【图+摘要1】,首页栏目图片文章默认开启 6.[改变]前台网站公告、搜索结果默认显示模式为【图+摘要1】 7.[改变]RSS调用默认调用数量从20改成50 2013年04月15日 V2.73更新包 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去,后台更新下皮肤样式。 1.[改进]文章管理-评论管理 留言内容字符显示50字节扩大到80字节 2.[纠正]纠正后台登陆页密码框只能输入20字符的限制 3.[纠正]纠正栏目编辑时模式选择外部链接,导航菜单和首页栏目开关项没显示的BUG 4.[纠正]纠正sitemap生成把隐藏状态的文章也显示出来的bug 5.全站验证码默认选择传统数字型 6.修复其他小细节 2013年03月18日 V2.72更新包 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去,后台更新下皮肤样式。 1.[修复]由于上传图片管理中的占用数不准确,故去除 2.[改进]前台编辑器字体和背景色盘由原来的十几色改成全色系 3.[改进]完善过滤script标签的JS函数 4.[改进]后台密码设置由原来限制最长20位扩展到50位 5.[纠正]纠正幻灯片下面的广告位和首页栏目广告位宽度少2像素BUG 6.修复其他小细节 2013年03月02日 V2.71更新包 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去。 1.关键字管理的地址栏加入自动换行,防止网址过长造成右侧会没显示 2.列表页导航分页加入超过100分页时,下拉选择自动变成文本框输入,防止下拉项太多而影响速度 3.纠正文章管理因异常栏目信息,而造成页面错误 4.生成的缓存文件加入字符的编码,防止因配置内容含asp代码而造成程序出错 5.读取文件加入对0字节文件的错误判断处理 2013年01月11日 V2.7升级包 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去。 1.纠正后台AJAX函数的加载图路径错误BUG 2.宇初验证码换成印象码 3.纠正后台引导页可能会被安全狗屏蔽的问题 4.自动完善内容页正文里的图片alt和title属性值 5.纠正富媒体验证码浮层层级问题 6.改进后台首页HTML代码 7.程序里“分类”纠正成“栏目” 8.纠正最新消息更多列表没加入最新消息文章属性限制 9.后台评论管理中,文章链接地址改用伪静态路径 10.去掉后台参数设置中,[后台头部界面]和[用户管理菜单]项 11.解决后台编辑器之前空2格低于一个汉字的问题 12.完善几个细节问题 13.整合20121103~20121220补丁 2012年12月20日 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去。 1.富媒体验证码更新到最新版 2.采集项目管理增加创建时间信息 3.上传图片和上传文件框加入编码指定,防止乱码出现 4.修复其他细节bug 2012年11月03日 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去。 1.纠正后台文章编辑时获取编辑器缩略图有时没有显示全的bug 2.纠正数据处理管理页面权限不对bug 3.后台文章管理快捷属性设置,加入 首图/缩图/幻灯/滚图 属性的是否有缩略图文件判断 4.纠正后台皮肤管理 高级模式中 界面框架主体(960px)项没保存到数据库的bug 5.前台内容页关键词替换,加入排除已有超连接或图片标签的内容替换 2012年07月06日 V2.6升级包 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去。(如果之前没打过2012年05月25日补丁,需要在网站上运行admin/update_20120525.asp文件(如网址输入:http://你的域名/后台目录名/update_20120525.asp),升级完成后删掉update_20120525.asp文件;如果打过就跳过该步骤,直接删掉update_20120525.asp文件。) 1.后台 文章参数设置里的初始化设置,加入注释信息 2.后台 采集管理的[教程]链接纠正 3.皮肤管理加入2个皮肤风格下载路径 4.后台 网站参数设置对路径加入注释信息 5.其他细节修复 6.整合2012年03月27日~2012年06月07日补丁 2012年06月07日 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去。 1.[纠正]纠正一些站长工具检索不到网站关键词和网站描述 2.[纠正]纠正注册敏感词如有空值造成无法注册的BUG 3.纠正几处细节BUG 2012年05月25日 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去,覆盖好后,并在网站上运行admin/update_20120525.asp文件(如网址输入:http://你的域名/后台目录名/update_20120525.asp),升级完成后删掉update_20120525.asp文件。 1.后台 网站参数设置 新增验证码模式选择 2.后台 网站参数设置 纠正几处选项名称 3.纠正 关闭全站验证码 会员注册和登录 却还显示的BUG 2012年05月17日 1.纠正20120515补丁造成的页头JS失效BUG 2.纠正20120515补丁造成页头搜索项失效BUG 2012年05月15日 1.纠正后台皮肤管理的生成CSS样式的个别样式问题 2.文章编辑,获取编辑器图片增加bmp格式图片 3.纠正开启IP库时,如果没发现IP库会出现提示信息而不是页面出错 4.完善 字符串长度截取函数 5.改进前台底部友情链接间隔符 6.纠正留言和评论提交成功后会弹出验证码的BUG 7.纠正会员注册计算是否允许再次注册时出现页面错误BUG 8.纠正其他几个小细节 2012年04月17日 1.后台评论管理和留言管理内容加入强制换行,防止有时会撑爆页面 2.后台评论管理和留言管理新增允许修改用户留言/评论内容 2012年03月31日 1.[*紧急修复]修复次页存在的一个xss攻击安全漏洞 2.纠正当新的验证码无法使用时系统自动切换到传统数字验证码 3.其他几个小细节纠正 2012年03月27日 1.后台每页显示个数增加最大200限制 2.网站验证码改为富媒体验证码,更难以被识别和破解 3.其他几个小细节修复和完善 2012年03月21日 V2.5升级包 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去。 1.[新增]前台会员投稿和后台文章编辑 新增重复性标题检查按钮和本地获取关键词 2.[新增]新增本地关键词库(位置:inc/keyWord.txt),可以根据自己需求往里面编辑需要的关键词 3.[新增]前台 文章管理 新增查询栏 4.[新增]后台 常规设置-底部栏目 新增链接 留言板 模式 5.[改进]后台 文章管理-栏目管理 文字和细节完善 6.[改进]后台 留言管理和评论管理 内容加入强行换行,防止个别情况撑爆界面 7.[改进]后台 友情链接管理 纠正新增框初始排序值有时非最大值问题 8.[改进]完善前台编辑器细节 9.[修复]后台 会员管理 注册日期查询是失效的BUG 10.[修复]修复留言本页面 会员注册/登录失效问题 11.其他细节修复 2012年02月29日 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去。 1.[修复]后台文章管理-栏目管理 二级栏目模式显示错误的BUG 2.[修复]前台页头导航栏二级栏目模式为单篇页时,链接无效BUG 2012年02月10日 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去。 1.[新增]后台新增文章时作者栏 加入{%称呼%}自动替换为当前后台用户昵称 2.[改进]后台分页数太多,提交成功后要提示下 3.[新增]后台引导页加入生成首页静态页判断 4.[改进]后台留言管理,对留言内容和用户名加入字符串强制换行 5.[修复]纠正前台投票时,当投票数超过32767时,程序会出错的BUG 2012年01月18日 bug修复 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去,覆盖好后,并在网站上运行admin/update_20120119.asp文件(如网址输入:http://你的域名/后台目录名/update_20120119.asp),升级完成后删掉update_20120119.asp文件。 1.修复“最新信息”列表页会显示出 没有设置“最新信息”属性的文章BUG 2.修复文章管理,状态查询失效BUG 3.栏目编辑 新增 外部链接/单页面 也可设置打开方式 4.内容页正文底部广告移到正文分页下面 5.新增编号22广告位,位于内容页正文分页上面 6.更换“分享到代码”为 百度分享,地址改成百度分享,加入默认内置代码重置,内容页分享代码移到正文底部 7.搜索栏新增京东商城搜索 2012年01月05日 V2.4升级包 1.新增【管理员专区】->【数据处理管理】功能,可以批量替换数据和处理因敏感词而无法正常打开网页问题 2.纠正后台文章编辑,获取编辑器图片无法获取网络图片路径问题 3.后台文章管理,标题后面加入是否含缩略图的图标 4.纠正后台文章编辑,保存远程图片到本地,如果缩略图是网络图片没纠正成本地路径的BUG 5.删除网站参数设置里无用字段信息 6.后台友情链接新增 链接注释信息 栏 7.后台栏目编辑和单篇页编辑,加入模式选择(栏目、外部链接、单篇页) 8.更新免费版安装向导程序至V2.4,加入版本判断和数据库初始化功能 9.节日倒计时改为写文件方式保存缓存文件,以便无法访问外部的空间也可以正常使用该功能 10.整合12.4日补丁 2011年12月4日 bug修复 1.改进采集栏目中对过滤标签进行注释 2.采集过程中,加入 采集标题 去HTML化处理 3.改进保存远程图片和采集图片逻辑 4.几个小细节纠正 2011年11月24日 V2.3升级包 1.MSXML2.XMLHTTP更换成MSXML2.ServerXMLHTTP,防止采集或生成首页静态页会卡死问题 2.纠正采集的图片标签中有js脚本时,可能会造成图片获取不到的BUG 3.友情链接管理,到期日期加入快捷的1、3、6、12个月的选择按钮 4.sitemap加入最后更新时间 5.对首页最新评论和最新留言中连续数字或者连续字母进行强制换行,防止内容被隐藏 6.解除对p、img、input、button等标签默认空隙的限制 7.屏蔽掉网站数据库和采集数据库的数据库还原功能 8.广告管理中广告编辑,加入上传本地图片到编辑器中功能 9.纠正内容页图文排版时,后台设置图片间距无效bug 10.纠正后台采集结果预览,列表小图为绝对路径时没显示出来的BUG 11.整合5.14~11.4的更新包 2011年11月4日 BUG修复 1.修复11.1补丁造成的文章内容摘要过滤HTML失效而可能影响到前台排版问题【重要】 2011年11月1日 BUG修复+细节完善 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去;进入后台皮肤管理,点击[更新全部] 1.后台新增用户和修改用户名加入特殊符号过滤 2.纠正前台留言和评论,如果内容是连续数字或者英文可能会出现不能自动换行的BUG 3.首页logo加入首页超连接 4.纠正蒙层窗口分页导航含中文会错误的BUG 5.后台文章编辑,内容摘要编码化,防止破坏前台HTML代码 6.其他小细节修复 2011年10月13日 BUG修复+细节完善 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去 1.文章管理,标题编码化,防止因为含有HTML代码而排版错乱 2.后台有生成缓存文件的地方加入是否生成成功的判断 3.前台搜索 Google 搜索链接错误 4.减少打开蒙层窗口 要点击2次才会打开的机率 5.文章内容,超标图片下面新增[点击查看原图]超链接 2011年9月30日 BUG修复+细节完善 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去 1.广告管理加入【广告位具体位置说明】链接 2.提高导航下拉二级菜单的浮动层级,让其尽可能不会被其他元素覆盖了 3.纠正首页最新消息置顶文章和首页栏目more链接,当为外部链接时没采用外部链接bug 4.去掉生成首页静态页失败时用跳转语句替代 5.增加后台生成首页静态页错误的原因提示种类 2011年9月25日 功能增加+BUG修复+细节完善 ★更新方式:补丁包里的所有文件覆盖进去 1.网站地图条数最大限制增至100万 2.去掉获取网站自身网址时加入端口的判断 3.采集项目修改,预览采集目标地址列加编号 4.纠正会员投稿保存时,出现js代码文字 5.采集入库相关文章和评论默认开启 2011年9月12日 BUG修复 ★更新方式:补丁包里的所有文件覆盖进去 1.纠正前台编辑器明明有内容却提示没内容的BUG 2.纠正会员注册时有时没判断用户名的唯一性 3.纠正页头网站LOGO与右侧广告位错位3个像素BUG 4.针对百度SEO建议程序优化 (另:针对百度SEO优化请检查后台 网站参数设置-》网站基本信息-内容页“分享到”代码,如存在img标签并且不存在alt属性,请加上alt属性,不然会被扣‘图片Alt信息’分数,如“”改成“”) 2011年9月5日 细节完善+BUG修复 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去 1.纠正后台栏目管理,修改栏目所属,文章的分类没有相应纠正过来的BUG 2.纠正采集历史记录结果查询条件失效的BUG 3.前台编辑器更新到最新版KindEditor V3.5.5 4.后台编辑器更新到最新版ckeditor V3.6.1 2011年8月8日 细节完善+BUG修复 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去;进入后台皮肤管理,点击[更新全部] 1.上传文件名超长时,自动截断 2.纠正网速慢造成出现“stack overflow at line: 0”警示框的错误提示 3.纠正火狐导航下拉菜单只能显示1个子栏目BUG 4.纠正内容页的顶踩进度条在IE6下高度超出BUG 5.纠正114啦天气预报出错问题 6.几个小细节修复 2011年7月22日 细节完善+BUG修复 1.纠正广告管理,点查看代码,跳到页头BUG 2.纠正采集模块为隐藏状态时,新增/修改采集项目还会显示的BUG 3.后台 修改密码,新增其他信息设置菜单,加入列表页每页显示数量设置 4.完善新增栏目后会继承上个添加的部分信息 5.纠正2个小细节 2011年7月8日 细节完善+BUG修复 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去;进入后台皮肤管理,点击[更新全部] 1.纠正蒙层窗口关闭时,有时会造成部分页面元素被隐藏而没显示出来BUG 2.图片生成设置 加入是否aspJpeg组件是否支持的判断 3.后台引导页对网钛信息加入对空间访问官网的速度检测,如速度过慢,不自动获取官网信息 4.纠正列表页右侧分类导航IE浏览器右侧边线不见的BUG 5.纠正页面几个HTML标签错误 6.广告管理,加入生成缓存文件是否成功的提示信息 7.纠正导航菜单长度超过会折到第二行的BUG。 8.修复几个小细节 2011年6月16日 细节完善+BUG修复 ★更新方式:admin文件夹改成你后台目录名,然后补丁包里的所有文件覆盖进去。 1.纠正IE6下评论和留言页面的分页下拉框掩盖住蒙层窗口bug 2.加强注册信息保存时的过滤 3.后台 文章评论和留言新增显示游客IP信息 4.纠正前台会员个别用户名出现登录上去了,却提示还未登录的BUG 5.改进广告管理 6.修复几个小细节 2011年6月3日 细节完善+BUG修复 1.外部调用,文章属性由且关系,改成用或关系 2.文章列表页增加【标题】(只有标题)、【分类列表2】(即一行显示2个类别)2种模式 3.文章 缩略图/图片 新增支持外部图片链接(http://开头的) 4.纠正皮肤管理中,更改模板样式文件名失效的问题 5.评论和留言,默认留言者改为“游客” 6.纠正后台引导页中会员待审核文章统计错误BUG 7.纠正后台文章管理(会员投稿)审核状态查询条件失效BUG 8.完善小细节 2011年5月27日 1.改进首页静态页每日自动生成的逻辑。 2.纠正单页面图片路径不是采用相对路径问题 3.纠正几个小细节 2011年5月20日 1.纠正RSS订阅,栏目有时显示不全问题 2.纠正会员模块小细节bug 3.纠正后台外部调用页,栏目有时显示不全问题 4.优化几个小细节代码 2011年5月14日 ★更新方式:直接覆盖。 1.纠正标签列表页分页错误 2.纠正友情链接采用远程图片时,内页显示不出来问题 3.纠正内容页需要会员登录状态时,内容没显示出来问题 4.纠正伪静态时列表页和内容页,标签超连接错误问题 5.纠正几个小细节 2011年5月10日 OTCMS V2.2 升级包 ★更新方式:admin文件夹改成你后台目录名,然后直接覆盖,覆盖好后,并在网站上运行admin/update_5.8.asp文件(如网址输入:http://你的域名/后台目录名/update_5.8.asp),升级完成后删掉update_5.8.asp文件。 1.新增RSS功能 2.栏目标题改为:栏目名称+标题附加内容+网站名称 3.列表页新增标签信息 4.内容页新增标签信息和相关文章 5.新增功能外部调用代码 6.新增文章参数设置 7.多处小细节修复和完善 8.集成4月10日~4月26日的补丁 2011年4月26日 ★更新方式:admin文件夹改成你后台目录名,然后直接覆盖。 1.纠正后台设置项当为“-”或“.”造成前台读取文件错误问题 2.纠正后台皮肤管理权限ID匹配不正确问题 3.纠正伪静态时,导航菜单使用单页面是链接错误 4.纠正几个小细节 2011年4月20日 ★更新方式:admin文件夹改成你后台目录名,然后直接覆盖。 1.纠正个别滚动信息没显示出来的BUG 2.纠正伪静态内容页,评论数没递增Bug 3.后台索引页,授权信息获取方式修改 2011年4月12日 1.纠正V2.1前台会员投稿,栏目没关联上的问题 2.纠正单篇内容页页面错误打不开问题 3.纠正会员投稿点击提交一直卡在加载处理中状态 4.纠正V2.1前台会员投稿,栏目没关联上的问题 5.纠正V2.1后台采集栏目每关联上的问题 6.纠正几个其他小细节 2011年4月11日 1.纠正个别用户二级列表页显示不出内容的问题 2011年4月10日 ★更新方式:admin文件夹改成你后台目录名,然后直接覆盖。 1.纠正升级到V2.1后单篇内容页访问错误问题 2.纠正新增的114啦天气预报在伪静态列表页/内容页中找不到文件问题 3.纠正文章管理类别批量移动到更新到V2.1模式 2011年4月9日 OTCMS V2.1 升级包 ★更新方式:admin文件夹改成你后台目录名,然后直接覆盖,覆盖好后,并在网站上运行admin/update_4.1.asp文件(如网址输入:http://你的域名/后台目录名/update_4.1.asp),升级完成后删掉update_4.1.asp文件。 1.重点对文章部分优化程序和数据库 2.获取网址信息加入端口判断,防止不是用默认端口80,会造成页面访问错误问题 3.纠正留言和评论通过非正规渠道提交而绕过字数限制BUG,及加强恶意灌水的防范措施。 4.纠正后台上传图片,选择swf时会提示图片格式无效问题。 5.去除网站参数中最新消息范围选取,改为新增文章属性最新消息 6.文章属性新增“首页缩略图”,首页栏目显示的图片文章于缩略图无关,于此属性有关 7.留言和评论加入楼层字段 8.会员发表文章采用新的编辑器kindeditor,并加入分页设置功能 9.纠正文字水印当为英文时定位位置不准确问题 10.纠正编辑器从word、excel里黏贴进来的样式丢失问题 11.新增对ok3w/老Y迁移程序迁移过来的会员登录支持 12.皮肤管理新增[全部更新]按钮,一次性更新所有皮肤css样式文件 13.新增页头日期 年月日星期、年月日时分秒星期 2种模式选择 14.新增页头天气预报 114啦天气预报 今天、今天明天 2种模式选择 15.新增滚动信息显示条数控制 16.新增搜索结果、网站公告列表页、最新消息列表页的每页条数控制 17.新增首页最新留言、最新评论的标题名称 18.新增评论、留言回复称呼和回复颜色自定义 19.新增生成sitemaps功能 20.新增会员重复注册时间间隔设置,及注册IP加入黑名单 21.新增IP记录管理管理注册间隔时间和黑名单的IP信息 22.其他细节调整和优化 23.整合2.2日~3.2日的所有补丁 2011年3月2日 1.自动过滤掉内容页中正文的clear样式,已防止部分采集的内容被隐藏 2.纠正26日补丁造成的后台获取关键词失效问题 3.纠正后台皮肤管理出现JS错误问题 4.后台采集入库新增过滤内容摘要中的分页符 5.去掉页头JS时间时分秒及时间自动刷新 6.去掉最新消息及列表页的内容摘要链接 7.加入文章仅限会员阅读,而不需限制积分。 8.纠正评论、会员的文章管理分页失效问题。 9.优化部分代码 ★更新方式:直接覆盖,admin文件夹改成你后台目录名,覆盖好后,进入后台的皮肤管理【刷新】下当前使用的皮肤css文件(该补丁有改动皮肤样式文件,故需重新生成下)。 2011年2月26日 1.纠正之前漏加入的国外空间运行出错问题 2.会员退出登录后返回到当前页面,而不是首页 3.会员投稿提交后如为即时响应会有等待10秒设置,防止个别网站速度有所延时,造成会员重复提交问题。 4.首页最新留言去掉经过变粗,首页最新评论经过改成变下划线 5.纠正非动态路径时,站内搜索结果页,第二页开始页面错误问题 6.改善后台授权人信息栏代码 ★更新方式:直接覆盖,admin文件夹改成你后台目录名。 2011年2月23日 紧急补丁 1.【紧急】纠正22日补丁造成的连续新增文章而变成修改上一篇新增文章的问题。 2.首页栏目h1标签改成h2标签 3.首页栏目标题加入超连接 ★更新方式:直接覆盖,admin文件夹改成你后台目录名,覆盖好后,进入后台的皮肤管理【刷新】下当前使用的皮肤css文件。 2011年2月22日 1.纠正15日补丁造成的首页全3栏第一栏会掉下来的问题。【需要在后台对当前皮肤修改保存下以更新皮肤样式】 2.纠正文章内容页有些超连接文字字号偏小问题. 3.扩大导航菜单个数的选择,新增3~8 4.后台新增文章后跳到新增文章页时会自动内置些之前添加的信息 5.后台文章管理里新增“来源”“作者”查询条件 6.后台皮肤管理,新增[更新css]按钮 7.纠正前台搜索结果、网站公告列表、留言板右侧精彩推荐、点击排行没记录问题 2011年2月15日 1.纠正伪静态时,底部栏目链接错误问题 2.去掉底部栏目“请您留言”菜单 3.纠正首页中间模块部分向右偏移2像素问题【需要在后台对当前皮肤修改保存下以更新皮肤样式】 4.纠正伪静态是,当前位置的首页链接错误问题 5.在首页打开蒙层窗口时,暂停滚动信息和滚动图片的滚动,以消除光标闪烁频繁问题 6.会员等级里“管理员”等级暂时没用到,暂时屏蔽掉 7.纠正后台新增会员无法修改密码的错误 2011年2月8日 1.纠正后台皮肤管理,专业模式,保存出错问题 2.去除皮肤模板中,重复定义样式 3.纠正后台文章新增/修改结束,2秒倒计时返回有时会失效问题。 4.纠正伪静态路径文章内容页附件下载错误问题 5.新增皮肤导入/导出 6.纠正后台默认页网站统计信息,一周内/一个月内范围选取错误问题。 7.纠正后台编辑器对复制于word、excel文档的内容进行过滤格式问题 8.纠正伪静态时前台需用户登录才可查看的文章图片路径错误,及附件没显示出来问题(感谢 ♂嫼脃暒涳♀ 发现该问题) 2011年2月2日 1.纠正前台和后台获取关键字乱码问题 2.纠正内容页[上一篇][下一篇]没把隐藏状态的文章排除的问题 3.纠正注册时,个别注册成功而没有提示成功及刷新的问题 4.后台默认页新增网站统计信息,及安全性提示。 5.后台栏目管理加入栏目所属不能选择自己的判断,及新增检测有没有被遗漏的栏目 2011年1月31日 发布OTCMS V2.0 正式版

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

升崽不会飞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值