提交描述

This commit is contained in:
2024-01-17 07:46:08 +08:00
parent 9cd49595db
commit 53b678f2ec
32 changed files with 1410 additions and 414 deletions

View File

@ -1 +1,3 @@
VITE_API_BASE_URL= '' VITE_API_URL= ''

View File

@ -1,7 +1,7 @@
import { mergeConfig } from 'vite'; import { mergeConfig } from 'vite';
import eslint from 'vite-plugin-eslint'; import eslint from 'vite-plugin-eslint';
import * as path from 'path';
import baseConfig from './vite.config.base'; import baseConfig from './vite.config.base';
import * as path from "path";
export default mergeConfig( export default mergeConfig(
{ {
@ -11,11 +11,13 @@ export default mergeConfig(
fs: { fs: {
strict: true, strict: true,
}, },
proxy:{ proxy: {
'/api': { '/api': {
target: 'http://localhost:8080', target: 'http://59.110.238.182:8081',
// target: 'http://4.246.149.244:8081',
// target: 'http://localhost:5173',
changeOrigin: true, changeOrigin: true,
} },
}, },
}, },
plugins: [ plugins: [

View File

@ -1,13 +1,17 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" /> <head>
<link rel="shortcut icon" type="image/x-icon" href="https://unpkg.byted-static.com/latest/byted/arco-config/assets/favicon.ico"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="shortcut icon" type="image/x-icon"
<title>Arco Design Pro - 开箱即用的中台前端/设计解决方案</title> href="https://unpkg.byted-static.com/latest/byted/arco-config/assets/favicon.ico">
</head> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<body> <title>学院学习平台</title>
<div id="app"></div> </head>
<script type="module" src="/src/main.ts"></script>
</body> <body>
</html> <div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

195
npminstall-debug.log Normal file
View File

@ -0,0 +1,195 @@
{
root: 'C:\\Users\\29602\\Desktop\\study\\study-fontend',
registry: 'https://registry.npmmirror.com',
pkgs: [],
production: false,
cacheStrict: false,
cacheDir: 'C:\\Users\\29602\\.npminstall_tarball',
env: {
npm_config_registry: 'https://registry.npmmirror.com',
npm_config_argv: '{"remain":[],"cooked":["--fix-bug-versions","--china","--userconfig=C:\\\\Users\\\\29602\\\\.cnpmrc","--disturl=https://cdn.npmmirror.com/binaries/node","--registry=https://registry.npmmirror.com"],"original":["--fix-bug-versions","--china","--userconfig=C:\\\\Users\\\\29602\\\\.cnpmrc","--disturl=https://cdn.npmmirror.com/binaries/node","--registry=https://registry.npmmirror.com"]}',
npm_config_user_agent: 'npminstall/7.11.1 npm/? node/v18.19.0 win32 x64',
npm_config_cache: 'C:\\Users\\29602\\.npminstall_tarball',
NODE: 'C:\\Program Files\\nodejs\\node.exe',
npm_node_execpath: 'C:\\Program Files\\nodejs\\node.exe',
npm_execpath: 'C:\\Users\\29602\\AppData\\Roaming\\npm\\node_modules\\cnpm\\node_modules\\npminstall\\bin\\install.js',
npm_config_userconfig: 'C:\\Users\\29602\\.cnpmrc',
npm_config_disturl: 'https://cdn.npmmirror.com/binaries/node',
npm_config_r: 'https://registry.npmmirror.com',
COREPACK_NPM_REGISTRY: 'https://registry.npmmirror.com',
NODEJS_ORG_MIRROR: 'https://cdn.npmmirror.com/binaries/node',
NVM_NODEJS_ORG_MIRROR: 'https://cdn.npmmirror.com/binaries/node',
PHANTOMJS_CDNURL: 'https://cdn.npmmirror.com/binaries/phantomjs',
CHROMEDRIVER_CDNURL: 'https://cdn.npmmirror.com/binaries/chromedriver',
OPERADRIVER_CDNURL: 'https://cdn.npmmirror.com/binaries/operadriver',
CYPRESS_DOWNLOAD_PATH_TEMPLATE: 'https://cdn.npmmirror.com/binaries/cypress/${version}/${platform}-${arch}/cypress.zip',
ELECTRON_MIRROR: 'https://cdn.npmmirror.com/binaries/electron/',
ELECTRON_BUILDER_BINARIES_MIRROR: 'https://cdn.npmmirror.com/binaries/electron-builder-binaries/',
SASS_BINARY_SITE: 'https://cdn.npmmirror.com/binaries/node-sass',
SWC_BINARY_SITE: 'https://cdn.npmmirror.com/binaries/node-swc',
NWJS_URLBASE: 'https://cdn.npmmirror.com/binaries/nwjs/v',
PUPPETEER_DOWNLOAD_HOST: 'https://cdn.npmmirror.com/binaries/chrome-for-testing',
PUPPETEER_DOWNLOAD_BASE_URL: 'https://cdn.npmmirror.com/binaries/chrome-for-testing',
PLAYWRIGHT_DOWNLOAD_HOST: 'https://cdn.npmmirror.com/binaries/playwright',
SENTRYCLI_CDNURL: 'https://cdn.npmmirror.com/binaries/sentry-cli',
SAUCECTL_INSTALL_BINARY_MIRROR: 'https://cdn.npmmirror.com/binaries/saucectl',
RE2_DOWNLOAD_MIRROR: 'https://cdn.npmmirror.com/binaries/node-re2',
RE2_DOWNLOAD_SKIP_PATH: 'true',
PRISMA_ENGINES_MIRROR: 'https://cdn.npmmirror.com/binaries/prisma',
npm_config_better_sqlite3_binary_host: 'https://cdn.npmmirror.com/binaries/better-sqlite3',
npm_config_keytar_binary_host: 'https://cdn.npmmirror.com/binaries/keytar',
npm_config_sharp_binary_host: 'https://cdn.npmmirror.com/binaries/sharp',
npm_config_sharp_libvips_binary_host: 'https://cdn.npmmirror.com/binaries/sharp-libvips',
npm_config_robotjs_binary_host: 'https://cdn.npmmirror.com/binaries/robotjs',
npm_rootpath: 'C:\\Users\\29602\\Desktop\\study\\study-fontend',
INIT_CWD: 'C:\\Users\\29602\\Desktop\\study\\study-fontend'
},
binaryMirrors: {
ENVS: {
COREPACK_NPM_REGISTRY: 'https://registry.npmmirror.com',
NODEJS_ORG_MIRROR: 'https://cdn.npmmirror.com/binaries/node',
NVM_NODEJS_ORG_MIRROR: 'https://cdn.npmmirror.com/binaries/node',
PHANTOMJS_CDNURL: 'https://cdn.npmmirror.com/binaries/phantomjs',
CHROMEDRIVER_CDNURL: 'https://cdn.npmmirror.com/binaries/chromedriver',
OPERADRIVER_CDNURL: 'https://cdn.npmmirror.com/binaries/operadriver',
CYPRESS_DOWNLOAD_PATH_TEMPLATE: 'https://cdn.npmmirror.com/binaries/cypress/${version}/${platform}-${arch}/cypress.zip',
ELECTRON_MIRROR: 'https://cdn.npmmirror.com/binaries/electron/',
ELECTRON_BUILDER_BINARIES_MIRROR: 'https://cdn.npmmirror.com/binaries/electron-builder-binaries/',
SASS_BINARY_SITE: 'https://cdn.npmmirror.com/binaries/node-sass',
SWC_BINARY_SITE: 'https://cdn.npmmirror.com/binaries/node-swc',
NWJS_URLBASE: 'https://cdn.npmmirror.com/binaries/nwjs/v',
PUPPETEER_DOWNLOAD_HOST: 'https://cdn.npmmirror.com/binaries/chrome-for-testing',
PUPPETEER_DOWNLOAD_BASE_URL: 'https://cdn.npmmirror.com/binaries/chrome-for-testing',
PLAYWRIGHT_DOWNLOAD_HOST: 'https://cdn.npmmirror.com/binaries/playwright',
SENTRYCLI_CDNURL: 'https://cdn.npmmirror.com/binaries/sentry-cli',
SAUCECTL_INSTALL_BINARY_MIRROR: 'https://cdn.npmmirror.com/binaries/saucectl',
RE2_DOWNLOAD_MIRROR: 'https://cdn.npmmirror.com/binaries/node-re2',
RE2_DOWNLOAD_SKIP_PATH: 'true',
PRISMA_ENGINES_MIRROR: 'https://cdn.npmmirror.com/binaries/prisma',
npm_config_better_sqlite3_binary_host: 'https://cdn.npmmirror.com/binaries/better-sqlite3',
npm_config_keytar_binary_host: 'https://cdn.npmmirror.com/binaries/keytar',
npm_config_sharp_binary_host: 'https://cdn.npmmirror.com/binaries/sharp',
npm_config_sharp_libvips_binary_host: 'https://cdn.npmmirror.com/binaries/sharp-libvips',
npm_config_robotjs_binary_host: 'https://cdn.npmmirror.com/binaries/robotjs'
},
'@ali/s2': { host: 'https://cdn.npmmirror.com/binaries/looksgood-s2' },
sharp: { replaceHostFiles: [Array], replaceHostMap: [Object] },
'@tensorflow/tfjs-node': {
replaceHostFiles: [Array],
replaceHostRegExpMap: [Object],
replaceHostMap: [Object]
},
cypress: {
host: 'https://cdn.npmmirror.com/binaries/cypress',
newPlatforms: [Object]
},
'utf-8-validate': {
host: 'https://cdn.npmmirror.com/binaries/utf-8-validate/v{version}'
},
xprofiler: {
remote_path: './xprofiler/v{version}/',
host: 'https://cdn.npmmirror.com/binaries'
},
leveldown: { host: 'https://cdn.npmmirror.com/binaries/leveldown/v{version}' },
couchbase: { host: 'https://cdn.npmmirror.com/binaries/couchbase/v{version}' },
gl: { host: 'https://cdn.npmmirror.com/binaries/gl/v{version}' },
sqlite3: {
host: 'https://cdn.npmmirror.com/binaries/sqlite3',
remote_path: 'v{version}'
},
'@journeyapps/sqlcipher': { host: 'https://cdn.npmmirror.com/binaries' },
grpc: {
host: 'https://cdn.npmmirror.com/binaries',
remote_path: '{name}/v{version}'
},
'grpc-tools': { host: 'https://cdn.npmmirror.com/binaries' },
wrtc: {
host: 'https://cdn.npmmirror.com/binaries',
remote_path: '{name}/v{version}'
},
fsevents: { host: 'https://cdn.npmmirror.com/binaries/fsevents' },
nodejieba: { host: 'https://cdn.npmmirror.com/binaries/nodejieba' },
canvas: { host: 'https://cdn.npmmirror.com/binaries/canvas' },
'skia-canvas': { host: 'https://cdn.npmmirror.com/binaries/skia-canvas' },
'flow-bin': {
replaceHost: 'https://github.com/facebook/flow/releases/download/v',
host: 'https://cdn.npmmirror.com/binaries/flow/v'
},
'jpegtran-bin': {
replaceHost: [Array],
host: 'https://cdn.npmmirror.com/binaries/jpegtran-bin'
},
'cwebp-bin': {
replaceHost: [Array],
host: 'https://cdn.npmmirror.com/binaries/cwebp-bin'
},
'zopflipng-bin': {
replaceHost: [Array],
host: 'https://cdn.npmmirror.com/binaries/zopflipng-bin'
},
'optipng-bin': {
replaceHost: [Array],
host: 'https://cdn.npmmirror.com/binaries/optipng-bin'
},
mozjpeg: {
replaceHost: [Array],
host: 'https://cdn.npmmirror.com/binaries/mozjpeg-bin'
},
gifsicle: {
replaceHost: [Array],
host: 'https://cdn.npmmirror.com/binaries/gifsicle-bin'
},
'pngquant-bin': {
replaceHost: [Array],
host: 'https://cdn.npmmirror.com/binaries/pngquant-bin',
replaceHostMap: [Object]
},
'pngcrush-bin': {
replaceHost: [Array],
host: 'https://cdn.npmmirror.com/binaries/pngcrush-bin'
},
'jpeg-recompress-bin': {
replaceHost: [Array],
host: 'https://cdn.npmmirror.com/binaries/jpeg-recompress-bin'
},
'advpng-bin': {
replaceHost: [Array],
host: 'https://cdn.npmmirror.com/binaries/advpng-bin'
},
'pngout-bin': {
replaceHost: [Array],
host: 'https://cdn.npmmirror.com/binaries/pngout-bin'
},
'jpegoptim-bin': {
replaceHost: [Array],
host: 'https://cdn.npmmirror.com/binaries/jpegoptim-bin'
},
argon2: { host: 'https://cdn.npmmirror.com/binaries/argon2' },
'ali-zeromq': { host: 'https://cdn.npmmirror.com/binaries/ali-zeromq' },
'ali-usb_ctl': { host: 'https://cdn.npmmirror.com/binaries/ali-usb_ctl' },
'gdal-async': { host: 'https://cdn.npmmirror.com/binaries/node-gdal-async' },
'libpg-query': { host: 'https://cdn.npmmirror.com/binaries' }
},
forbiddenLicenses: null,
flatten: false,
proxy: undefined,
prune: false,
disableFallbackStore: false,
workspacesMap: Map(0) {},
enableWorkspace: false,
workspaceRoot: 'C:\\Users\\29602\\Desktop\\study\\study-fontend',
isWorkspaceRoot: true,
isWorkspacePackage: false,
offline: false,
strictSSL: true,
ignoreScripts: false,
foregroundScripts: false,
ignoreOptionalDependencies: false,
detail: false,
forceLinkLatest: false,
trace: false,
engineStrict: false,
registryOnly: false,
client: false,
autoFixVersion: [Function: autoFixVersion]
}

View File

@ -12,7 +12,11 @@
"preview": "npm run build && vite preview --host", "preview": "npm run build && vite preview --host",
"type:check": "vue-tsc --noEmit --skipLibCheck", "type:check": "vue-tsc --noEmit --skipLibCheck",
"lint-staged": "npx lint-staged", "lint-staged": "npx lint-staged",
"prepare": "husky install" "prepare": "husky install",
"start": "npm run dev"
}, },
"lint-staged": { "lint-staged": {
"*.{js,ts,jsx,tsx}": [ "*.{js,ts,jsx,tsx}": [

View File

@ -21,6 +21,14 @@ axios.interceptors.request.use(
// this example using the JWT token // this example using the JWT token
// Authorization is a custom headers key // Authorization is a custom headers key
// please modify it according to the actual situation // please modify it according to the actual situation
const userStore = useUserStore();
// console.log('config', config);
// if (config.url === '/api/erst/user/me') {
// userStore.headerName = 'zsc';
// userStore.tokenDate = '123';
config.headers['X-CSRF-TOKEN'] = userStore.tokenDate;
// }
const token = getToken(); const token = getToken();
if (token) { if (token) {
if (!config.headers) { if (!config.headers) {
@ -28,6 +36,7 @@ axios.interceptors.request.use(
} }
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;
} }
return config; return config;
}, },
(error) => { (error) => {
@ -70,7 +79,7 @@ axios.interceptors.response.use(
}, },
(error) => { (error) => {
const { response } = error; const { response } = error;
console.log(response); console.log('error', error);
if ( if (
[50008, 50012, 50014].includes(response.data.code) && [50008, 50012, 50014].includes(response.data.code) &&
response.config.url !== '/api/user/info' response.config.url !== '/api/user/info'

View File

@ -8,6 +8,17 @@ export interface RoleCreateRecord {
permissionIds: (number | undefined)[]; permissionIds: (number | undefined)[];
remark: string; remark: string;
} }
export interface ListRecord {
username: string;
phone: string;
email: string;
enable: string;
nickname: string;
avater: string;
address: string;
}
export interface RoleRecord extends RoleCreateRecord { export interface RoleRecord extends RoleCreateRecord {
permissions?: []; permissions?: [];
} }
@ -16,24 +27,35 @@ export interface RoleListRecord extends RoleRecord {
name: string; name: string;
} }
export function create(data: RoleCreateRecord) { export function create(data: RoleCreateRecord) {
return axios.post(`/api/role`, data); return axios.post(`/api/role`, data);
} }
// 更新用户
export function update(data: RoleRecord) { export function update(data: RoleRecord) {
return axios.patch(`/api/role/${data.id}`, data); return axios.patch(`/api/rest/user/${data.id}`, data);
} }
export function getDetail(id: number) { export function getDetail(id: number) {
return axios.get<RoleRecord>(`/api/role/${id}`); return axios.get<RoleRecord>(`/api/role/${id}`);
} }
export function remove(id: number) { // 删除用户
return axios.delete(`/api/role/${id}`); export function remove(id: string) {
return axios.delete(`/api/rest/role/${id}`);
} }
// 模糊查询用户列表
export function queryRoles(params?: ListParams<Partial<RoleRecord>>) { export function queryRoles(params?: ListParams<Partial<RoleRecord>>) {
return queryList<RoleRecord>(`/api/role`, params); return queryList<RoleRecord>(`/api/rest/user/query`, params);
}
// 添加用户
export function addUser(params: ListRecord) {
return axios.post('/api/rest/user/register', params);
}
// 是否启用
export function enabled(id: string) {
return axios.patch(`/api/rest/user/${id}/toggle`);
} }

View File

@ -21,7 +21,8 @@ export interface LoginRes {
} }
export interface PasswordReSetModel { export interface PasswordReSetModel {
newPassword: string; oldPassword: string;
password: string;
confirmPassword: string; confirmPassword: string;
} }
@ -73,16 +74,30 @@ export interface UserListRes {
pageable: Pageable; pageable: Pageable;
} }
export function login(data: LoginData) { // 获取令牌
return axios.post<LoginRes>('/api/user/login', data); export function me() {
return axios.get('/api/rest/user/me');
}
// 用户登录
export function login(data: LoginData, headerName: string, token: string) {
return axios({
url: '/api/rest/user/login',
data,
method: 'post',
headers: {
[headerName]: token,
},
});
} }
export function logout() { export function logout() {
return axios.post<LoginRes>('/api/user/logout'); return axios.post<LoginRes>('/api/rest/user/logout');
} }
// 更新密码
export function resetPassword(data: PasswordReSetModel) { export function resetPassword(data: PasswordReSetModel) {
return axios.patch('/api/user/self/update-password', data); return axios.patch('/api/rest/user/self/update-password', data);
} }
export function create(data: CreateRecord) { export function create(data: CreateRecord) {
@ -94,15 +109,16 @@ export function update(data: UserRecord) {
} }
export function selfUpdate(data: UserState) { export function selfUpdate(data: UserState) {
return axios.patch<Res>(`/api/user/self`, data); return axios.patch<Res>(`/api/rest/user/self`, data);
} }
export function switchRole(roleId: number) { export function switchRole(roleId: number) {
return axios.patch<UserState>(`/api/user/self/switch-role/${roleId}`); return axios.patch<UserState>(`/api/user/self/switch-role/${roleId}`);
} }
// 获取用户信息
export function getUserInfo() { export function getUserInfo() {
return axios.get<UserState>('/api/user/info'); return axios.get<UserState>('/api/rest/user/self');
} }
export function getUserDetail(id: number) { export function getUserDetail(id: number) {
@ -124,4 +140,4 @@ export function queryUserList(params: UserParams) {
return qs.stringify(obj); return qs.stringify(obj);
}, },
}); });
} }

View File

@ -10,7 +10,7 @@
:style="{ margin: 0, fontSize: '18px' }" :style="{ margin: 0, fontSize: '18px' }"
:heading="5" :heading="5"
> >
Arco Pro 中山学院
</a-typography-title> </a-typography-title>
<icon-menu-fold <icon-menu-fold
v-if="!topMenu && appStore.device === 'mobile'" v-if="!topMenu && appStore.device === 'mobile'"
@ -23,15 +23,15 @@
<Menu v-if="topMenu" /> <Menu v-if="topMenu" />
</div> </div>
<ul class="right-side"> <ul class="right-side">
<!-- <li>--> <!-- <li>-->
<!-- <a-tooltip :content="$t('settings.search')">--> <!-- <a-tooltip :content="$t('settings.search')">-->
<!-- <a-button class="nav-btn" type="outline" :shape="'circle'">--> <!-- <a-button class="nav-btn" type="outline" :shape="'circle'">-->
<!-- <template #icon>--> <!-- <template #icon>-->
<!-- <icon-search />--> <!-- <icon-search />-->
<!-- </template>--> <!-- </template>-->
<!-- </a-button>--> <!-- </a-button>-->
<!-- </a-tooltip>--> <!-- </a-tooltip>-->
<!-- </li>--> <!-- </li>-->
<li> <li>
<a-tooltip :content="$t('settings.language')"> <a-tooltip :content="$t('settings.language')">
<a-button <a-button
@ -144,16 +144,7 @@
</a-button> </a-button>
</a-tooltip> </a-tooltip>
</li> </li>
<li>
<a-tooltip content="切换用户角色">
<a-select
v-model="defaultRole"
:style="{ width: '140px' }"
:options="roleOptions as SelectOptionData[]"
@change="switchRoles"
/>
</a-tooltip>
</li>
<li> <li>
<a-dropdown trigger="click"> <a-dropdown trigger="click">
<a-avatar <a-avatar
@ -163,14 +154,14 @@
<img alt="avatar" :src="avatar" /> <img alt="avatar" :src="avatar" />
</a-avatar> </a-avatar>
<template #content> <template #content>
<!-- <a-doption>--> <!-- <a-doption>-->
<!-- <a-space @click="switchRoles">--> <!-- <a-space @click="switchRoles">-->
<!-- <icon-tag />--> <!-- <icon-tag />-->
<!-- <span>--> <!-- <span>-->
<!-- {{ $t('messageBox.switchRoles') }}--> <!-- {{ $t('messageBox.switchRoles') }}-->
<!-- </span>--> <!-- </span>-->
<!-- </a-space>--> <!-- </a-space>-->
<!-- </a-doption>--> <!-- </a-doption>-->
<a-doption> <a-doption>
<a-space @click="$router.push({ name: 'Info' })"> <a-space @click="$router.push({ name: 'Info' })">
<icon-user /> <icon-user />
@ -203,144 +194,144 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, inject } from 'vue'; import { computed, ref, inject } from 'vue';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { useDark, useToggle, useFullscreen } from '@vueuse/core'; import { useDark, useToggle, useFullscreen } from '@vueuse/core';
import { useAppStore, useUserStore } from '@/store'; import { useAppStore, useUserStore } from '@/store';
import { LOCALE_OPTIONS } from '@/locale'; import { LOCALE_OPTIONS } from '@/locale';
import useLocale from '@/hooks/locale'; import useLocale from '@/hooks/locale';
import useUser from '@/hooks/user'; import useUser from '@/hooks/user';
import Menu from '@/components/menu/index.vue'; import Menu from '@/components/menu/index.vue';
import userIcon from '@/assets/images/user-circle.png'; import userIcon from '@/assets/images/user-circle.png';
import { SelectOptionData } from '@arco-design/web-vue/es/select/interface'; import { SelectOptionData } from '@arco-design/web-vue/es/select/interface';
import MessageBox from '../message-box/index.vue'; import MessageBox from '../message-box/index.vue';
const appStore = useAppStore(); const appStore = useAppStore();
const userStore = useUserStore(); const userStore = useUserStore();
const { logout } = useUser(); const { logout } = useUser();
const { changeLocale, currentLocale } = useLocale(); const { changeLocale, currentLocale } = useLocale();
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen(); const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
const locales = [...LOCALE_OPTIONS]; const locales = [...LOCALE_OPTIONS];
const avatar = computed(() => { const avatar = computed(() => {
return userStore.avatar ? userStore.avatar : userIcon; return userStore.avatar ? userStore.avatar : userIcon;
});
const theme = computed(() => {
return appStore.theme;
});
const topMenu = computed(() => appStore.topMenu && appStore.menu);
const isDark = useDark({
selector: 'body',
attribute: 'arco-theme',
valueDark: 'dark',
valueLight: 'light',
storageKey: 'arco-theme',
onChanged(dark: boolean) {
// overridden default behavior
appStore.toggleTheme(dark);
},
});
const toggleTheme = useToggle(isDark);
const handleToggleTheme = () => {
toggleTheme();
};
const setVisible = () => {
appStore.updateSettings({ globalSettings: true });
};
const refBtn = ref();
const triggerBtn = ref();
const defaultRole = ref(userStore.role?.id);
const roleOptions = computed(() => {
return userStore.roles?.map((role) => {
return {
value: role.id,
label: role.name,
};
}); });
const theme = computed(() => { });
return appStore.theme; const setPopoverVisible = () => {
const event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true,
}); });
const topMenu = computed(() => appStore.topMenu && appStore.menu); refBtn.value.dispatchEvent(event);
const isDark = useDark({ };
selector: 'body', const handleLogout = () => {
attribute: 'arco-theme', logout();
valueDark: 'dark', };
valueLight: 'light', const setDropDownVisible = () => {
storageKey: 'arco-theme', const event = new MouseEvent('click', {
onChanged(dark: boolean) { view: window,
// overridden default behavior bubbles: true,
appStore.toggleTheme(dark); cancelable: true,
},
}); });
const toggleTheme = useToggle(isDark); triggerBtn.value.dispatchEvent(event);
const handleToggleTheme = () => { };
toggleTheme(); const switchRoles = async (value: number) => {
}; //
const setVisible = () => { await userStore.switchRoles(value);
appStore.updateSettings({ globalSettings: true }); window.location.reload();
}; // Message.success(res as string);
const refBtn = ref(); };
const triggerBtn = ref(); const toggleDrawerMenu = inject('toggleDrawerMenu') as () => void;
const defaultRole = ref(userStore.role?.id);
const roleOptions = computed(() => {
return userStore.roles?.map((role) => {
return {
value: role.id,
label: role.name,
};
});
});
const setPopoverVisible = () => {
const event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true,
});
refBtn.value.dispatchEvent(event);
};
const handleLogout = () => {
logout();
};
const setDropDownVisible = () => {
const event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true,
});
triggerBtn.value.dispatchEvent(event);
};
const switchRoles = async (value: number) => {
//
await userStore.switchRoles(value);
window.location.reload();
// Message.success(res as string);
};
const toggleDrawerMenu = inject('toggleDrawerMenu') as () => void;
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.navbar { .navbar {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
height: 100%; height: 100%;
background-color: var(--color-bg-2); background-color: var(--color-bg-2);
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
} }
.left-side { .left-side {
display: flex;
align-items: center;
padding-left: 20px;
}
.center-side {
flex: 1;
}
.right-side {
display: flex;
padding-right: 20px;
list-style: none;
:deep(.locale-select) {
border-radius: 20px;
}
li {
display: flex; display: flex;
align-items: center; align-items: center;
padding-left: 20px; padding: 0 10px;
} }
.center-side { a {
flex: 1; color: var(--color-text-1);
text-decoration: none;
} }
.nav-btn {
.right-side { border-color: rgb(var(--gray-2));
display: flex; color: rgb(var(--gray-8));
padding-right: 20px; font-size: 16px;
list-style: none;
:deep(.locale-select) {
border-radius: 20px;
}
li {
display: flex;
align-items: center;
padding: 0 10px;
}
a {
color: var(--color-text-1);
text-decoration: none;
}
.nav-btn {
border-color: rgb(var(--gray-2));
color: rgb(var(--gray-8));
font-size: 16px;
}
.trigger-btn,
.ref-btn {
position: absolute;
bottom: 14px;
}
.trigger-btn {
margin-left: 14px;
}
} }
.trigger-btn,
.ref-btn {
position: absolute;
bottom: 14px;
}
.trigger-btn {
margin-left: 14px;
}
}
</style> </style>
<style lang="less"> <style lang="less">
.message-popover { .message-popover {
.arco-popover-content { .arco-popover-content {
margin-top: 0; margin-top: 0;
}
} }
}
</style> </style>

