Browse Source

1.修改前端代码适配ABP4的接口:获取站点信息、登录、获取用户信息;2.添加用户和角色的路由和表单配置,适配列表功能

pull/1/head^2
wanggang 1 year ago
parent
commit
ac7bee6eb4
  1. 2
      code/WebApp/vanilla/api/site.js
  2. 16
      code/WebApp/vanilla/api/user.js
  3. 27
      code/WebApp/vanilla/components/form/form-input.js
  4. 11
      code/WebApp/vanilla/components/form/form-item.js
  5. 67
      code/WebApp/vanilla/components/form/index.js
  6. 43
      code/WebApp/vanilla/components/icon/index.js
  7. 687
      code/WebApp/vanilla/components/list/index.js
  8. 36
      code/WebApp/vanilla/components/markdown/index.js
  9. 13
      code/WebApp/vanilla/index.html
  10. 13
      code/WebApp/vanilla/layouts/header.js
  11. 26
      code/WebApp/vanilla/layouts/index.js
  12. 7
      code/WebApp/vanilla/layouts/locale.js
  13. 20
      code/WebApp/vanilla/layouts/menu-item.js
  14. 2
      code/WebApp/vanilla/layouts/menu.js
  15. 35
      code/WebApp/vanilla/layouts/tabs.js
  16. 36
      code/WebApp/vanilla/lib/element-plus/index.full.min.mjs
  17. 2
      code/WebApp/vanilla/main.js
  18. 52
      code/WebApp/vanilla/models/login.js
  19. 50
      code/WebApp/vanilla/models/role.js
  20. 98
      code/WebApp/vanilla/models/user.js
  21. 72
      code/WebApp/vanilla/request/index.js
  22. 114
      code/WebApp/vanilla/router/index.js
  23. 101
      code/WebApp/vanilla/router/routes.js
  24. 33
      code/WebApp/vanilla/signalr/index.js
  25. 7
      code/WebApp/vanilla/store/app.js
  26. 5
      code/WebApp/vanilla/styles/site.css
  27. 6
      code/WebApp/vanilla/utils/index.js
  28. 2
      code/WebApp/vanilla/views/404.js
  29. 17
      code/WebApp/vanilla/views/base-data/role.js
  30. 17
      code/WebApp/vanilla/views/base-data/user.js
  31. 17
      code/WebApp/vanilla/views/list.js
  32. 84
      code/WebApp/vanilla/views/login.js
  33. 15
      code/WebApp/vanilla/views/user/index.js

2
code/WebApp/vanilla/api/site.js