View File

@ -28,6 +28,7 @@ import localeUserSetting from '@/views/user/setting/locale/en-US';
import systemUser from '@/views/system/user/locale/en-US'; import systemUser from '@/views/system/user/locale/en-US';
import localRole from '@/views/character/manage/locale/zh-CN';
import localeSettings from './en-US/settings'; import localeSettings from './en-US/settings';
export default { export default {
@ -45,6 +46,7 @@ export default {
'menu.arcoWebsite': 'Arco Design', 'menu.arcoWebsite': 'Arco Design',
'menu.faq': 'FAQ', 'menu.faq': 'FAQ',
'menu.system': 'System', 'menu.system': 'System',
'menu.role': 'Role',
'navbar.docs': 'Docs', 'navbar.docs': 'Docs',
'navbar.action.locale': 'Switch to English', 'navbar.action.locale': 'Switch to English',
...localeSettings, ...localeSettings,
@ -67,5 +69,6 @@ export default {
...locale500, ...locale500,
...localeUserInfo, ...localeUserInfo,
...localeUserSetting, ...localeUserSetting,
...localRole,
...systemUser, ...systemUser,
}; };

View File

@ -28,6 +28,7 @@ import localeUserSetting from '@/views/user/setting/locale/zh-CN';
import systemUser from '@/views/system/user/locale/zh-CN'; import systemUser from '@/views/system/user/locale/zh-CN';
import localRole from '@/views/character/manage/locale/zh-CN';
import localeSettings from './zh-CN/settings'; import localeSettings from './zh-CN/settings';
export default { export default {
@ -45,6 +46,7 @@ export default {
'menu.arcoWebsite': 'Arco Design', 'menu.arcoWebsite': 'Arco Design',
'menu.faq': '常见问题', 'menu.faq': '常见问题',
'menu.system': '系统管理', 'menu.system': '系统管理',
'menu.role': '角色中心',
'navbar.docs': '文档中心', 'navbar.docs': '文档中心',
'navbar.action.locale': '切换为中文', 'navbar.action.locale': '切换为中文',
...localeSettings, ...localeSettings,
@ -67,6 +69,7 @@ export default {
...locale500, ...locale500,
...localeUserInfo, ...localeUserInfo,
...localeUserSetting, ...localeUserSetting,
...localRole,
...systemUser, ...systemUser,
}; };

View File

@ -12,7 +12,7 @@ setupMock({
// Mock.XHR.prototype.withCredentials = true; // Mock.XHR.prototype.withCredentials = true;
// 用户信息 // 用户信息
Mock.mock(new RegExp('/api/user/info'), () => { Mock.mock(new RegExp('/api/rest/user/info'), () => {
if (isLogin()) { if (isLogin()) {
const role = window.localStorage.getItem('userRole') || 'admin'; const role = window.localStorage.getItem('userRole') || 'admin';
return successResponseWrap({ return successResponseWrap({
@ -39,7 +39,7 @@ setupMock({
}); });
// 登录 // 登录
Mock.mock(new RegExp('/api/user/login'), (params: MockParams) => { Mock.mock(new RegExp('/api/rest/user/login'), (params: MockParams) => {
const { username, password } = JSON.parse(params.body); const { username, password } = JSON.parse(params.body);
if (!username) { if (!username) {
return failResponseWrap(null, '用户名不能为空', 50000); return failResponseWrap(null, '用户名不能为空', 50000);

View File

@ -23,6 +23,7 @@ const router = createRouter({
requiresAuth: false, requiresAuth: false,
}, },
}, },
...appRoutes, ...appRoutes,
REDIRECT_MAIN, REDIRECT_MAIN,
NOT_FOUND_ROUTE, NOT_FOUND_ROUTE,

View File

@ -1,6 +1,7 @@
import type { RouteRecordNormalized } from 'vue-router'; import type { RouteRecordNormalized } from 'vue-router';
const modules = import.meta.glob('./modules/*.ts', { eager: true }); const modules = import.meta.glob('./modules/*.ts', { eager: true });
const externalModules = import.meta.glob('./externalModules/*.ts', { const externalModules = import.meta.glob('./externalModules/*.ts', {
eager: true, eager: true,
}); });

View File

@ -0,0 +1,28 @@
import { DEFAULT_LAYOUT } from '../base';
import { AppRouteRecordRaw } from '../types';
const CHARACTER: AppRouteRecordRaw = {
path: '/character',
name: 'character',
component: DEFAULT_LAYOUT,
meta: {
locale: 'menu.role',
icon: 'icon-computer',
requiresAuth: true,
order: 2,
},
children: [
{
path: 'manage',
name: 'Manage',
component: () => import('@/views/character/manage/index.vue'),
meta: {
locale: 'menu.role.manage',
requiresAuth: true,
permissions: ['*'],
},
},
],
};
export default CHARACTER;

View File

@ -8,8 +8,8 @@ const DASHBOARD: AppRouteRecordRaw = {
meta: { meta: {
locale: 'menu.dashboard', locale: 'menu.dashboard',
requiresAuth: true, requiresAuth: true,
icon: 'icon-dashboard', icon: 'icon-dashboard', // 设置图标
order: 0, order: 0, // 排序路由菜单项。如果设置该值,值越高,越靠前
}, },
children: [ children: [
{ {
@ -24,13 +24,13 @@ const DASHBOARD: AppRouteRecordRaw = {
}, },
{ {
path: 'monitor', path: 'monitor',// 一级路径
name: 'Monitor', name: 'Monitor',// 路由名称
component: () => import('@/views/dashboard/monitor/index.vue'), component: () => import('@/views/dashboard/monitor/index.vue'),// 要跳转的视图,这里要跳转到页面的基本布局
meta: { meta: {
locale: 'menu.dashboard.monitor', locale: 'menu.dashboard.monitor',// 菜单名字
requiresAuth: true, requiresAuth: true,// 需要登录鉴权
permissions: ['admin'], permissions: ['admin'],// 只允许管理员用户访问
}, },
}, },
], ],

View File

@ -3,9 +3,10 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import useAppStore from './modules/app'; import useAppStore from './modules/app';
import useUserStore from './modules/user'; import useUserStore from './modules/user';
import useTabBarStore from './modules/tab-bar'; import useTabBarStore from './modules/tab-bar';
import useRoleStore from './modules/role';
const pinia = createPinia(); const pinia = createPinia();
pinia.use(piniaPluginPersistedstate); pinia.use(piniaPluginPersistedstate);
export { useAppStore, useUserStore, useTabBarStore }; export { useAppStore, useUserStore, useTabBarStore, useRoleStore };
export default pinia; export default pinia;

View File

@ -0,0 +1,54 @@
import { defineStore } from 'pinia';
import {
queryRoles,
ListRecord,
addUser,
remove,
update,
enabled,
} from '@/api/role';
import { RoleState } from './type';
const useRoleStore = defineStore('role', {
state: (): RoleState => ({
current: undefined,
pageSize: undefined,
username: undefined,
phone: undefined,
email: undefined,
enable: undefined,
nickname: undefined,
avater: undefined,
address: undefined,
}),
getters: {
roleInfo(state: RoleState): RoleState {
return { ...state };
},
},
actions: {
async getUserInfo(params: any) {
const res = await queryRoles(params);
return res;
},
async addUser(params: any) {
return addUser(params);
},
async deleteUser(id: string) {
return remove(id);
},
async updateUser(data: any) {
return update(data);
},
async enabledStatus(id: string) {
return enabled(id);
},
},
});
export default useRoleStore;

View File

@ -0,0 +1,14 @@
import { RoleRecord } from '@/api/role';
export type RoleType = '' | '*' | 'admin' | 'user' | string[];
export interface RoleState {
current?: string;
pageSize?: string;
username?: string;
phone?: string;
email?: string;
enable?: string;
nickname?: string;
avater?: string;
address?: string;
}

View File

@ -5,6 +5,7 @@ import {
switchRole, switchRole,
getUserInfo, getUserInfo,
LoginData, LoginData,
me,
} from '@/api/user'; } from '@/api/user';
import { setToken, clearToken } from '@/utils/auth'; import { setToken, clearToken } from '@/utils/auth';
import { removeRouteListener } from '@/utils/route-listener'; import { removeRouteListener } from '@/utils/route-listener';
@ -14,16 +15,19 @@ import useAppStore from '../app';
const useUserStore = defineStore('user', { const useUserStore = defineStore('user', {
state: (): UserState => ({ state: (): UserState => ({
username: undefined, username: undefined,
name: undefined, nickName: undefined,
avatar: undefined, avatar: undefined,
email: undefined, email: undefined,
phone: undefined, phone: undefined,
address: undefined,
createAt: undefined, createAt: undefined,
remark: undefined, remark: undefined,
id: undefined, id: undefined,
role: undefined, role: undefined,
roles: undefined, roles: undefined,
permissions: [], permissions: [],
headerName: '',
tokenDate: '',
}), }),
getters: { getters: {
@ -60,12 +64,20 @@ const useUserStore = defineStore('user', {
this.setInfo(res.data); this.setInfo(res.data);
}, },
// Get user's crsf
async me() {
const res = await me();
this.headerName = res.data.csrf.headerName;
this.tokenDate = res.data.csrf.token;
return res;
},
// Login // Login
async login(loginForm: LoginData) { async login(loginForm: LoginData, headerName: string, token: string) {
try { try {
const res = await userLogin(loginForm); const res = await userLogin(loginForm, headerName, token);
// setToken(res.data.token); setToken(token);
setToken(res.data.token);
} catch (err) { } catch (err) {
clearToken(); clearToken();
throw err; throw err;

View File

@ -3,14 +3,17 @@ import { RoleRecord } from '@/api/role';
export type RoleType = '' | '*' | 'admin' | 'user' | string[]; export type RoleType = '' | '*' | 'admin' | 'user' | string[];
export interface UserState { export interface UserState {
username?: string; username?: string;
name?: string; nickName?: string;
avatar?: string; avatar?: string;
email?: string; email?: string;
phone?: string; phone?: string;
address?: string;
createAt?: string; createAt?: string;
remark?: string; remark?: string;
id?: number; id?: number;
role?: RoleRecord; role?: RoleRecord;
roles?: RoleRecord[]; roles?: RoleRecord[];
permissions?: string[] | '' | '*' | 'admin' | 'user'; permissions?: string[] | '' | '*' | 'admin' | 'user';
headerName: string;
tokenDate: string;
} }

View File

@ -24,9 +24,8 @@ export interface PostData {
} }
export interface Pagination { export interface Pagination {
current: number; page: number;
pageSize: number; size: number;
total?: number;
} }
export type TimeRanger = [string, string]; export type TimeRanger = [string, string];

View File

@ -0,0 +1,489 @@
<template>
<div class="container">
<Breadcrumb :items="['menu.role', 'menu.role.manage']" />
<a-card class="general-card" :title="$t('menu.list.searchTable')">
<a-row>
<a-col :flex="1">
<a-form
:model="formModel"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 18 }"
label-align="left"
>
<a-row :gutter="16">
<a-col :span="7">
<a-form-item
field="username"
:label="$t('searchTable.form.username')"
>
<a-input
v-model="formModel.username"
:placeholder="$t('searchTable.form.username.placeholder')"
/>
</a-form-item>
</a-col>
<a-col :span="9">
<a-form-item
field="phone"
:label="$t('searchTable.form.phone')"
>
<a-input
v-model="formModel.phone"
:placeholder="$t('searchTable.form.phone.placeholder')"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item
field="email"
:label="$t('searchTable.form.email')"
>
<a-input
v-model="formModel.email"
:placeholder="$t('searchTable.form.email.placeholder')"
/>
</a-form-item>
</a-col>
<!-- <a-col :span="9">
<a-form-item field="role" :label="$t('searchTable.form.role')">
<a-select
v-model="formModel.role"
:options="roleOptions"
:placeholder="$t('searchTable.form.role.placeholder')"
/>
</a-form-item>
</a-col> -->
</a-row>
</a-form>
</a-col>
<a-divider style="height: 84px" direction="vertical" />
<!-- 更新和重置按钮 -->
<a-col :flex="'86px'" style="text-align: right">
<a-space direction="vertical" :size="18">
<a-button type="primary" @click="search">
<template #icon>
<icon-search />
</template>
{{ $t('searchTable.form.search') }}
</a-button>
<a-button @click="reset">
<template #icon>
<icon-refresh />
</template>
{{ $t('searchTable.form.reset') }}
</a-button>
</a-space>
</a-col>
</a-row>
<a-divider style="margin-top: 0" />
<a-row :gutter="10">
<a-col :span="9">
<a-button
type="primary"
@click="handleClick"
style="margin-right: 16px"
>
<template #icon>
<icon-plus />
</template>
{{ $t('searchTable.operation.create') }}
</a-button>
<a-button type="primary" status="danger" @click="handleDelete">
<template #icon>
<icon-close />
</template>
{{ $t('searchTable.operation.delete') }}
</a-button>
</a-col>
</a-row>
<a-divider style="margin-top: 16px" />
<a-table
row-key="id"
:loading="loading"
:pagination="false"
:columns="(cloneColumns as TableColumnData[])"
:data="renderData"
:bordered="false"
:size="size"
:row-selection="true"
@select="selectRow"
>
<template #index="{ rowIndex }">
{{ rowIndex + 1 + (pagination.page - 1) * pagination.size }}
</template>
<template #enabled="{ record }">
<a-switch
:model-value="record.enabled"
:checked-value="true"
:unchecked-value="false"
@change="enabledStatus(record)"
/>
</template>
<template #operations="{ record }">
<a-button
type="text"
size="small"
@click="handleClick(record, 'modify')"
>
{{ $t('searchTable.columns.operations.detail') }}
</a-button>
</template>
</a-table>
<a-space direction="vertical" size="large" style="margin-top: 16px">
<a-pagination
show-total
:current="pagination.page"
@change="onPageChange"
:page-size="pagination.pageSize"
:total="pagination.total"
/>
</a-space>
<a-modal
v-model:visible="visible"
:title="dialogTitle"
:mask-closable="false"
@cancel="handleCancel"
:before-ok="handleBeforeOk"
>
<a-form :model="formDate">
<a-form-item field="username" :label="$t('add.user.info.username')">
<a-input
v-model="formDate.username"
:placeholder="$t('add.user.info.username.placeholder')"
:required="required"
:rules="[
{
required: true,
message: '用户名不能为空',
},
]"
/>
</a-form-item>
<a-form-item field="password" :label="$t('add.user.info.password')">
<a-input-password
v-model="formDate.password"
:placeholder="$t('add.user.info.password.placeholder')"
:rules="[
{
required: true,
message: '密码不能为空',
},
]"
/>
</a-form-item>
<a-form-item field="phone" :label="$t('add.user.info.phone')">
<a-input
v-model="formDate.phone"
:placeholder="$t('add.user.info.phone.placeholder')"
:rules="[
{
required: true,
message: '手机号码不能为空',
},
]"
/>
</a-form-item>
<a-form-item field="email" :label="$t('add.user.info.email')">
<a-input
v-model="formDate.email"
:placeholder="$t('add.user.info.email.placeholder')"
/>
</a-form-item>
<a-form-item field="nickName" :label="$t('add.user.info.nickName')">
<a-input
v-model="formDate.nickName"
:placeholder="$t('add.user.info.nickName.placeholder')"
/>
</a-form-item>
<a-form-item field="address" :label="$t('add.user.info.address')">
<a-input
v-model="formDate.address"
:placeholder="$t('add.user.info.address.placeholder')"
/>
</a-form-item>
<a-form-item field="dept" :label="$t('add.user.info.dept')">
<a-select
v-model="formDate.dept"
:placeholder="$t('add.user.info.dept.placeholder')"
>
<a-option value="user">user</a-option>
<a-option value="auditor">auditor</a-option>
<a-option value="admin">admin</a-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, reactive, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import type { SelectOptionData } from '@arco-design/web-vue/es/select/interface';
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
import { useRoleStore } from '@/store';
import { ListParams } from '@/api/list';
import { Pagination } from '@/types/global';
import { ListRecord } from '@/api/role';
import { Message } from '@arco-design/web-vue';
import useLoading from '@/hooks/loading';
import { status } from 'nprogress';
import { stat } from 'fs';
type SizeProps = 'mini' | 'small' | 'medium' | 'large';
type Column = TableColumnData & { checked?: true };
//
const generateFormModel = () => {
return {
username: '',
phone: '',
email: '',
role: '',
};
};
const { t } = useI18n();
const { loading, setLoading } = useLoading();
const roleStore = useRoleStore();
const renderData = ref<ListRecord[]>([]);
const cloneColumns = ref<Column[]>([]);
const showColumns = ref<Column[]>([]);
const size = ref<SizeProps>('medium');
const seleteRowList = ref();
const visible = ref(false);
const messageType = ref();
let formDifer = {};
const dialogTitle = ref('添加用户');
const form = () => {
return {
id: '',
username: '',
password: '',
phone: '',
email: '',
nickName: '',
address: '',
dept: '',
};
};
const formDate = ref(form());
//
const handleClick = (record: any, type: string) => {
if (type === 'modify') {
messageType.value = 'modify';
formDifer = record;
formDate.value = { ...record };
dialogTitle.value = '修改用户信息';
}
visible.value = true;
};
//
const diffDataForm = (newData: any, oldData: any) => {
const result = {}; //
Object.keys(oldData).forEach((key) => {
if (oldData[key] !== newData[key] || key === 'id') {
result[key] = newData[key];
}
});
return result;
};
//
const handleBeforeOk = async (done: any) => {
try {
formDifer = diffDataForm(formDate.value, formDifer);
if (dialogTitle.value === '修改用户信息') {
const res = await roleStore.updateUser(formDifer);
if (res.status === 200) {
Message.success({
content: t('add.user.info.sucess'),
duration: 3 * 1000,
});
} else {
Message.error({
content: t('add.user.info.fail'),
duration: 3 * 1000,
});
}
} else {
const res = await roleStore.addUser(formDate.value);
if (res.status === 200) {
Message.success({
content: t('modify.user.info.sucess'),
duration: 3 * 1000,
});
} else {
Message.error({
content: t('modify.user.info.fail'),
duration: 3 * 1000,
});
}
}
} catch (err) {
// you can report use errorHandler or other
} finally {
formDate.value = form();
search();
}
};
//
const enabledStatus = async (record: string) => {
const res = await roleStore.enabledStatus(record.id);
search();
};
//
const selectRow = (rowKeys: string[]) => {
seleteRowList.value = rowKeys;
};
//
const handleDelete = async () => {
const res = await roleStore.deleteUser(seleteRowList.value);
};
//
const handleCancel = () => {
formDate.value = form();
visible.value = false;
dialogTitle.value = '添加用户';
};
// formModel
const formModel = ref(generateFormModel());
//
const basePagination: Pagination = {
page: 1,
size: 10,
};
const pagination = reactive({
...basePagination,
});
//
const onPageChange = (current: number) => {
const page = current;
fetchData({ ...basePagination, page });
};
//
const fetchData = async (params: { page: 1; size: 10 }) => {
setLoading(true);
try {
const { data } = await roleStore.getUserInfo(params);
renderData.value = data.list;
pagination.page = params.page;
pagination.total = data.total;
} catch (err) {
// you can report use errorHandler or other
} finally {
setLoading(false);
}
};
//
const search = () => {
fetchData({
...basePagination,
...formModel.value,
} as unknown as any);
};
search();
//
const reset = () => {
formModel.value = generateFormModel();
};
//
const columns = computed<TableColumnData[]>(() => [
{
title: t('searchTable.columns.index'),
dataIndex: 'index',
slotName: 'index',
},
{
title: t('searchTable.columns.username'),
dataIndex: 'username',
},
{
title: t('searchTable.columns.phone'),
dataIndex: 'phone',
},
{
title: t('searchTable.columns.email'),
dataIndex: 'email',
},
{
title: t('searchTable.columns.nickName'),
dataIndex: 'nickName',
},
{
title: t('searchTable.columns.address'),
dataIndex: 'address',
},
{
title: t('searchTable.columns.enabled'),
dataIndex: 'enabled',
slotName: 'enabled',
},
{
title: t('searchTable.columns.operations'),
dataIndex: 'operations',
slotName: 'operations',
},
]);
//
watch(
() => columns.value,
(val) => {
cloneColumns.value = val;
cloneColumns.value.forEach((item, index) => {
item.checked = true;
});
showColumns.value = cloneColumns.value;
},
{ deep: true, immediate: true }
);
//
const roleOptions = computed<SelectOptionData[]>(() => [
{
label: t('searchTable.form.role.user'),
vlaue: 'user',
},
{
label: t('searchTable.form.role.auditor'),
vlaue: 'auditor',
},
]);
</script>
<script lang="ts">
export default {
name: 'Manage',
};
</script>
<style scoped lang="less">
.container {
padding: 0 20px 20px 20px;
}
</style>

View File

@ -0,0 +1,45 @@
export default {
'menu.list.searchTable': 'Search Table',
'menu.role.manage': 'Role Manage',
'searchTable.form.number': 'Set Number',
'searchTable.form.number.placeholder': 'Please enter Set Number',
'searchTable.form.name': 'Set Name',
'searchTable.form.name.placeholder': 'Please enter Set Name',
'searchTable.form.contentType': 'Content Type',
'searchTable.form.contentType.img': 'image-text',
'searchTable.form.contentType.horizontalVideo': 'Horizontal short video',
'searchTable.form.contentType.verticalVideo': 'Vertical short video',
'searchTable.form.filterType': 'Filter Type',
'searchTable.form.filterType.artificial': 'artificial',
'searchTable.form.filterType.rules': 'Rules',
'searchTable.form.createdTime': 'Create Date',
'searchTable.form.status': 'Status',
'searchTable.form.status.online': 'Online',
'searchTable.form.status.offline': 'Offline',
'searchTable.form.search': 'Search',
'searchTable.form.reset': 'Reset',
'searchTable.form.selectDefault': 'All',
'searchTable.operation.create': 'Create',
'searchTable.operation.import': 'Import',
'searchTable.operation.download': 'Download',
// columns
'searchTable.columns.index': '#',
'searchTable.columns.number': 'Set Number',
'searchTable.columns.name': 'Set Name',
'searchTable.columns.contentType': 'Content Type',
'searchTable.columns.filterType': 'Filter Type',
'searchTable.columns.count': 'Count',
'searchTable.columns.createdTime': 'CreatedTime',
'searchTable.columns.status': 'Status',
'searchTable.columns.operations': 'Operations',
'searchTable.columns.operations.view': 'View',
// size
'searchTable.size.mini': 'mini',
'searchTable.size.small': 'small',
'searchTable.size.medium': 'middle',
'searchTable.size.large': 'large',
// actions
'searchTable.actions.refresh': 'refresh',
'searchTable.actions.density': 'density',
'searchTable.actions.columnSetting': 'columnSetting',
};

View File

@ -0,0 +1,46 @@
export default {
'menu.list.searchTable': '查询表格',
'menu.role.manage': '角色管理',
'roleManage.username': '用户名',
'searchTable.form.username': '用户名',
'searchTable.form.username.placeholder': '请输入用户名',
'searchTable.form.phone': '电话号码',
'searchTable.form.phone.placeholder': '请输入电话号码',
'searchTable.form.role': '角色',
'searchTable.form.role.placeholder': '请选择角色',
'searchTable.form.role.user': '普通用户',
'searchTable.form.email': 'Email',
'searchTable.form.email.placeholder': '请输入Email',
'searchTable.form.role.auditor': '审核员',
'searchTable.form.search': '查询',
'searchTable.form.reset': '重置',
'searchTable.columns.nickName': '昵称',
'searchTable.columns.username': '用户名',
'searchTable.columns.phone': '电话',
'searchTable.columns.email': 'Email',
'searchTable.columns.enabled': '是否启用',
'searchTable.columns.avater': '头像',
'searchTable.columns.address': '地址',
'searchTable.columns.operations.detail': '详细',
'searchTable.operation.create': '添加',
'searchTable.operation.delete': '删除',
'add.user.info': '添加用户',
'add.user.info.username': '用户名',
'add.user.info.username.placeholder': '请输入用户名',
'add.user.info.password': '密码',
'add.user.info.password.placeholder': '请输入密码',
'add.user.info.phone': '电话号码',
'add.user.info.phone.placeholder': '请输入电话号码',
'add.user.info.email': 'Email',
'add.user.info.email.placeholder': '请输入Email',
'add.user.info.nickName': '昵称',
'add.user.info.nickName.placeholder': '请输入昵称',
'add.user.info.address': '地址',
'add.user.info.address.placeholder': '请输入地址',
'add.user.info.dept': '角色',
'add.user.info.dept.placeholder': '请选择角色',
'add.user.info.sucess': '添加成功',
'add.user.info.fail': '添加失败',
'modify.user.info.sucess': '修改成功',
'modify.user.info.fail': '修改失败',
};

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="login-form-wrapper"> <div class="login-form-wrapper">
<div class="login-form-title">{{ $t('login.form.title') }}</div> <div class="login-form-title">{{ $t('login.form.title') }}</div>
<!-- <div class="login-form-sub-title">{{ $t('login.form.title') }}</div>--> <!-- <div class="login-form-sub-title">{{ $t('login.form.title') }}</div>-->
<!-- <div class="login-form-sub-title">请先登录</div>--> <!-- <div class="login-form-sub-title">请先登录</div>-->
<div class="login-form-error-msg">{{ errorMessage }}</div> <div class="login-form-error-msg">{{ errorMessage }}</div>
<a-form <a-form
ref="loginForm" ref="loginForm"
@ -56,6 +56,7 @@
<a-button type="primary" html-type="submit" long :loading="loading"> <a-button type="primary" html-type="submit" long :loading="loading">
{{ $t('login.form.login') }} {{ $t('login.form.login') }}
</a-button> </a-button>
<a-button type="text" long class="login-form-register-btn"> <a-button type="text" long class="login-form-register-btn">
{{ $t('login.form.register') }} {{ $t('login.form.register') }}
</a-button> </a-button>
@ -65,103 +66,134 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive } from 'vue'; import { ref, reactive } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { ValidatedError } from '@arco-design/web-vue/es/form/interface'; import { ValidatedError } from '@arco-design/web-vue/es/form/interface';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useStorage } from '@vueuse/core'; import { useStorage } from '@vueuse/core';
import { useUserStore } from '@/store'; import { useUserStore } from '@/store';
import useLoading from '@/hooks/loading'; import useLoading from '@/hooks/loading';
import type { LoginData } from '@/api/user'; import { LoginData } from '@/api/user';
const router = useRouter();
const { t } = useI18n();
const errorMessage = ref('');
const { loading, setLoading } = useLoading();
const userStore = useUserStore();
const loginConfig = useStorage('login-config', {
rememberPassword: true,
username: '', //
password: '', // demo default value
});
const userInfo = reactive({
username: loginConfig.value.username,
password: loginConfig.value.password,
});
const handleSubmit = async ({ const router = useRouter();
errors, const { t } = useI18n();
values, const errorMessage = ref('');
}: { const { loading, setLoading } = useLoading();
errors: Record<string, ValidatedError> | undefined; const userStore = useUserStore();
values: Record<string, any>;
}) => { const loginConfig = useStorage('login-config', {
if (loading.value) return; // 使 useStorage
if (!errors) { rememberPassword: true,
setLoading(true); username: '', //
try { password: '', // demo default value
await userStore.login(values as LoginData); });
const { redirect, ...othersQuery } = router.currentRoute.value.query;
router.push({ const userInfo = reactive({
name: (redirect as string) || 'Workplace', // loginConfig username password
query: { username: loginConfig.value.username,
...othersQuery, password: loginConfig.value.password,
}, });
});
Message.success(t('login.form.login.success')); const handleSubmit = async ({
const { rememberPassword } = loginConfig.value; //
const { username, password } = values; errors,
// values,
// The actual production environment requires encrypted storage. }: {
loginConfig.value.username = rememberPassword ? username : ''; errors: Record<string, ValidatedError> | undefined;
loginConfig.value.password = rememberPassword ? password : ''; values: Record<string, any>;
} catch (err) { }) => {
// errorMessage.value = (err as Error).message; //
errorMessage.value = '登录失败'; if (loading.value) return;
} finally {
setLoading(false); //
} if (!errors) {
setLoading(true);
try {
const res = await userStore.me();
// console.log('e', res.data.csrf);
// await userCsrf.update(res)
// userCsrf.content = res.data.csrf;
// 使 userStore.login
await userStore.login(
values as LoginData,
res.data.csrf.headerName,
res.data.csrf.token
);
const csrf = await userStore.me();
// console.log('vue', csrf.data.csrf);
// userCsrf.content = csrf.data.csrf;
// let {crsf} = storeToRefs(userStore)
// useUserStore.user.csrf = csrf.data.csrf;
//
const { redirect, ...othersQuery } = router.currentRoute.value.query;
router.push({
name: 'Workplace',
query: {
...othersQuery,
},
});
//
Message.success(t('login.form.login.success'));
const { rememberPassword } = loginConfig.value;
const { username, password } = values;
//
// The actual production environment requires encrypted storage.
loginConfig.value.username = rememberPassword ? username : '';
loginConfig.value.password = rememberPassword ? password : '';
} catch (err) {
// errorMessage.value = (err as Error).message;
errorMessage.value = '登录失败';
} finally {
setLoading(false);
} }
}; }
const setRememberPassword = (value: boolean) => { };
loginConfig.value.rememberPassword = value; const setRememberPassword = (value: boolean) => {
}; loginConfig.value.rememberPassword = value;
};
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.login-form { .login-form {
&-wrapper { &-wrapper {
width: 320px; width: 320px;
}
&-title {
color: var(--color-text-1);
font-weight: 500;
font-size: 24px;
line-height: 32px;
}
&-sub-title {
color: var(--color-text-3);
font-size: 16px;
line-height: 24px;
}
&-error-msg {
height: 32px;
color: rgb(var(--red-6));
line-height: 32px;
}
&-password-actions {
display: flex;
justify-content: space-between;
}
&-register-btn {
color: var(--color-text-3) !important;
}
} }
&-title {
color: var(--color-text-1);
font-weight: 500;
font-size: 24px;
line-height: 32px;
}
&-sub-title {
color: var(--color-text-3);
font-size: 16px;
line-height: 24px;
}
&-error-msg {
height: 32px;
color: rgb(var(--red-6));
line-height: 32px;
}
&-password-actions {
display: flex;
justify-content: space-between;
}
&-register-btn {
color: var(--color-text-3) !important;
}
}
</style> </style>

View File

@ -1,5 +1,5 @@
export default { export default {
'login.form.title': '中山学院学习平台', 'login.form.title': '学院学习平台',
'login.form.userName.errMsg': '用户名不能为空', 'login.form.userName.errMsg': '用户名不能为空',
'login.form.password.errMsg': '密码不能为空', 'login.form.password.errMsg': '密码不能为空',
'login.form.login.errMsg': '登录出错,请刷新重试', 'login.form.login.errMsg': '登录出错,请刷新重试',

View File

@ -8,24 +8,26 @@
<img :src="userInfo.avatar" /> <img :src="userInfo.avatar" />
</a-avatar> </a-avatar>
<a-typography-title :heading="6" style="margin: 0"> <a-typography-title :heading="6" style="margin: 0">
{{ userInfo.username }} {{ userInfo.nickName }}
</a-typography-title> </a-typography-title>
<div class="user-msg"> <div class="user-msg">
<a-space :size="18"> <a-space :size="18">
<div> <div>
<icon-user /> <icon-phone />
<a-typography-text>{{ userInfo.username }}</a-typography-text>
</div>
<div>
<icon-home />
<a-typography-text> <a-typography-text>
{{ userInfo.phone }} {{ userInfo.phone }}
</a-typography-text> </a-typography-text>
</div> </div>
<!-- <div>--> <div>
<!-- <icon-location />--> <icon-email />
<!-- <a-typography-text>{{ userInfo.locationName }}</a-typography-text>--> <a-typography-text>
<!-- </div>--> {{ userInfo.email }}
</a-typography-text>
</div>
<!-- <div>-->
<!-- <icon-location />-->
<!-- <a-typography-text>{{ userInfo.locationName }}</a-typography-text>-->
<!-- </div>-->
</a-space> </a-space>
</div> </div>
</a-space> </a-space>
@ -33,37 +35,37 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useUserStore } from '@/store'; import { useUserStore } from '@/store';
const userInfo = useUserStore(); const userInfo = useUserStore();
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.header { .header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 204px; height: 204px;
color: var(--gray-10); color: var(--gray-10);
background: url(//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/41c6b125cc2e27021bf7fcc9a9b1897c.svg~tplv-49unhts6dw-image.image) background: url(//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/41c6b125cc2e27021bf7fcc9a9b1897c.svg~tplv-49unhts6dw-image.image)
no-repeat; no-repeat;
background-size: cover; background-size: cover;
border-radius: 4px; border-radius: 4px;
:deep(.arco-avatar-trigger-icon-button) { :deep(.arco-avatar-trigger-icon-button) {
color: rgb(var(--arcoblue-6)); color: rgb(var(--arcoblue-6));
:deep(.arco-icon) { :deep(.arco-icon) {
vertical-align: -1px; vertical-align: -1px;
}
}
.user-msg {
.arco-icon {
color: rgb(var(--gray-10));
}
.arco-typography {
margin-left: 6px;
}
} }
} }
.user-msg {
.arco-icon {
color: rgb(var(--gray-10));
}
.arco-typography {
margin-left: 6px;
}
}
}
</style> </style>

View File

@ -38,7 +38,7 @@
/> />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
field="name" field="nickName"
label="昵称" label="昵称"
:rules="[ :rules="[
{ {
@ -48,13 +48,13 @@
]" ]"
> >
<a-input <a-input
v-model="formData.name" v-model="formData.nickName"
:placeholder="$t('userSetting.basicInfo.placeholder.nickname')" :placeholder="$t('userSetting.basicInfo.placeholder.nickname')"
/> />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
field="profile" field="address"
label="备注" label="地址"
:rules="[ :rules="[
{ {
maxLength: 200, maxLength: 200,
@ -63,7 +63,7 @@
]" ]"
row-class="keep-margin" row-class="keep-margin"
> >
<a-textarea v-model="formData.remark" /> <a-textarea v-model="formData.address" />
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-space> <a-space>
@ -79,41 +79,41 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useUserStore } from '@/store'; import { useUserStore } from '@/store';
import { FormInstance } from '@arco-design/web-vue/es/form'; import { FormInstance } from '@arco-design/web-vue/es/form';
import { UserState } from '@/store/modules/user/types'; import { UserState } from '@/store/modules/user/types';
import { selfUpdate } from '@/api/user'; import { selfUpdate } from '@/api/user';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
const userStore = useUserStore(); const userStore = useUserStore();
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const formData = ref<UserState>({ const formData = ref<UserState>({
...userStore.userInfo, ...userStore.userInfo,
}); });
const validate = async () => { const validate = async () => {
const valid = await formRef.value?.validate(); const valid = await formRef.value?.validate();
if (!valid) { if (!valid) {
// do some thing // do some thing
// you also can use html-type to submit // you also can use html-type to submit
const res = await selfUpdate(formData.value); const res = await selfUpdate(formData.value);
if (res.status === 200) { if (res.status === 200) {
Message.success({ Message.success({
content: '编辑成功', content: '编辑成功',
duration: 5 * 1000, duration: 5 * 1000,
}); });
}
} }
}; }
const reset = async () => { };
await formRef.value?.resetFields(); const reset = async () => {
}; await formRef.value?.resetFields();
};
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.form { .form {
width: 540px; width: 540px;
margin: 20px auto; margin: 20px auto;
} }
</style> </style>

View File

@ -7,13 +7,26 @@
:wrapper-col-props="{ span: 16 }" :wrapper-col-props="{ span: 16 }"
> >
<a-form-item <a-form-item
field="newPassword" field="oldPassword"
:label="$t('userSetting.passwordReset.form.label.oldPassword')"
:rules="[{ required: true, message: $t('login.form.password.errMsg') }]"
>
<a-input-password
v-model="formData.oldPassword"
placeholder="请输入原密码"
allow-clear
>
</a-input-password>
</a-form-item>
<a-form-item
field="password"
:label="$t('userSetting.passwordReset.form.label.newPassword')" :label="$t('userSetting.passwordReset.form.label.newPassword')"
:rules="[{ required: true, message: $t('login.form.password.errMsg') }]" :rules="[{ required: true, message: $t('login.form.password.errMsg') }]"
:validate-trigger="['change', 'blur']" :validate-trigger="['change', 'blur']"
> >
<a-input-password <a-input-password
v-model="formData.newPassword" v-model="formData.password"
placeholder="请输入新密码" placeholder="请输入新密码"
allow-clear allow-clear
> >
@ -46,55 +59,58 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { FormInstance } from '@arco-design/web-vue/es/form'; import { FormInstance } from '@arco-design/web-vue/es/form';
import { PasswordReSetModel, resetPassword } from '@/api/user'; import { PasswordReSetModel, resetPassword } from '@/api/user';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import useUser from "@/hooks/user"; import useUser from '@/hooks/user';
const { t } = useI18n(); const { t } = useI18n();
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const formData = ref<PasswordReSetModel>({ const formData = ref<PasswordReSetModel>({
newPassword: '', oldPassword: '',
confirmPassword: '', password: '',
}); confirmPassword: '',
const checkEquals = ( });
value: string | undefined, const checkEquals = (
callback: (error?: string) => void value: string | undefined,
) => { callback: (error?: string) => void
if (!value) { ) => {
callback(t('userSetting.passwordReset.form.validate.blank')); if (!value) {
} else if (formData.value.newPassword !== formData.value.confirmPassword) { callback(t('userSetting.passwordReset.form.validate.blank'));
callback(t('userSetting.passwordReset.form.validate.noEquals')); } else if (formData.value.password !== formData.value.confirmPassword) {
callback(t('userSetting.passwordReset.form.validate.noEquals'));
}
};
const user = useUser();
const validate = async () => {
const vali = await formRef.value?.validate();
if (!vali) {
// do some thing
// you also can use html-type to submit
const res = await resetPassword(formData.value);
if (res.status === 200) {
Message.success({
content: res.data,
duration: 5 * 1000,
});
// await user.logout();
} }
}; }
};
const user = useUser(); const reset = async () => {
const validate = async () => { await formRef.value?.resetFields();
const vali = await formRef.value?.validate(); };
if (!vali) {
// do some thing
// you also can use html-type to submit
const res = await resetPassword(formData.value);
if (res.status === 200) {
Message.success({
content: res.data,
duration: 5 * 1000,
});
await user.logout();
}
}
};
const reset = async () => {
await formRef.value?.resetFields();
};
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.form { .form {
width: 540px; width: 540px;
margin: 20px auto; margin: 20px auto;
} }
</style> </style>

View File

@ -45,6 +45,7 @@ export default {
'userSetting.basicInfo.placeholder.profile': 'userSetting.basicInfo.placeholder.profile':
'Please enter your profile, no more than 200 words', 'Please enter your profile, no more than 200 words',
'userSetting.form.error.profile.maxLength': 'No more than 200 words', 'userSetting.form.error.profile.maxLength': 'No more than 200 words',
'userSetting.passwordReset.form.label.oldPassword': 'Orinal Password',
'userSetting.SecuritySettings.form.label.password': 'Login Password', 'userSetting.SecuritySettings.form.label.password': 'Login Password',
'userSetting.SecuritySettings.placeholder.password': 'userSetting.SecuritySettings.placeholder.password':
'Has been set. The password must contain at least six letters, digits, and special characters except Spaces. The password must contain both uppercase and lowercase letters.', 'Has been set. The password must contain at least six letters, digits, and special characters except Spaces. The password must contain both uppercase and lowercase letters.',

View File

@ -33,6 +33,7 @@ export default {
'userSetting.basicInfo.placeholder.profile': 'userSetting.basicInfo.placeholder.profile':
'请输入您的个人简介最多不超过200字。', '请输入您的个人简介最多不超过200字。',
'userSetting.form.error.profile.maxLength': '最多不超过200字', 'userSetting.form.error.profile.maxLength': '最多不超过200字',
'userSetting.passwordReset.form.label.oldPassword': '原密码',
'userSetting.passwordReset.form.label.newPassword': '新密码', 'userSetting.passwordReset.form.label.newPassword': '新密码',
'userSetting.passwordReset.form.label.confirmPassword': '确认新密码', 'userSetting.passwordReset.form.label.confirmPassword': '确认新密码',
'userSetting.SecuritySettings.form.label.password': '登录密码', 'userSetting.SecuritySettings.form.label.password': '登录密码',