@ -17,7 +17,7 @@ async function getLocalizationAsync() {
locale: "zh",
messages: {
zh: {
application: "北京北汽模塑-SAS结算分析系统",
application: "北汽模塑销售结算系统",
copyright: "长春市闻荫科技有限公司 ©2023",
test: "测试",
compareAttribute: "{0}”和{1}不匹配",

16
code/WebApp/vanilla/api/user.js

@ -16,6 +16,7 @@ const isLogin = async () => {
return true;
} else {
appStore.token = null;
removeAccessToken();
}
}
return false;
@ -39,6 +40,7 @@ const login = async (data) => {
const logout = () => {
const appStore = useAppStore();
appStore.token = null;
removeAccessToken();
removeRefreshToken();
router.push({ path: "/login", query: { redirect: router.currentRoute.value.fullPath } });
};
@ -47,11 +49,13 @@ const getUser = async () => {
const result = await get("abp/application-configuration");
const data = result.data;
const user = {};
user.name = data.currentUser.userName;
user.id = data.currentUser.id;
user.userName = data.currentUser.userName;
user.email = data.currentUser.email;
user.phoneNumber = data.currentUser.phoneNumber;
user.roles = data.currentUser.roles;
const menus = (await get("base/role-menus")).data;
user.permissions = menus.items;
user.permissions = data.auth.policies;
user.localization = data.localization;
return user;
};
@ -66,6 +70,10 @@ const hasPermission = (to) => {
}
};
const accessTokenKey = "access_token";
const getAccessToken = () => localStorage.getItem(accessTokenKey);
const setAccessToken = (token) => localStorage.setItem(accessTokenKey, token);
const removeAccessToken = () => localStorage.removeItem(accessTokenKey);
const refreshTokenKey = "refresh_token";
const getRefreshToken = () => localStorage.getItem(refreshTokenKey);
@ -77,4 +85,4 @@ const removeRefreshToken = () => {
connection.stop();
};
export { isLogin, login, logout, getUser, hasPermission };
export { isLogin, login, logout, getAccessToken, setAccessToken, setRefreshToken, getUser, hasPermission };

27
code/WebApp/vanilla/components/form/form-input.js

@ -1,6 +1,7 @@
import html from "html";
import { ref, reactive, watch } from "vue";
import { dayjs } from "element-plus";
import { post } from "../../request/index.js";
export default {
template: html`
@ -20,8 +21,9 @@ export default {
:multiple="!!schema.multiple"
clearable
style="width:100%"
:title="JSON.stringify(options)"
>
<el-option v-for="item in schema.options" :key="item.value" :label="item.label" :value="item.value" />
<el-option v-for="item in options" :key="item.key" :label="item.label" :value="item.value" />
</el-select>
</template>
<el-input
@ -40,7 +42,7 @@ export default {
/>
<template v-else-if="schema.type==='boolean'">
<el-select :disabled="getDisabled()" v-model="model[prop]" :placeholder="schema.title" v-if="schema.nullable">
<el-option prop="select" :value="null" :label="$t('select')" />
<el-option prop="select" value="" :label="$t('select')" />
<el-option prop="true" :value="true" :label="$t('true')" />
<el-option prop="false" :value="false" :label="$t('false')" />
</el-select>
@ -83,11 +85,32 @@ export default {
return schema.input ?? schema.type;
};
/*end*/
//options
const selectProps = ref({});
const selectValues = ref([]);
const options = ref([]);
if (props.schema.options) {
options.value = props.schema.options;
} else if (props.schema.url) {
try {
const url = `${props.schema.url}`;
const result = await post(url, { queryAll: true, query: { isReadonly: null, isDisabled: null, order: null } });
options.value = result.data?.items.map((o) => ({
value: o[props.schema.value],
label: o[props.schema.label],
}));
} catch (error) {
console.log(error);
}
}
return {
model,
getDisabled,
getInput,
dayjs,
selectProps,
selectValues,
options,
};
},
};

11
code/WebApp/vanilla/components/form/form-item.js

@ -1,16 +1,15 @@
import html from "html";
import { ref, reactive, watch } from "vue";
import AppFormInput from "./form-input.js";
import { defineAsyncComponent, ref, reactive, watch } from "vue";
export default {
name: "formItem",
components: { AppFormInput },
components: { AppFormInput: defineAsyncComponent(() => import("./form-input.js")) },
template: html`
<template v-if="showItem()">
<template v-if="schema.type==='object'"></template>
<template v-if="schema.type!=='array'||(schema.items.type!=='object'&&schema.items.type!=='array')"> </template>
<template v-else-if="schema.type!=='array'||schema.items.type!=='array'">
<el-form-item
:title="getProp(prop)"
:title="prop"
:label="schema.title"
:prop="getProp(prop)"
:rules="getRules(parentSchema,schema,model)"
@ -33,7 +32,7 @@ export default {
if (props.schema.hidden) {
return false;
}
if (props.schema.readOnly && (props.mode === "query" || props.mode === "create" || props.mode === "update")) {
if (props.schema.readOnly && (props.mode === "create" || props.mode === "update")) {
return false;
}
return true;

67
code/WebApp/vanilla/components/form/index.js

@ -1,27 +1,28 @@
import html from "html";
import { ref, reactive, watch } from "vue";
import AppFormItem from "./form-item.js";
import { defineAsyncComponent, ref, reactive, watch } from "vue";
export default {
components: { AppFormItem },
components: { AppFormItem: defineAsyncComponent(() => import("./form-item.js")) },
name: "AppForm",
template: html`<el-form ref="formRef" :model="model" label-width="auto">
<template v-for="(value, prop) in schema.properties">
<app-form-item
:parentSchema="schema"
:schema="value"
v-model="model"
:prop="prop"
:mode="mode"
:errors="errors"
/>
</template>
<slot name="items"></slot>
<el-form-item v-if="!hideButton">
<template #label></template>
<el-button type="primary" @click="submit" :disabled="loading"><slot>$t('confirm')</slot></el-button>
</el-form-item>
</el-form>`,
template: html`<div v-loading="loading">
<el-form ref="formRef" :model="model" label-width="auto">
<template v-for="(value, prop) in schema.properties">
<app-form-item
:parentSchema="schema"
:schema="value"
v-model="model"
:prop="prop"
:mode="mode"
:errors="errors"
/>
</template>
<slot name="items"></slot>
<el-form-item v-if="!hideButton">
<template #label></template>
<el-button type="primary" @click="submit" :disabled="loading"><slot>$t('confirm')</slot></el-button>
</el-form-item>
</el-form>
</div>`,
props: ["modelValue", "schema", "action", "hideButton", "isQueryForm", "mode"],
emits: ["update:modelValue", "submit"],
setup(props, context) {
@ -46,19 +47,21 @@ export default {
// submit
const submit = async () => {
try {
//const valid = await validate();
//if (valid) {
loading.value = true;
context.emit("submit", (serverErrors) => {
if (serverErrors) {
errors.value = serverErrors;
}
});
//}
const valid = await validate();
if (valid) {
loading.value = true;
context.emit(
"submit",
(serverErrors) => {
if (serverErrors) {
errors.value = serverErrors;
}
},
loading
);
}
} catch (error) {
console.error(error);
} finally {
loading.value = false;
console.log(error);
}
};
// expose

43
code/WebApp/vanilla/components/icon/index.js

@ -1,29 +1,42 @@
import html from "html";
import { onMounted, ref } from "vue";
const template = `<component v-if="name.indexOf('ep-')===0" :is="name" /> <v-else g v-html="svg" /> `;
import { useAppStore } from "../../store/index.js";
export default {
template: html`<template v-if="name.indexOf('ep-')===0">
<component :is="name" />
</template>
<template v-else>
<g v-html="svg" />
</template> `,
props: {
name: {
default: "file",
},
},
template,
setup(props) {
const svg = ref("");
async setup(props) {
const svg = ref(null);
onMounted(async () => {
if (props.name.indexOf("ep-") !== 0) {
if (!props.name.startsWith("ep-")) {
try {
const response = await fetch(`./assets/icons/${props.name}.svg`);
if (response.ok && response.status === 200) {
svg.value = await response.text();
}
const url = `./assets/icons/${props.name}.svg`;
navigator.locks.request(url, async () => {
const appStore = useAppStore();
if (appStore.cache.has(url)) {
svg.value = appStore.cache.get(url);
} else {
const response = await fetch(url);
if (response.ok && response.status === 200) {
svg.value = await response.text();
appStore.cache.set(url, svg.value);
}
}
});
} catch (error) {
console.error(error);
}
if (!svg.value) {
const response = await fetch("./assets/icons/file.svg");
svg.value = await response.text();
console.log(error);
if (!svg.value) {
svg.value = `<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M512 64a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32zm0 640a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V736a32 32 0 0 1 32-32zm448-192a32 32 0 0 1-32 32H736a32 32 0 1 1 0-64h192a32 32 0 0 1 32 32zm-640 0a32 32 0 0 1-32 32H96a32 32 0 0 1 0-64h192a32 32 0 0 1 32 32zM195.2 195.2a32 32 0 0 1 45.248 0L376.32 331.008a32 32 0 0 1-45.248 45.248L195.2 240.448a32 32 0 0 1 0-45.248zm452.544 452.544a32 32 0 0 1 45.248 0L828.8 783.552a32 32 0 0 1-45.248 45.248L647.744 692.992a32 32 0 0 1 0-45.248zM828.8 195.264a32 32 0 0 1 0 45.184L692.992 376.32a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0zm-452.544 452.48a32 32 0 0 1 0 45.248L240.448 828.8a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0z"></path></svg>`;
}
}
}
});

687
code/WebApp/vanilla/components/list/index.js

@ -1,135 +1,156 @@
import html, { getProp } from "html";
import AppForm from "../form/index.js";
import { get, post } from "../../request/index.js";
import { ref, reactive } from "vue";
import { useRoute } from "vue-router";
import request, { get, post } from "../../request/index.js";
import { defineAsyncComponent, ref, reactive, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import SvgIcon from "../../components/icon/index.js";
import { schemaToModel } from "../../utils/index.js";
import { listToTree, schemaToModel } from "../../utils/index.js";
import qs from "../../lib/qs/shim.js";
import AppFormInput from "../form/form-input.js";
import VueOfficeExcel from "@vue-office/excel";
import { camelCase, capitalize } from "lodash";
export default {
components: { AppForm, SvgIcon, AppFormInput, VueOfficeExcel },
name: "AppList",
components: {
AppForm: defineAsyncComponent(() => import("../form/index.js")),
SvgIcon: defineAsyncComponent(() => import("../../components/icon/index.js")),
AppFormInput: defineAsyncComponent(() => import("../form/form-input.js")),
VueOfficeExcel,
},
template: html`
<el-row>
<el-col>
<app-form
inline
mode="query"
label-position="left"
:schema="queryFromSchema"
v-model="data.query"
@submit="load"
:hideButton="true"
:isQueryForm="true"
/>
</el-col>
</el-row>
<el-row style="padding-bottom:20px;">
<el-col>
<template v-for="item in $route.meta.buttons">
<el-button
:class="item.meta.htmlClass??'el-button--primary'"
v-if="item.meta.isTop"
@click="click(item,selectedRows)"
>
<el-icon v-if="item.meta.icon"><svg-icon :name="item.meta.icon" /></el-icon>
<span>{{item.meta.title}}</span>
<div v-loading="tableLoading">
<el-row>
<el-col>
<app-form
inline
mode="query"
label-position="left"
:schema="querySchema"
v-model="queryModel"
@submit="load"
:hideButton="true"
:isQueryForm="true"
v-if="querySchema"
/>
</el-col>
</el-row>
<el-row style="padding-bottom:20px;">
<el-col>
<template v-for="item in buttons">
<el-button
:class="item.meta.htmlClass??'el-button--primary'"
v-if="item.meta.isTop"
@click="click(item,selectedRows)"
>
<el-icon v-if="item.meta.icon"><svg-icon :name="item.meta.icon" /></el-icon>
<span>{{item.meta.title}}</span>
</el-button>
</template>
<el-button @click="click('filter',selectedRows)">
<el-icon><ep-filter /></el-icon>
<span>{{$t('筛选')}}</span>
</el-button>
</template>
<slot name="tableButtons" :rows="selectedRows"></slot>
</el-col>
</el-row>
<el-row>
<el-col>
<el-scrollbar>
<el-table
ref="tableRef"
v-loading="tableLoading"
row-key="id"
table-layout="auto"
border
fit
:data="data.items"
@selection-change="handleSelectionChange"
@sort-change="sortChange"
:header-cell-class-name="getClass"
v-if="data.items"
>
<el-table-column fixed="left" type="selection" />
<el-table-column type="index" :label="$t('rowIndex')">
<template #default="scope"> {{ (data.pageIndex - 1) * data.pageSize + scope.$index + 1 }} </template>
</el-table-column>
<template v-for="(item,key) in tableSchema.items.properties">
<template v-if="key==='properties'">
<el-table-column :label="subKey" v-for="(subItem,subKey) in item.properties">
<template #default="scope">{{ scope.row[key][subKey] }} </template>
</el-table-column>
</template>
<template v-else-if="item.navigation">
<el-table-column :prop="key" :label="item.title">
<template #default="scope">{{getProp(scope.row,item.navigation)}}</template>
</el-table-column>
</template>
<template v-else>
<template v-if="showColumn(item,key)">
<el-table-column :prop="key" sortable="custom" :sort-orders="['descending', 'ascending', null]">
<template #header="scope">{{item.title}}</template>
<slot name="tableButtons" :rows="selectedRows"></slot>
</el-col>
</el-row>
<el-row>
<el-col>
<el-scrollbar>
<el-table
:key="tableKey"
ref="tableRef"
:tree-props="treeProps"
:data="tableData"
@selection-change="handleSelectionChange"
@sort-change="sortChange"
:header-cell-class-name="getClass"
row-key="id"
table-layout="auto"
border
fit
>
<el-table-column fixed="left" type="selection" />
<el-table-column type="index" :label="$t('rowIndex')">
<template #default="scope">
{{ (pageModel.pageIndex - 1) * pageModel.pageSize + scope.$index + 1 }}
</template>
</el-table-column>
<template v-for="(item,key) in config.table.schema.properties">
<template v-if="key==='properties'">
<el-table-column :label="subKey" v-for="(subItem,subKey) in item.properties">
<template #default="scope">{{ scope.row[key][subKey] }} </template>
</el-table-column>
</template>
<template v-else-if="item.navigation">
<el-table-column :prop="key" :label="item.title">
<template #default="scope">{{getProp(scope.row,item.navigation)}}</template>
</el-table-column>
</template>
<template v-else-if="item.oneToMany">
<el-table-column :prop="key" :label="item.title">
<template #default="scope">
<app-form-input :isReadOnly="true" :schema="item" :prop="key" v-model="scope.row" />
<el-link type="primary" @click="showList(scope.row[key],item.oneToMany)">
<app-form-input :isReadOnly="true" :schema="item" :prop="key" v-model="scope.row" />
</el-link>
</template>
</el-table-column>
</template>
</template>
</template>
<slot name="columns"></slot>
<el-table-column fixed="right">
<template #header>
<el-button @click="filterDrawer = true">
{{$t('operations')}}
<el-icon class="el-icon--right"><ep-filter /></el-icon>
</el-button>
</template>
<template #default="scope">
<div class="flex">
<template v-for="item in $route.meta.buttons">
<el-button
:class="item.meta.htmlClass??'el-button--primary'"
v-if="!item.meta.isTop"
@click="click(item,[scope.row])"
>
<el-icon v-if="item.meta.icon"><svg-icon :name="item.meta.icon" /></el-icon>
<span>{{item.meta.title}}</span>
</el-button>
<template v-else>
<template v-if="showColumn(item,key)">
<el-table-column :prop="key" sortable="custom" :sort-orders="['descending', 'ascending', null]">
<template #header="scope">{{item.title}}</template>
<template #default="scope">
<app-form-input :isReadOnly="true" :schema="item" :prop="key" v-model="scope.row" />
</template>
</el-table-column>
</template>
<slot name="rowButtons" :rows="[scope.row]"></slot>
</div>
</template>
</template>
</el-table-column>
</el-table>
</el-scrollbar>
</el-col>
</el-row>
<el-row>
<el-col>
<el-pagination
v-if="data.items&&data.pageSize<data.totalCount"
v-model:currentPage="data.pageIndex"
v-model:page-size="data.pageSize"
:total="data.totalCount"
:page-sizes="[20, 50, 100]"
class="justify-end"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
@size-change="onPageSizeChange"
@current-change="onPageIndexChange"
style="margin-top:20px"
/>
</el-col>
</el-row>
<slot name="columns"></slot>
<el-table-column fixed="right">
<template #header>
<el-button @click="filterDrawer = true">
{{$t('operations')}}
<el-icon class="el-icon--right"><ep-filter /></el-icon>
</el-button>
</template>
<template #default="scope">
<div class="flex">
<template v-for="item in buttons">
<el-button
:class="item.meta.htmlClass??'el-button--primary'"
v-if="!item.meta.isTop"
@click="click(item,[scope.row])"
>
<el-icon v-if="item.meta.icon"><svg-icon :name="item.meta.icon" /></el-icon>
<span>{{item.meta.title}}</span>
</el-button>
</template>
<slot name="rowButtons" :rows="[scope.row]"></slot>
</div>
</template>
</el-table-column>
</el-table>
</el-scrollbar>
</el-col>
</el-row>
<el-row>
<el-col>
<el-pagination
v-if="tableData.length&&pageModel.pageSize<pageModel.totalCount"
v-model:currentPage="pageModel.pageIndex"
v-model:page-size="pageModel.pageSize"
:total="pageModel.total"
:page-sizes="pageModel.sizeList"
class="justify-end"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
@size-change="onPageSizeChange"
@current-change="onPageIndexChange"
style="margin-top:20px"
/>
</el-col>
</el-row>
</div>
<el-drawer v-model="filterDrawer" destroy-on-close @close="tableRef.doLayout()">
<template #header> <span class="el-dialog__title"> {{$t('filter')}} </span> </template>
<el-scrollbar>
@ -151,30 +172,138 @@ export default {
</el-col>
</el-row>
</el-scrollbar>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="filterDrawer=false"> {{$t('confirm')}} </el-button>
</span>
</template>
</el-drawer>
<el-dialog v-model="dialogVisible" align-center destroy-on-close width="700">
<el-drawer v-model="subDrawer" destroy-on-close size="50%">
<el-scrollbar>
<app-list
v-if="subDrawer"
:controller="subListQuery.controller"
:query="subListQuery.query"
:buttons="subListQuery.buttons"
/>
</el-scrollbar>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="subDrawer=false"> {{$t('confirm')}} </el-button>
</span>
</template>
</el-drawer>
<el-dialog
v-model="dialogVisible"
align-center
destroy-on-close
style="width:auto;min-width:500px;max-width:1000px;"
>
<template #header> <span class="el-dialog__title"> {{editFormTitle}} </span> </template>
<el-row>
<el-col style="max-height:calc(100vh - 180px );">
<el-row v-loading="editFormloading">
<el-col style="max-height:calc(100vh - 180px );min-height:100%;">
<el-scrollbar>
<app-form
v-loading="editFormloading"
:disabled="editFormMode==='details'"
:mode="editFormMode"
ref="editFormRef"
inline
label-position="left"
:hideButton="true"
:schema="editFormSchema"
v-model="editFormModel"
v-if="editFormMode!=='import'"
/>
<template v-if="editFormMode==='create'||editFormMode==='update'||editFormMode==='details'">
<app-form
:disabled="editFormMode==='details'"
:mode="editFormMode"
ref="editFormRef"
inline
label-position="left"
:hideButton="true"
:schema="editFormSchema"
v-model="editFormModel"
v-if="editFormSchema&&editFormMode"
/>
</template>
<template v-else-if="editFormMode==='export'">
<el-form :model="exportModel">
<el-form-item :label="$t('全部')">
<el-switch v-model="exportModel.includeAll" />
</el-form-item>
<el-form-item :label="$t('已删除')">
<el-switch v-model="exportModel.includeDeleted" />
</el-form-item>
</el-form>
</template>
<template v-else-if="editFormMode==='import'">
<el-form :model="importModel" inline>
<el-form-item :label="$t('部分成功')">
<el-switch v-model="importModel.partial" />
</el-form-item>
<el-form-item :label="$t('全部替换')">
<el-switch v-model="importModel.replace" />
</el-form-item>
<el-form-item :label="$t('导入模板')">
<el-link type="primary" @click="getImportTemplate">{{$t('下载')}}</el-link>
</el-form-item>
<el-form-item :label="$t('文件')">
<el-upload
ref="uploadRef"
drag
accept=".xlsx"
:disabled="fileList.length===1"
:limit="1"
:auto-upload="false"
:on-change="handleChange"
>
<el-icon class="el-icon--upload"><ep-upload-filled /></el-icon>
</el-upload>
</el-form-item>
</el-form>
</template>
<template v-else-if="editFormMode==='filter'">
<el-form :model="queryList" inline class="filter">
<el-row v-for="(item,index) in queryList">
<el-col :span="6">
<el-select v-model="item.property" :placeholder="$t('字段')">
<el-option v-for="(value, prop) in querySchema.properties" :value="prop" :label="value.title" />
</el-select>
</el-col>
<el-col :span="6">
<el-select v-model="item.operator" :placeholder="$t('操作符')">
<el-option value="{0}=@0" :label="$t('等于')" />
<el-option value="{0}!=@0" :label="$t('不等于')" />
<el-option value="{0}>@0" :label="$t('大于')" />
<el-option value="{0}>=@0" :label="$t('大于等于')" />
<el-option value="{0}<@0" :label="$t('小于')" />
<el-option value="{0}<=@0" :label="$t('小于等于')" />
<el-option value="{0}.Contains(@0)" :label="$t('包含')" />
<el-option value="{0}.StartsWith(@0)" :label="$t('开始于')" />
<el-option value="{0}.StartsWith(@0)" :label="$t('结束于')" />
</el-select>
</el-col>
<el-col :span="6">
<el-input v-model="item.value" :placeholder="$t('值')" />
</el-col>
<!-- <el-col :span="4">
<el-select v-model="item.logic" :placeholder="$t('关系')">
<el-option value="and" :label="$t('且')" />
<el-option value="or" :label="$t('或')" />
</el-select>
</el-col> -->
<el-col :span="2">
<el-button circle @click="queryList.splice(index, 1)">
<template #icon>
<ep-close />
</template>
</el-button>
</el-col>
</el-row>
<el-row>
<el-col>
<el-button circle @click="pushQueryList">
<template #icon>
<ep-plus />
</template>
</el-button>
</el-col>
</el-row>
</el-form>
</template>
<template v-else>
<slot :name="editFormMode"></slot>
</template>
</el-scrollbar>
</el-col>
</el-row>
@ -185,25 +314,66 @@ export default {
</template>
</el-dialog>
`,
props: ["modelValue"],
styles: html`<style>
.el-form.filter .el-col {
padding: 5px;
}
</style>`,
props: ["modelValue", "config", "querySchema", "controller", "query", "buttons"],
emits: ["command"],
async setup(props, context) {
// 变量定义
//// 配置
const config = ref(props.config);
//// 分页
const pageModel = reactive({
sizeList: [20, 50, 100],
pageIndex: 1,
pageSize: 10,
total: 0,
});
const treeProps = reactive({
children: "children",
});
const tableKey = ref(false);
const tableRef = ref(null);
const uploadRef = ref(null);
const columns = ref([]);
const filterDrawer = ref(false);
const tableLoading = ref(false);
const subDrawer = ref(false);
const subListQuery = ref({});
const tableLoading = ref(true);
const selectedRows = ref([]);
const dialogVisible = ref(false);
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const baseUrl = `${route.meta.path}`.substring(1);
const indexUrl = `${baseUrl}/index`;
const vm = (await get(indexUrl)).data;
const schema = vm.schema;
const data = reactive(vm.model ?? schemaToModel(schema));
const buttons = ref(props.buttons ?? route.meta.children);
const baseUrl = props.controller ?? `${route.meta.path}`;
const indexUrl = props.indexUrl ?? `${baseUrl}/index`;
const queryModel = ref({});
const sortColumns = ref(new Map());
const querySchema = ref(props.querySchema);
const queryList = ref([]);
const tableSchema = ref({});
const tableData = ref([]);
const editFormRef = ref(null);
const editFormloading = ref(false);
const editFormMode = ref(null);
const editFormTitle = ref("");
const editFormSchema = ref(null);
const editFormModel = ref(null);
const exportModel = reactive({
includeAll: false,
includeDeleted: false,
});
const importModel = reactive({
partial: true,
replace: false,
});
const fileList = ref([]);
const getSortModel = (model) => {
const orderBy = model.orderBy
model.orderBy
.split(",")
.map((o) => o.trim())
.filter((o) => o)
@ -212,32 +382,30 @@ export default {
order: (o.split(" ").filter((o) => o)[1] ?? "asc") + "ending",
}))
.forEach((o) => sortColumns.value.set(o.prop, o.order));
return orderBy;
};
const sortModel = reactive(getSortModel(data));
const getColumns = (schema) => {
Object.keys(schema.properties).forEach((propertyName) => {
const property = schema.properties[propertyName];
if (property.type !== "object" && property.type !== "array" && !property.hidden && property.showForList) {
columns.value.push({ name: propertyName, title: property.title, checked: true });
}
});
};
const getClass = ({ row, column }) => {
if (column.property) {
column.order = sortColumns.value.get(column.property);
}
};
const sortChange = ({ column, prop, order }) => {
const sortChange = async ({ column, prop, order }) => {
if (order === null) {
sortColumns.value.delete(prop);
} else {
sortColumns.value.set(prop, order);
}
data.orderBy = Array.from(sortColumns.value)
queryModel.value.orderBy = Array.from(sortColumns.value)
.map((o) => capitalize(o[0]) + (o[1] === "ascending" ? "" : ` DESC`))
.join(",");
load(indexUrl);
};
const getColumns = (schema) => {
Object.keys(schema.properties).forEach((propertyName) => {
const property = schema.properties[propertyName];
if (property.type !== "object" && property.type !== "array" && !property.hidden) {
columns.value.push({ name: propertyName, title: property.title, checked: true });
}
});
await load(indexUrl);
};
const showColumn = (item, prop) => {
return (
@ -247,63 +415,62 @@ export default {
columns.value.findIndex((o) => o.name === prop && o.checked) >= 0
);
};
getColumns(schema.properties.query);
const queryFromSchema = schema.properties.query;
const tableSchema = schema.properties.items;
const editFormRef = ref(null);
const editFormloading = ref(false);
const editFormMode = ref(null);
const editFormTitle = ref("");
const editFormSchema = reactive({});
const editFormModel = reactive({});
const exportModel = reactive({
includeAll: false,
includeDeleted: false,
});
const handleSelectionChange = (rows) => (selectedRows.value = rows);
const load = async (url) => {
tableLoading.value = true;
try {
const postData = JSON.parse(JSON.stringify(data));
delete postData["Id"];
delete postData["items"];
Object.assign(data, (await post(url, postData)).data);
const url = config.value.query.url;
const postData = JSON.parse(JSON.stringify(queryModel.value));
postData.filters = queryList.value.filter((o) => o.property && o.value);
if (postData.items) {
delete postData["items"];
}
if (postData.query?.id) {
delete postData.query["id"];
}
const listData = (await request(url, postData, { method: config.value.query.method.toUpperCase() })).data;
const items = listData.items;
if (tableSchema.value.isTree) {
items = listToTree(listData.items);
}
tableData.value = items;
//data.value = listData;
queryModel.tableKey.value = !tableKey.value;
} catch (error) {
console.log(error);
} finally {
tableLoading.value = false;
}
};
const onPageIndexChange = () => load(indexUrl);
const onPageSizeChange = () => load(indexUrl);
const onPageIndexChange = async () => {
await load(indexUrl);
};
const onPageSizeChange = async () => await load(indexUrl);
const click = async (item, rows) => {
editFormMode.value = item.path;
editFormloading.value = true;
editFormMode.value = item.path ?? item;
context.emit("command", item, rows);
if (item.path === "index") {
//list
await load(indexUrl);
} else if (item.path === "details") {
//details
const detailsUrl = `${baseUrl}/${item.path}?${qs.stringify({ id: rows[0].id })}`;
Object.assign(editFormSchema, schema.properties.items.items);
Object.assign(editFormModel, (await post(detailsUrl)).data);
editFormTitle.value = `${t("details")}${schema.title}`;
const url = `${baseUrl}/${item.path}?${qs.stringify({ id: rows[0].id })}`;
editFormSchema.value = (await get(url)).data;
editFormModel.value = (await post(url)).data;
editFormTitle.value = `${querySchema.value?.title}${t("details")}`;
dialogVisible.value = true;
} else if (item.path === "create") {
} else if (item.path === "create" || item.path === "update") {
//create
const url = `${baseUrl}/${item.path}`;
let url = `${baseUrl}/${item.path}`;
if (item.path === "update") {
url = `${url}?${qs.stringify({ id: rows[0].id })}`;
}
const vm = (await get(url)).data;
Object.assign(editFormSchema, vm.schema);
Object.assign(editFormModel, vm.model);
editFormTitle.value = `${t("create")}${schema.title}`;
dialogVisible.value = true;
} else if (item.path === "update") {
//update
const url = `${baseUrl}/${item.path}`;
const vm = (await get(url, { id: rows[0].id })).data;
Object.assign(editFormSchema, vm.schema);
Object.assign(editFormModel, vm.model);
editFormTitle.value = `${t("update")}${schema.title}`;
editFormSchema.value = vm.schema;
editFormModel.value = vm.model;
editFormTitle.value = `${t(item.path)}${querySchema.value?.title}`;
dialogVisible.value = true;
} else if (item.path === "delete") {
//delete
@ -318,15 +485,19 @@ export default {
await load(indexUrl);
} else if (item.path === "export") {
//export
const url = `${baseUrl}/${item.path}`;
const exportUrl = `${url}?${qs.stringify(exportModel)}`;
await load(exportUrl);
editFormTitle.value = `${t(item.path)}${querySchema.value?.title}`;
dialogVisible.value = true;
} else if (item.path === "import") {
//import
const url = `${baseUrl}/${item.path}`;
editFormTitle.value = `${t("import")}${schema.title}`;
editFormTitle.value = `${t(item.path)}${querySchema.value?.title}`;
fileList.value = [];
dialogVisible.value = true;
} else if (item === "filter") {
editFormTitle.value = t("自定义查询");
dialogVisible.value = true;
}
editFormloading.value = false;
};
const submit = async () => {
if (editFormMode.value === "create" || editFormMode.value === "update") {
@ -335,7 +506,7 @@ export default {
if (valid) {
editFormloading.value = true;
const url = `${baseUrl}/${editFormMode.value}`;
const result = await post(url, editFormModel);
const result = await post(url, editFormModel.value);
if (result.errors) {
model.errors = result.errors; //??
} else {
@ -345,46 +516,154 @@ export default {
}
}
} catch (error) {
console.error(error);
console.log(error);
} finally {
editFormloading.value = false;
}
} else if (editFormMode.value === "details") {
load(indexUrl);
dialogVisible.value = false;
editFormMode.value = null;
} else if (editFormMode.value === "export") {
const postData = JSON.parse(JSON.stringify(queryModel.value));
postData.filters = queryList.value.filter((o) => o.property && o.value);
delete postData.query["items"];
delete postData.query["id"];
const url = `${baseUrl}/${editFormMode.value}?${qs.stringify(exportModel)}`;
const response = await post(url, postData);
download(response);
dialogVisible.value = false;
} else if (editFormMode.value === "import") {
editFormloading.value = true;
const url = `${baseUrl}/${editFormMode.value}`;
const formData = new FormData();
formData.append("partial", importModel.partial);
formData.append("replace", importModel.replace);
formData.append("file", fileList.value[0]?.raw);
console.log(uploadRef.value);
const response = await post(url, formData);
editFormloading.value = false;
dialogVisible.value = false;
await load(indexUrl);
} else if (editFormMode.value === "filter") {
await load(indexUrl);
dialogVisible.value = false;
}
};
await load(indexUrl);
const showList = (value, nav) => {
if (!subDrawer.value) {
const controller = nav.substr(0, nav.lastIndexOf(".")).toLowerCase();
const findRoute = (tree) => {
for (const item of tree) {
if (item.meta.controller === controller) {
return item;
}
if (item.children) {
return findRoute(item.children);
}
}
};
const targetRoute = router.getRoutes().find((o) => o.meta?.controller === controller);
subListQuery.value = {
controller,
query: { [camelCase(nav.substr(nav.lastIndexOf(".") + 1))]: value },
buttons: targetRoute.meta.buttons,
};
subDrawer.value = true;
}
};
const pushQueryList = () => {
queryList.value.push({
property: "",
operator: "{0}=@0",
value: "",
logic: "and",
});
};
const download = (response) => {
const downloadUrl = window.URL.createObjectURL(response.data);
const filename = response.filename;
let link = document.createElement("a");
link.href = downloadUrl;
link.download = filename;
link.click();
window.URL.revokeObjectURL(downloadUrl);
};
const getImportTemplate = async () => {
const url = `${baseUrl}/${editFormMode.value}`;
const response = await get(url);
download(response);
};
const handleChange = (uploadFile, uploadFiles) => {
fileList.value = uploadFiles;
};
onMounted(async () => {
pushQueryList();
// if (!querySchema.value) {
// const vm = (await get(indexUrl)).data;
// querySchema.value = vm.schema.properties.query;
// tableSchema.value = vm.schema.properties.items.items;
// data.value = vm.model ?? schemaToModel(vm.schema);
// if (props.query) {
// Object.assign(data.value.query, props.query);
// }
// getSortModel(data.value);
// getColumns(vm.schema.properties.query);
// }
if (!config.value) {
//
}
getColumns(config.value.table.schema);
queryModel.value = schemaToModel(config.value.query.schema);
if (props.query) {
Object.assign(queryModel.value.query, props.query);
}
// getSortModel(data.value);
// getColumns(vm.schema.properties.query);
await load(indexUrl);
});
return {
route,
config,
queryModel,
pageModel,
treeProps,
tableKey,
tableRef,
uploadRef,
tableLoading,
columns,
showColumn,
filterDrawer,
subDrawer,
dialogVisible,
selectedRows,
schema,
queryFromSchema,
querySchema,
queryList,
tableSchema,
data,
sortModel,
buttons,
tableData,
getClass,
sortChange,
getProp,
getImportTemplate,
editFormRef,
editFormloading,
editFormMode,
editFormTitle,
editFormSchema,
editFormModel,
exportModel,
importModel,
onPageSizeChange,
onPageIndexChange,
handleSelectionChange,
load,
click,
submit,
showList,
subListQuery,
pushQueryList,
fileList,
handleChange,
};
},
};

36
code/WebApp/vanilla/components/markdown/index.js

@ -1,14 +1,18 @@
import { ref, onMounted } from 'vue';
import { marked, setOptions } from '../../lib/marked/marked.esm.js';
import mermaid from '../../lib/mermaid/mermaid.esm.min.mjs';
import hljs from '../../lib/highlightjs/highlight.min.js';
import html from "html";
import { ref, onMounted } from "vue";
import { marked, setOptions } from "../../lib/marked/marked.esm.js";
import mermaid from "../../lib/mermaid/mermaid.esm.min.mjs";
import hljs from "../../lib/highlightjs/highlight.min.js";
export default {
template: `<div ref="tplRef"><div class="source" style="display:none;"><slot /></div><div class="markdown-body"></div></template>`,
template: html`<div ref="tplRef">
<div class="source" style="display:none;"><slot /></div>
<div class="markdown-body"></div>
</div>`,
props: {
name: {
default: null
}
default: null,
},
},
setup(props) {
const tplRef = ref(null);
@ -17,25 +21,25 @@ export default {
onMounted(async () => {
setOptions({
highlight: function (code, lang) {
if (lang === 'mermaid') {
if (lang === "mermaid") {
return mermaid.mermaidAPI.render(`mermaid${id++}`, code, undefined);
} else {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
const language = hljs.getLanguage(lang) ? lang : "plaintext";
return hljs.highlight(code, { language }).value;
}
},
langPrefix: 'hljs language-',
langPrefix: "hljs language-",
});
let mdText = tplRef.value.querySelector('.source').innerText;
let mdText = tplRef.value.querySelector(".source").innerText;
if (props.name !== null) {
const response = await fetch(`./assets/docs/${props.name}.md`);
mdText = await response.text();
}
tplRef.value.querySelector('.markdown-body').innerHTML = marked(mdText);
tplRef.value.querySelector('.source').remove();
tplRef.value.querySelector(".markdown-body").innerHTML = marked(mdText);
tplRef.value.querySelector(".source").remove();
});
return {
tplRef
tplRef,
};
}
}
},
};

13
code/WebApp/vanilla/index.html

@ -3,9 +3,7 @@
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<style>
@keyframes loading-rotate {
@ -29,6 +27,7 @@
margin: auto;
width: 50px;
height: 50px;
color: #409eff;
}
</style>
<link rel="stylesheet" href="./main.css" />
@ -37,7 +36,13 @@
<body>
<div id="app"></div>
<img src="./assets/icons/loading.svg" id="loading" class="loading">
<i id="loading" class="loading">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M512 64a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32zm0 640a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V736a32 32 0 0 1 32-32zm448-192a32 32 0 0 1-32 32H736a32 32 0 1 1 0-64h192a32 32 0 0 1 32 32zm-640 0a32 32 0 0 1-32 32H96a32 32 0 0 1 0-64h192a32 32 0 0 1 32 32zM195.2 195.2a32 32 0 0 1 45.248 0L376.32 331.008a32 32 0 0 1-45.248 45.248L195.2 240.448a32 32 0 0 1 0-45.248zm452.544 452.544a32 32 0 0 1 45.248 0L828.8 783.552a32 32 0 0 1-45.248 45.248L647.744 692.992a32 32 0 0 1 0-45.248zM828.8 195.264a32 32 0 0 1 0 45.184L692.992 376.32a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0zm-452.544 452.48a32 32 0 0 1 0 45.248L240.448 828.8a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0z">
</path>
</svg>
</i>
<script type="importmap">
{
"imports": {

13
code/WebApp/vanilla/layouts/header.js

@ -1,18 +1,21 @@
import html from "html";
import { ref, onMounted, onUnmounted } from "vue";
import { defineAsyncComponent, ref, onMounted, onUnmounted } from "vue";
import { useAppStore } from "../store/index.js";
import SvgIcon from "../components/icon/index.js";
import LayoutLogo from "./logo.js";
import { useDark, useFullscreen, useToggle } from "@vueuse/core";
import { ElMessage, ElMessageBox } from "element-plus";
import { useI18n } from "vue-i18n";
import { logout } from "../api/user.js";
import LayoutLocale from "./locale.js";
import router from "../router/index.js";
import { treeToList } from "../utils/index.js";
export default {
components: { SvgIcon, LayoutLogo, LayoutLocale, ElMessage, ElMessageBox },
components: {
SvgIcon: defineAsyncComponent(() => import("../components/icon/index.js")),
LayoutLogo: defineAsyncComponent(() => import("./logo.js")),
LayoutLocale: defineAsyncComponent(() => import("./locale.js")),
ElMessage,
ElMessageBox,
},
template: html`
<div class="flex items-center justify-between">
<div class="flex items-center justify-center">

26
code/WebApp/vanilla/layouts/index.js

@ -1,14 +1,14 @@
import html from "html";
import LayoutHeader from "./header.js";
import LayoutMenu from "./menu.js";
import LayoutTabs from "./tabs.js";
import LayoutFooter from "./footer.js";
import Icon from "../components/icon/index.js";
import { useAppStore } from "../store/index.js";
import { computed } from "vue";
import { defineAsyncComponent, computed } from "vue";
export default {
components: { Icon, LayoutHeader, LayoutMenu, LayoutTabs, LayoutFooter },
components: {
LayoutHeader: defineAsyncComponent(() => import("./header.js")),
LayoutMenu: defineAsyncComponent(() => import("./menu.js")),
LayoutTabs: defineAsyncComponent(() => import("./tabs.js")),
LayoutFooter: defineAsyncComponent(() => import("./footer.js")),
},
template: html`<el-container>
<el-header><layout-header /></el-header>
<el-container>
@ -20,17 +20,9 @@ export default {
<layout-tabs />
<el-main>
<router-view v-if="!isRefreshing" v-slot="{ Component, route }">
<component
:is="Component"
v-if="!appStore.isUseTabsRouter || !route.meta?.cached"
:key="$route.fullPath"
/>
<component :is="Component" v-if="route.meta?.disableCaching" :key="$route.fullPath" />
<keep-alive>
<component
:is="Component"
v-if="appStore.isUseTabsRouter && route.meta?.cached"
:key="route.fullPath"
/>
<component :is="Component" v-if="!route.meta?.disableCaching" :key="route.fullPath" />
</keep-alive>
</router-view>
</el-main>

7
code/WebApp/vanilla/layouts/locale.js

@ -1,13 +1,14 @@
import html from "html";
import { defineAsyncComponent } from "vue";
import { useAppStore } from "../store/index.js";
import { useI18n } from "vue-i18n";
import Icon from "../components/icon/index.js";
export default {
components: { Icon },
components: { SvgIcon: defineAsyncComponent(() => import("../components/icon/index.js")) },
template: html`<el-dropdown class="cursor-pointer" v-if="appStore.settings.enableLocale">
<span class="el-dropdown-link flex">
<el-icon :size="18">
<icon name="lang" />
<svg-icon name="lang" />
</el-icon>
</span>
<template #dropdown>

20
code/WebApp/vanilla/layouts/menu-item.js

@ -1,28 +1,32 @@
import html from "html";
import { reactive, watch } from "vue";
import Icon from "../components/icon/index.js";
import { defineAsyncComponent, reactive, watch } from "vue";
import { useRouter } from "vue-router";
export default {
name: "menuItem",
components: { Icon },
template: html`<el-sub-menu :index="modelValue.meta.path" v-if="modelValue.children">
components: { SvgIcon: defineAsyncComponent(() => import("../components/icon/index.js")) },
template: html`<template v-if="!modelValue.meta.isHidden">
<el-sub-menu
:index="modelValue.meta.path"
v-if="modelValue.children&&modelValue.children.some(o=>!o.meta.isHidden)"
>
<template #title>
<el-icon><icon :name="modelValue.meta.icon??'folder'" /></el-icon>
<el-icon><svg-icon :name="modelValue.meta.icon??'folder'" /></el-icon>
<span>{{modelValue.meta.title}}</span>
</template>
<menu-item v-for="item in modelValue.children" v-model="item" />
</el-sub-menu>
<el-menu-item
v-else
v-else-if="modelValue.meta.type==='page'"
:index="modelValue.meta.isExternal?null:modelValue.meta.path"
@click.native="click(modelValue)"
>
<el-icon><icon :name="modelValue.meta.icon??file" /></el-icon>
<el-icon><svg-icon :name="modelValue.meta.icon??file" /></el-icon>
<template #title>
<span>{{modelValue.meta.title}}</span>
</template>
</el-menu-item>`,
</el-menu-item>
</template>`,
props: {
modelValue: {
typeof: Object,

2
code/WebApp/vanilla/layouts/menu.js

@ -15,7 +15,7 @@ export default {
</el-menu>`,
setup() {
const appStore = useAppStore();
const menus = router.getRoutes().find((o) => o.name === "layout").children;
const menus = router.getRoutes().find((o) => o.path === "/").children;
return {
appStore,
menus,

35
code/WebApp/vanilla/layouts/tabs.js

@ -1,12 +1,10 @@
import html from "html";
import { ref, nextTick } from "vue";
import { defineAsyncComponent, ref, nextTick, getCurrentInstance } from "vue";
import { useRoute, onBeforeRouteUpdate, useRouter } from "vue-router";
import Icon from "../components/icon/index.js";
import { useAppStore } from "../store/index.js";
import MenuItem from "./menu-item.js";
export default {
components: { Icon, MenuItem },
components: { SvgIcon: defineAsyncComponent(() => import("../components/icon/index.js")) },
template: html`<el-tabs
v-model="model"
type="border-card"
@ -24,23 +22,24 @@ export default {
@visible-change="showContextMenu(index, $event)"
>
<span class="inline-flex items-center">
<el-icon><icon v-if="item.meta.icon" :name="item.meta.icon" /></el-icon>
<el-icon><svg-icon v-if="item.meta.icon" :name="item.meta.icon" /></el-icon>
{{ item.meta?.title ?? item.fullPath }}
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="refresh(index)"><i-ep-refresh />刷新</el-dropdown-item>
<el-dropdown-item @click="refresh(index)">
<el-icon><ep-refresh /></el-icon><span></span>
</el-dropdown-item>
<el-dropdown-item :disabled="index === 0" @click="removeLeft(index)">
<i-ep-back />关闭左侧
<el-icon><ep-back /></el-icon><span></span>
</el-dropdown-item>
<el-dropdown-item :disabled="index === appStore.routes.length - 1" @click="removeRight(index)">
<i-ep-right />关闭右侧
<el-icon><ep-right /></el-icon><span></span>
</el-dropdown-item>
<el-dropdown-item
:disabled="index === 0 && index === appStore.routes.length - 1"
@click="removeOthers(index)"
>
<i-ep-switch />
><el-icon><ep-switch /></el-icon><span></span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
@ -66,6 +65,10 @@ export default {
.router-tab .el-tabs__content {
display: none;
}
.router-tab .el-icon {
margin-right: 5px;
}
</style>
`,
setup() {
@ -108,11 +111,17 @@ export default {
});
};
const deleteItem = (start, end) => {
appStore.routes.splice(start, end);
const vue = getCurrentInstance();
console.log(vue);
};
const remove = (name) => {
if (appStore.routes.length > 1) {
const index = appStore.routes.findIndex((o) => o.fullPath === name);
const currentIndex = appStore.routes.findIndex((o) => o.fullPath === currentRoute.fullPath);
appStore.routes.splice(index, 1);
deleteItem(index, 1);
if (index === currentIndex) {
if (appStore.routes[index]) {
router.push(appStore.routes[index]);
@ -126,7 +135,7 @@ export default {
const removeLeft = (index) => {
const currentIndex = appStore.routes.findIndex((o) => o.fullPath === currentRoute.fullPath);
const route = appStore.routes[index];
appStore.routes.splice(0, index);
deleteItem(0, index);
if (currentIndex < index) {
router.push(route);
}
@ -134,7 +143,7 @@ export default {
const removeRight = (index) => {
const currentIndex = appStore.routes.findIndex((o) => o.fullPath === currentRoute.fullPath);
appStore.routes.splice(index + 1, appStore.routes.length - index);
deleteItem(index + 1, appStore.routes.length - index);
if (currentIndex > index) {
router.push(appStore.routes[index]);
}

36
code/WebApp/vanilla/lib/element-plus/index.full.min.mjs

File diff suppressed because one or more lines are too long

2
code/WebApp/vanilla/main.js

@ -1,5 +1,5 @@
import { createApp } from "vue";
import style from './mixins/style.js';
import style from "./mixins/style.js";
import store, { useAppStore } from "./store/index.js";
import router from "./router/index.js";
import ElementPlus from "element-plus";

52
code/WebApp/vanilla/models/login.js

@ -0,0 +1,52 @@
export default function () {
return {
title: "登录",
type: "object",
properties: {
username: {
title: "用户名",
type: "string",
rules: [
{
required: true,
message: "用户名不能为空",
},
{
max: 64,
message: "用户名的最大长度为 64",
},
],
},
password: {
title: "密码",
type: "string",
format: "password",
rules: [
{
required: true,
message: "密码不能为空",
},
{
max: 64,
message: "密码的最大长度为 64",
},
{
message: "DataTypeAttribute",
},
],
},
client_id: {
default: "basic-web",
hidden: true,
},
grant_type: {
default: "password",
hidden: true,
},
scope: {
default: "WebAppGateway BaseService",
hidden: true,
},
},
};
}

50
code/WebApp/vanilla/models/role.js

@ -0,0 +1,50 @@
const schema = {
title: "角色",
type: "object",
properties: {
name: {
title: "角色名",
type: "string",
showForList: true,
rules: [
{
required: true,
},
{
max: 64,
message: "用户名的最大长度为 64",
},
],
},
},
};
export default function () {
return {
query: {
url: "identity/roles",
method: "get",
schema: {
title: "用户",
type: "object",
properties: {
filter: {
title: "角色名",
type: "string",
},
skipCount: {
hidden: true,
default: 0,
},
maxResultCount: {
hidden: true,
default: 10,
},
},
},
},
table: {
schema: schema,
},
};
}

98
code/WebApp/vanilla/models/user.js

@ -0,0 +1,98 @@
const schema = {
title: "用户",
type: "object",
properties: {
userName: {
title: "用户名",
type: "string",
showForList: true,
rules: [
{
required: true,
},
{
max: 64,
message: "用户名的最大长度为 64",
},
],
},
phoneNumber: {
title: "电话",
type: "string",
input: "password",
showForList: true,
rules: [
{
required: true,
},
],
},
name: {
title: "姓名",
type: "string",
showForList: true,
rules: [
{
required: true,
},
],
},
email: {
title: "邮箱",
type: "string",
showForList: true,
rules: [
{
required: true,
},
],
},
roleNames: {
title: "角色",
type: "array",
input: "select",
multiple: true,
},
password: {
title: "密码",
type: "string",
input: "password",
rules: [
{
required: true,
message: "密码不能为空",
},
],
},
},
};
export default function () {
return {
query: {
url: "base/user",
method: "get",
schema: {
title: "用户",
type: "object",
properties: {
filter: {
title: "用户名",
type: "string",
},
skipCount: {
hidden: true,
default: 0,
},
maxResultCount: {
hidden: true,
default: 10,
},
},
},
},
table: {
schema: schema,
},
};
}

72
code/WebApp/vanilla/request/index.js

@ -1,8 +1,13 @@
import qs from "../lib/qs/shim.js";
import { isLogin } from "../api/user.js";
import { useAppStore } from "../store/index.js";
import { getFileName } from "../utils/index.js";
import settings from "../config/settings.js";
// const requestSettings = {
// baseURL: "/api",
// };
const addToken = async (options) => {
if (await isLogin()) {
const appStore = useAppStore();
@ -17,6 +22,15 @@ const getUrl = (url) => {
}
let result = settings.baseURL;
return (result += `/${url}`);
// let result = requestSettings.baseURL;
// if (withoutCulture) {
// result += `/${url}`;
// } else {
// const appStore = useAppStore();
// result += withoutCulture ? "/" : `/${appStore.localization.locale}/`;
// result += url;
// }
// return result;
};
const getResult = async (response) => {
@ -34,22 +48,26 @@ const getResult = async (response) => {
message: messages.get(response.status),
};
if (response.status == 200) {
result.data = await response.json();
const contentType = response.headers.get("Content-Type");
if (contentType.indexOf("application/json") > -1) {
result.data = await response.json();
} else if (contentType === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
result.data = await response.blob();
result.filename = getFileName(response.headers.get("Content-Disposition"));
}
} else if (response.status === 400 || response.status === 500) {
result.errors = await response.json();
}
return result;
};
const get = async (url, data, options, withoutToken = false, withoutCulture = false) => {
url = getUrl(url, withoutCulture);
const get = async (url, data, options, withoutToken = false) => {
url = getUrl(url);
if (data) {
url = `${url}?${qs.stringify(data)}`;
}
const defaultOptions = {
headers: {
"Accept-Language": "zh-Hans",
},
headers: { "Accept-Language": "zh-Hans" },
};
if (options) {
Object.assign(defaultOptions, options);
@ -61,23 +79,25 @@ const get = async (url, data, options, withoutToken = false, withoutCulture = fa
return getResult(response);
};
const post = async (url, data, options, withoutToken = false, withoutCulture = false) => {
url = getUrl(url, withoutCulture);
const defaultOptions = {
const post = async (url, data, options, withoutToken = false) => {
url = getUrl(url);
let defaultOptions = {
method: "POST",
headers: {
"Accept-Language": "zh-Hans",
"Content-Type": "application/json",
},
headers: { "Accept-Language": "zh-Hans" },
};
if (!(data instanceof FormData)) {
defaultOptions.headers["Content-Type"] = "application/json";
}
if (options) {
Object.assign(defaultOptions, options);
}
if (data && !defaultOptions.body) {
if (defaultOptions.headers["Content-Type"] === "application/x-www-form-urlencoded") {
if (defaultOptions.headers["Content-Type"]?.startsWith("application/x-www-form-urlencoded")) {
defaultOptions.body = qs.stringify(data);
} else {
} else if (defaultOptions.headers["Content-Type"]?.startsWith("application/json")) {
defaultOptions.body = JSON.stringify(data);
} else {
defaultOptions.body = data;
}
}
if (!withoutToken) {
@ -87,4 +107,24 @@ const post = async (url, data, options, withoutToken = false, withoutCulture = f
return getResult(response);
};
export { getUrl, get, post };
async function request(url, data, options, withoutToken = false) {
url = getUrl(url);
let defaultOptions = {
method: "POST",
headers: { "Accept-Language": "zh-Hans" },
};
if (options) {
Object.assign(defaultOptions, options);
}
if (defaultOptions.method !== "GET" && !(data instanceof FormData)) {
defaultOptions.headers["Content-Type"] = "application/json";
}
if (!withoutToken) {
await addToken(defaultOptions);
}
const response = await fetch(url, defaultOptions);
return getResult(response);
}
export default request;
export { get, post };

114
code/WebApp/vanilla/router/index.js

@ -1,3 +1,4 @@
import { defineAsyncComponent } from "vue";
import { createRouter, createWebHashHistory } from "vue-router";
import { useTitle } from "@vueuse/core";
import NProgress from "../lib/nprogress/nprogress.vite-esm.js";
@ -5,30 +6,17 @@ import { isLogin, hasPermission } from "../api/user.js";
import { useAppStore } from "../store/index.js";
import { listToTree } from "../utils/index.js";
import { connection, connect } from "../signalr/index.js";
import remoteRoutes from "./routes.js";
NProgress.configure({ showSpinner: false });
const routes = [
{
path: "/",
redirect: "/home",
component: () => import("../layouts/index.js"),
children: [
{
path: "home",
component: () => import("../views/home.js"),
meta: {
title: "首页",
icon: "home",
},
},
],
},
{
path: "/login",
component: () => import("../views/login.js"),
meta: {
title: "登录",
isHidden: true,
},
},
{
@ -36,6 +24,7 @@ const routes = [
component: () => import("../views/403.js"),
meta: {
title: "权限不足",
isHidden: true,
},
},
{
@ -43,6 +32,7 @@ const routes = [
component: () => import("../views/404.js"),
meta: {
title: "无法找到",
isHidden: true,
},
},
];
@ -53,14 +43,13 @@ const router = createRouter({
});
router.beforeEach(async (to, from, next) => {
console.log(`before: ${from.path} -> ${to.path}`);
NProgress.start();
try {
if (to.path !== "/login") {
if (!(await isLogin())) {
next({ path: "/login", query: { redirect: to.fullPath } });
} else {
if (!hasPermission(to)) {
if (!to.meta.public && to.meta.hasPermission === false) {
next({ path: "/403", query: { redirect: to.fullPath } });
} else {
next();
@ -74,64 +63,67 @@ router.beforeEach(async (to, from, next) => {
}
});
router.afterEach((to, from) => {
console.log(`after: ${from.path} -> ${to.path}`);
router.afterEach((to) => {
try {
if (to.meta.title) {
useTitle().value = `${to.meta.title}`;
}
if (to.fullPath.startsWith("/")) {
if (!to.meta.isHidden) {
const appStore = useAppStore();
appStore.add(to);
}
if (to.meta.title) {
useTitle().value = `${to.meta.title}`;
}
to.meta.cache = new Map();
} finally {
NProgress.done();
}
});
const reset = async (list, parent = null) => {
return list.map(async (o) => {
const item = {
path: o.path,
meta: o.meta,
};
if (o.component && typeof o.component === "string" && o.component !== "Layout") {
try {
item.component = await import(`../views/${o.component}.js`);
} catch (error) {
item.component = await import(`../views/list.js`);
}
}
item.meta.path = `${parent === null ? "" : parent.meta.path + "/"}${item.path}`;
item.meta.fullName = `${parent === null ? "" : parent.meta.title + " > "}${item.meta.title}`;
if (o.type === "Resource") {
if (o.children.length) {
item.meta.buttons = o.children.map((b) => {
return {
path: b.path,
meta: b.meta,
};
});
}
} else if (o.type !== "Operation" && o.children?.length) {
item.children = reset(o.children, item);
}
return item;
});
};
const refreshRouter = async () => {
await connect();
//await connect();
const appStore = useAppStore();
const permissions = appStore.user.permissions.filter((o) => !o.isHidden);
const serverRoutes = await reset(permissions);
const route = {
const permissions = appStore.user.permissions;
const serverRoutes = JSON.parse(JSON.stringify(remoteRoutes));
const setPermission = (list, parent = null) => {
list.forEach((o) => {
// full path
o.meta.path = `${parent === null ? "/" : parent.meta.path + "/"}${o.path}`;
// full name
o.meta.fullName = `${parent === null ? "" : parent.meta.title + " > "}${o.meta.title}`;
// permission
if (o.meta.type === "page" || o.meta.type === "button") {
if (!o.meta.public) {
o.meta.hasPermission = !!permissions[o.meta.permission];
}
}
// component
if (o.meta?.type === "page") {
if (!o.component) {
o.component = o.path;
}
if (o.component.constructor === String) {
o.component = () => import(`../views${o.meta.path}.js`);
}
}
// children
if (o.children?.length) {
setPermission(o.children, o);
if (o.meta.type === "page") {
o.meta.children = o.children;
delete o.children;
}
}
});
};
setPermission(serverRoutes);
router.removeRoute("layout");
const layout = {
name: "layout",
path: "",
path: "/",
redirect: "/home",
component: () => import("../layouts/index.js"),
children: serverRoutes,
};
router.removeRoute("layout");
router.addRoute("/", route);
router.addRoute("/", layout);
};
export default router;
export { refreshRouter };

101
code/WebApp/vanilla/router/routes.js

@ -0,0 +1,101 @@
export default [
{
path: "home",
meta: {
type: "page",
title: "首页",
icon: "home",
public: true,
},
},
{
path: "base-data",
meta: {
type: "group",
title: "基础数据",
icon: "folder",
},
children: [
{
path: "user",
meta: {
type: "page",
title: "用户管理",
icon: "file",
permission: "AbpIdentity.Users",
},
children: [
{
path: "create",
meta: {
type: "button",
title: "新建",
icon: "file",
permission: "AbpIdentity.Users.Create",
isTop: true,
},
},
{
path: "update",
meta: {
type: "button",
title: "编辑",
icon: "file",
htmlClass: "el-button--primary",
permission: "AbpIdentity.Users.Update",
},
},
{
path: "delete",
meta: {
type: "button",
title: "删除",
icon: "file",
permission: "AbpIdentity.Users.Delete",
},
},
],
},
{
path: "role",
meta: {
type: "page",
title: "角色管理",
icon: "file",
permission: "AbpIdentity.Users",
},
children: [
{
path: "create",
meta: {
type: "button",
title: "新建",
icon: "file",
permission: "AbpIdentity.Users.Create",
isTop: true,
},
},
{
path: "update",
meta: {
type: "button",
title: "编辑",
icon: "file",
htmlClass: "el-button--primary",
permission: "AbpIdentity.Users.Update",
},
},
{
path: "delete",
meta: {
type: "button",
title: "删除",
icon: "file",
permission: "AbpIdentity.Users.Delete",
},
},
],
},
],
},
];

33
code/WebApp/vanilla/signalr/index.js

@ -7,27 +7,26 @@ let connectionId = null;
const connection = new signalR.HubConnectionBuilder()
.withUrl("./api/hub", {
accessTokenFactory: () => {
const appStore = useAppStore();
return appStore.token;
return useAppStore().token;
},
})
.withAutomaticReconnect()
//.withAutomaticReconnect()
.build();
const connect = async () => {
return;
if (await isLogin()) {
if (connection.state === signalR.HubConnectionState.Disconnected) {
connection
.start()
.then(function () {
console.log("signalr connected");
})
.catch(function (error) {
console.error(error);
//setTimeout(connect, 5000);
});
}
}
// if (await isLogin()) {
// if (connection.state === signalR.HubConnectionState.Disconnected) {
// connection
// .start()
// .then(function () {
// console.log("signalr connected");
// })
// .catch(async function (error) {
// console.log(error);
// await isLogin();
// setTimeout(connect, 5000);
// });
// }
// }
};
connection.onclose(async () => {
await connect();

7
code/WebApp/vanilla/store/app.js

@ -1,6 +1,6 @@
import { defineStore } from "pinia";
import settings from "../config/settings.js";
import { getUser, isLogin } from "../api/user.js";
import { getAccessToken, getUser, isLogin } from "../api/user.js";
import { get, post } from "../request/index.js";
import { refreshRouter } from "../router/index.js";
import { getLocalizationAsync } from "../api/site.js";
@ -12,6 +12,7 @@ const useAppStore = defineStore("app", {
isMenuCollapse: false,
isRefreshing: false,
routes: [],
cache: new Map(),
};
const localSettings = JSON.parse(localStorage.getItem("settings") ?? "{}");
Object.assign(state.settings, localSettings);
@ -20,6 +21,10 @@ const useAppStore = defineStore("app", {
actions: {
async init() {
// 获取站点信息
// const result = await get("localization", null, null, true, true);
// this.localization = result.data;
//
this.token = getAccessToken();
this.localization = await getLocalizationAsync();
// 获取用户信息
if (await isLogin()) {

5
code/WebApp/vanilla/styles/site.css

@ -103,16 +103,13 @@ a.logo {
white-space: nowrap;
}
.el-icon {
margin-right: 5px;
}
.el-dialog__header,
.el-dialog__footer,
.el-drawer__header,
.el-drawer__footer {
height: var(--header);
padding: 15px;
margin: 0;
}
.el-dialog__header,

6
code/WebApp/vanilla/utils/index.js

@ -116,5 +116,9 @@ function getProp(instance, propPath) {
return get(instance, propPath);
}
function getFileName(contentDisposition) {
return decodeURIComponent(/filename\*=UTF-8''([\w%\-\.]+)(?:; ?|$)/i.exec(contentDisposition)[1]);
}
export default html;
export { persentFormat, bytesFormat, format, schemaToModel, listToTree, treeToList, getProp };
export { persentFormat, bytesFormat, format, schemaToModel, listToTree, treeToList, getProp, getFileName };

2
code/WebApp/vanilla/views/404.js

@ -1,3 +1,3 @@
export default {
template: `404:{{$route}}`,
template: `404`,
};

17
code/WebApp/vanilla/views/base-data/role.js

@ -0,0 +1,17 @@
import AppList from "../../components/list/index.js";
import html from "html";
import useConfig from "../../models/role.js";
export default {
components: { AppList },
template: html`<app-list :config="config" @command="onCommand" />`,
setup() {
// 变量定义
const config = useConfig();
// 函数定义
const onCommand = (item, rows) => {
console.log(item.path, item, rows);
};
return { config, onCommand };
},
};

17
code/WebApp/vanilla/views/base-data/user.js

@ -0,0 +1,17 @@
import AppList from "../../components/list/index.js";
import html from "html";
import useConfig from "../../models/user.js";
export default {
components: { AppList },
template: html`<app-list :config="config" @command="onCommand" />`,
setup() {
// 变量定义
const config = useConfig();
// 函数定义
const onCommand = (item, rows) => {
console.log(item.path, item, rows);
};
return { config, onCommand };
},
};

17
code/WebApp/vanilla/views/list.js

@ -5,22 +5,7 @@ import router from "../router/index.js";
export default {
components: { AppList },
template: html`<el-scrollbar>
<app-list @command="onCommand">
<template #columns="scope">
<el-table-column label="自定义列测试1" prop="id">
<template #default="scope"> {{scope.row.id}} </template>
</el-table-column>
<el-table-column label="自定义列测试2">
<template #default="scope"> {{scope.row.parent?.id}} </template>
</el-table-column>
</template>
<template #tableButtons="{rows}">
<el-button class="el-button--primary" @click="()=>(console.log(rows))">{{$t('test')}}</el-button>
</template>
<template #rowButtons="{rows}">
<el-button class="el-button--primary" @click="()=>(console.log(rows))">{{$t('test')}}</el-button>
</template>
</app-list>
<app-list @command="onCommand"> </app-list>
</el-scrollbar>`,
setup() {
console.log(router.currentRoute.value.fullPath);

84
code/WebApp/vanilla/views/login.js

@ -1,11 +1,14 @@
import html, { schemaToModel } from "html";
import { ref, reactive } from "vue";
import AppForm from "../components/form/index.js";
import { login } from "../api/user.js";
import { get } from "../request/index.js";
import { login, setRefreshToken, getUser, setAccessToken } from "../api/user.js";
import router, { refreshRouter } from "../router/index.js";
import { post } from "../request/index.js";
import LayoutLogo from "../layouts/logo.js";
import LayoutLocale from "../layouts/locale.js";
import LayoutFooter from "../layouts/footer.js";
import { useAppStore } from "../store/index.js";
import useLoginModel from "../models/login.js";
export default {
components: { AppForm, LayoutLogo, LayoutLocale, LayoutFooter },
@ -24,61 +27,32 @@ export default {
</el-main>
</el-container>`,
async setup() {
const schema = reactive({
title: "LoginRequestModel",
type: "object",
properties: {
username: {
title: "用户名",
type: "string",
rules: [
{
required: true,
message: "用户名不能为空",
},
{
max: 64,
message: "用户名的最大长度为 64",
},
],
},
password: {
title: "密码",
type: "string",
format: "password",
rules: [
{
required: true,
message: "密码不能为空",
},
{
max: 64,
message: "密码的最大长度为 64",
},
{
message: "DataTypeAttribute",
},
],
},
client_id: {
default: "basic-web",
hidden: true,
},
grant_type: {
default: "password",
hidden: true,
},
scope: {
default: "WebAppGateway BaseService",
hidden: true,
},
},
});
const schema = reactive(useLoginModel());
const model = reactive(schemaToModel(schema));
const submit = async (callback) => {
const result = await login(model);
if (result.errors) {
const submit = async (callback, loading) => {
try {
const url = "connect-token";
const appStore = useAppStore();
const result = await post(
url,
model,
{ headers: { "Content-Type": "application/x-www-form-urlencoded" } },
true
);
if (!result.errors) {
appStore.token = result.data.access_token;
setAccessToken(appStore.token);
//setRefreshToken(result.data.refresh_token);
appStore.user = await getUser();
await refreshRouter();
const redirect = router.currentRoute.value.query?.redirect ?? "/";
router.push(redirect);
}
callback(result.errors);
} catch (error) {
callback(error);
} finally {
loading.value = false;
}
};
return {

15
code/WebApp/vanilla/views/user/index.js

@ -1,15 +0,0 @@
import AppList from "../components/list/index.js";
import html from "html";
import router from "../router/index.js";
export default {
components: { AppList },
template: html`<app-list @command="onCommand"> </app-list>`,
setup() {
console.log(router.currentRoute.value.fullPath);
const onCommand = (item, rows) => {
console.log(item.path, item, rows);
};
return { onCommand };
},
};
Loading…
Cancel
Save