一大波更新

feat:添加api接入;更新依赖版本;完善框架;更新样式;
This commit is contained in:
LittleBoy 2024-12-18 22:38:20 +08:00
parent 8f695d09a3
commit d23f5d5668
31 changed files with 2165 additions and 770 deletions

View File

@ -1,3 +0,0 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

View File

@ -9,18 +9,24 @@
"preview": "vite preview"
},
"dependencies": {
"ant-design-vue": "4.x",
"ant-design-vue": "^4.2.6",
"autoprefixer": "^10.4.20",
"axios": "^1.7.9",
"dayjs": "^1.11.10",
"pinia": "^2.1.7",
"vue": "^3.4.0",
"vue-router": "4"
"js-md5": "^0.8.3",
"pinia": "^2.3.0",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@types/node": "^20.10.5",
"@vitejs/plugin-vue": "^4.1.0",
"sass": "^1.69.5",
"typescript": "^5.0.2",
"vite": "^4.3.2",
"vue-tsc": "^1.4.2"
}
"@types/node": "^22.10.2",
"@vitejs/plugin-vue": "^5.2.1",
"sass": "^1.83.0 ",
"typescript": "^5.7.2",
"vite": "^6.0.3",
"vue-tsc": "^2.1.10"
},
"packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
}

8
postcss.config.js Normal file
View File

@ -0,0 +1,8 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {
}
}
}

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import { ConfigProvider } from 'ant-design-vue'
import PageLoading from "./components/page-loading/index.vue";
import { useUserStore } from "./service/user-store.ts";
import Login from "./components/login/index.vue";
import PageLoading from "@/components/page-loading/index.vue";
import { useUserStore } from "@/service/user-store.ts";
import Login from "@/components/login/index.vue";
//

29
src/assets/libs.scss Normal file
View File

@ -0,0 +1,29 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@mixin media-breakpoint-down($name) {
@if $name ==sm {
@media (max-width: 767px) {
@content;
}
}
@if $name ==md {
@media (max-width: 991px) {
@content;
}
}
@if $name ==lg {
@media (max-width: 1199px) {
@content;
}
}
@if $name ==xl {
@media (max-width: 1399px) {
@content;
}
}
}

View File

@ -44,7 +44,7 @@ const onInputPaste = (e: ClipboardEvent, pIndex: number, fIndex: number) => {
<Button type="primary" @click="saveProductValues(productValues)">保存数据</Button>
</div>
<div class="calculator">
<Input :rows="4" placeholder="请输入计算公式"/>
<Input.TextArea :rows="4" placeholder="请输入计算公式"/>
</div>
</div>
</div>

View File

@ -1,11 +1,12 @@
<script lang="ts" setup>
import {useUserStore} from "../../service/user-store.ts";
import {BizError} from "../../core/errors.ts";
import {ref} from "vue";
import {AppConfig} from "../../app-config.ts";
import {useUserStore} from "@/service/user-store.ts";
import {AppConfig} from "@/app-config.ts";
import { BizError } from "@/types/core.ts";
import leftImage from './login_pic.png'
import {Button} from "../button";
import {LoginModel} from "../../service/api/user";
const loading = ref(false)
const message = ref('')
@ -25,7 +26,10 @@ const handleSubmit = () => {
loading.value = true
store
.login(params.value)
.catch((e: BizError) => message.value = e.message)
.catch((e: BizError) => {
console.log('login error',e)
message.value = e.message
})
.finally(() => loading.value = false)
}
}

View File

@ -1,8 +0,0 @@
export class BizError extends Error {
code: number;
constructor(code: number, message: string='') {
super(message);
this.code = code;
}
}

17
src/core/role.ts Normal file
View File

@ -0,0 +1,17 @@
export enum RoleEnum {
// super user
ROOT = 'root',
// admin user
ADMIN = 'admin',
// common user
USER = 'user',
}
export const RoleList = [
{ label: '超级管理员', value: RoleEnum.ROOT },
{ label: '管理员', value: RoleEnum.ADMIN },
]
export const AllRoleList = [
{ label: '全部', value: '' },
...RoleList
]

5
src/core/string.ts Normal file
View File

@ -0,0 +1,5 @@
import { md5 } from "js-md5";
export function getMd5(str: string) {
return md5(str);
}

View File

@ -41,7 +41,7 @@ const handleMenuClick = ({ key }: MenuInfo) => {
<div class="menu-link" v-for="r in currentMenus">
<router-link class="menu-item" :to="(r.path || '/')">
<div class="menu-icon" :class="r.meta?.icon"></div>
<div>{{ r.meta?.title }}</div>
<div class="menu-text">{{ r.meta?.title }}</div>
</router-link>
</div>
</div>
@ -57,8 +57,8 @@ const handleMenuClick = ({ key }: MenuInfo) => {
<MenuItem key="logout">退出登录</MenuItem>
</Menu>
</template>
<Button>
{{ store.userInfo?.nickname }}
<Button class="flex item-center">
<span>{{ store.userInfo?.nickname }}</span>
<DownOutlined />
</Button>
</Dropdown>
@ -138,6 +138,7 @@ const handleMenuClick = ({ key }: MenuInfo) => {
}
.app-main-container {
min-width: 1100px;
padding: 30px;
overflow: auto;
background: #f0f2f0;

View File

@ -3,7 +3,7 @@ import { ref, h } from "vue";
import { SearchOutlined, PlusOutlined } from '@ant-design/icons-vue';
import {
Pagination, Select, SelectOption, Input, Button,
Space, Table,
Space,
} from 'ant-design-vue'
import PageHeader from '../components/page-header.vue'
import { fields, getProductValues } from "../service/data";
@ -54,12 +54,12 @@ const opts = [
<thead>
<tr>
<th>营养制剂</th>
<th v-for="th in columns">{{th.name}}</th>
<th>操作</th>
<th v-for="th in columns">{{ th.name }}</th>
<th width="150">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(tr,rowIndex) in allDataList" :key="rowIndex">
<tr v-for="(tr, rowIndex) in allDataList" :key="rowIndex">
<td>{{ tr.product.name }}</td>
<td v-for="it in tr.values">{{ it.value }}</td>
<td>
@ -79,37 +79,4 @@ const opts = [
</div>
</div>
</template>
<style lang="scss">
.search-form {
display: flex;
align-items: center;
margin: 10px 0;
.form-item {
display: inline-flex;
align-items: center;
}
}
.search-form {
.ant-input,
.ant-select .ant-select-selector {
background-color: #f7f8fa;
border: solid 1px transparent;
min-width: 100px;
&:focus,
&:hover {
border-color: var(--primary-color-1-hover);
}
}
}
.search-result-table {}
.data-page{
margin-top: 20px;
text-align: right;
}
</style>
<style lang="scss"></style>

View File

@ -1,8 +1,10 @@
<template>
<p>输出计算</p>
<!-- <DataField /> -->
<Button @click="showMessage">test</Button>
</template>
<script setup lang="ts">
import DataField from "@/components/data-fields/index.vue"
import Button from "../components/button/button.vue";
import {message} from "../components/message";

View File

@ -1,115 +0,0 @@
<script setup lang="ts">
import { ref, h } from "vue";
import { SearchOutlined, PlusOutlined } from '@ant-design/icons-vue';
import {
Pagination, Select, SelectOption, Input, Button,
Space, Table,
} from 'ant-design-vue'
import { fields, getProductValues } from "../service/data";
import PageHeader from '../components/page-header.vue'
const searchParams = ref({
type: '',
name: ''
})
const current = ref(2);
const pageSize = ref<number>(20);
const columns = fields;
const allDataList = getProductValues();
console.log(allDataList)
const opts = [
{ label: '全部', value: '' },
{ label: '能量密度', value: 'power' },
{ label: 'Pro(g)', value: 'pro' },
{ label: 'Na(g)', value: 'na' },
]
</script>
<template>
<div class="data-fields-container">
<PageHeader title="登录账号管理" description="* 超级管理员: 拥有最高权限,可使用全部管理功能。管理员:普通管理权限,不可新建、删除、编辑账号。" />
<div class="search-form">
<Space :size="20">
<Space class="form-item">
<span>营养制剂</span>
<Input class="search-item" placeholder="请输入营养制剂" v-model:value="searchParams.name" />
</Space>
<Space class="form-item">
<span>类型</span>
<Select class="search-item" v-model:value="searchParams.type">
<SelectOption v-for="(it, opIndex) in opts" :key="opIndex" :value="it.value">{{ it.label }}
</SelectOption>
</Select>
</Space>
<Space class="form-item" :size="15">
<Button :icon="h(SearchOutlined)" type="primary">查询</Button>
<Button :icon="h(PlusOutlined)" class="btn-info">新增</Button>
</Space>
</Space>
</div>
<div class="search-result-table">
<div>
<table class="table">
<thead>
<tr>
<th>营养制剂</th>
<th v-for="th in columns">{{ th.name }}</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(tr, rowIndex) in allDataList" :key="rowIndex">
<td>{{ tr.product.name }}</td>
<td v-for="it in tr.values">{{ it.value }}</td>
<td>
<Space :size="20">
<a>编辑</a>
<a>删除</a>
</Space>
</td>
</tr>
</tbody>
</table>
</div>
<div class="data-page">
<Pagination v-model:current="current" v-model:page-size="pageSize" :total="50"
:show-total="total => `共 ${total} 条`" />
</div>
</div>
</div>
</template>
<style lang="scss">
.search-form {
display: flex;
align-items: center;
margin: 10px 0;
.form-item {
display: inline-flex;
align-items: center;
}
}
.search-form {
.ant-input,
.ant-select .ant-select-selector {
background-color: #f7f8fa;
border: solid 1px transparent;
min-width: 100px;
&:focus,
&:hover {
border-color: var(--primary-color-1-hover);
}
}
}
.search-result-table {}
.data-page {
margin-top: 20px;
text-align: right;
}
</style>

132
src/pages/user/index.vue Normal file
View File

@ -0,0 +1,132 @@
<script setup lang="ts">
import { ref, h } from "vue";
import { SearchOutlined, PlusOutlined } from '@ant-design/icons-vue';
import {
Pagination, Select, SelectOption, Input, Button, Space, Empty, Spin,
Modal,message
} from 'ant-design-vue'
import { RoleEnum, AllRoleList } from '@/core/role'
import { columns, getList,deleteUser } from "@/service/api/user";
import PageHeader from '@/components/page-header.vue'
import useRequest from "@/service/useRequest";
import EditModal from './modal.vue'
const editUser = ref<UserInfo>()
const searchParams = ref<UserSearchParam>({
page: 1,
limit: 10, role: ''
})
const { data: allDataList, loading, run } = useRequest(() => getList(searchParams.value))
// onMounted(() => {
// getList(searchParams.value).then(ret=>{
// allDataList.value = ret
// })
// })
function search() {
searchParams.value = {
...searchParams.value,
page: 1,
limit: 10
}
run()
}
function deleteUserById(id: number) {
deleteUser(id).then(() => {
message.success('删除成功')
run()
})
}
function handleDelete(id: number) {
Modal.confirm({
title:'删除账号',
content:'确定要删除该账号吗?',
cancelText:'取消',
okText:'删除',
okType: 'danger',
okButtonProps:{
},
onOk:()=>{
deleteUserById(id)
}
})
}
</script>
<template>
<div class="data-fields-container">
<PageHeader title="登录账号管理" description="* 超级管理员: 拥有最高权限,可使用全部管理功能。管理员:普通管理权限,不可新建、删除、编辑账号。" />
<div class="search-form">
<Space :size="20">
<Space class="form-item">
<span>姓名</span>
<Input class="search-item" placeholder="请输入姓名" v-model:value="searchParams.nickname" />
</Space>
<Space class="form-item">
<span>账号</span>
<Input class="search-item" placeholder="请输入账号" v-model:value="searchParams.account" />
</Space>
<Space class="form-item">
<span>类型</span>
<Select class="search-item" v-model:value="searchParams.role" style="width: 200px;">
<SelectOption v-for="(it, opIndex) in AllRoleList" :key="opIndex" :value="it.value">{{ it.label }}
</SelectOption>
</Select>
</Space>
<Space class="form-item" :size="15">
<Button :icon="h(SearchOutlined)" class="flex item-center" type="primary" @click="search()">查询</Button>
<Button :icon="h(PlusOutlined)" class="btn-info flex item-center" @click="editUser = {id:0}">新增</Button>
</Space>
</Space>
</div>
<div class="search-result-table">
<div>
<Spin :spinning="loading">
<table class="table">
<thead>
<tr>
<th v-for="th in columns">{{ th.title }}</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(user, rowIndex) in allDataList?.list" :key="rowIndex">
<td>{{ user.nickname }}</td>
<td>{{ user.account }}</td>
<td>{{ user.role == RoleEnum.ROOT ? '超级管理员' : '管理员' }}</td>
<td>
<Space :size="20">
<a @click="editUser = user">编辑</a>
<a @click="handleDelete(user.id)">删除</a>
</Space>
</td>
</tr>
</tbody>
</table>
<div v-if="!allDataList || allDataList?.total == 0" style="padding:30px;">
<Empty description="暂无数据" />
</div>
</Spin>
</div>
<div v-if="allDataList && allDataList?.total > 0" class="data-page">
<Pagination v-model:current="searchParams.page" v-model:page-size="searchParams.limit"
:total="allDataList.total" :show-total="(total: number) => `共 ${total} 条`" />
</div>
</div>
<EditModal v-if="!!editUser" v-model="editUser" />
</div>
</template>
<style lang="scss">
.ant-btn-dangerous{
background-color: hsl(359, 100%, 60%);
color: white !important;
&:hover{
background-color: hsl(359, 100%, 70%);
}
}
</style>

58
src/pages/user/modal.vue Normal file
View File

@ -0,0 +1,58 @@
<script lang="ts" setup>
import { defineModel } from 'vue';
import { Modal, Form, FormItem, Input, Select, SelectOption, Space, Button } from 'ant-design-vue';
import { RoleList } from '@/core/role';
const editUser = defineModel<UserInfo>();
function handleSave(values: UserInfo) {
console.log(values)
}
function handleCancel() {
editUser.value = undefined;
}
</script>
<template>
<Modal :width="400" :open="true" :footer="null" @cancel="handleCancel">
<div class="pt-12">
<Form @finish="handleSave" autocomplete="off" v-model="editUser" :label-col="{ span: 5 }"
:wrapper-col="{ span: 18 }">
<FormItem label="用户名" name="nickname" :rules="[{ required: true, message: '请输入姓名' }]">
<Input placeholder="请输入姓名" />
</FormItem>
<FormItem label="账号" name="account" :rules="[{ required: true, message: '请输入手机号或邮箱' }]">
<Input placeholder="请输入手机号或邮箱" />
</FormItem>
<FormItem label="密码" name="password" :rules="[{
validator: async (_:any, value:string) => {
console.log('mima',value)
if(editUser.value?.id){
return true;
}
if(!value || value.length < 6 || value.length > 12){
throw new Error('密码长度6-12位');
};
return true;
}
}]">
<Input placeholder="请输入6-12位密码" type="password" />
</FormItem>
<FormItem label="角色" name="role" :rules="[{ required: true, message: '请选择用户角色' }]">
<Select placeholder="请选择" style="width: 100%;">
<SelectOption v-for="(it, opIndex) in RoleList" :key="opIndex" :value="it.value">{{ it.label }}
</SelectOption>
</Select>
</FormItem>
<FormItem class="flex justify-end mt-10 mb-0">
<Space :size="20">
<Button @click="handleCancel">取消</Button>
<Button html-type="submit" class="btn-info">确定</Button>
</Space>
</FormItem>
</Form>
</div>
</Modal>
</template>

View File

@ -43,7 +43,7 @@ export const routes:RouteRecordRaw[] = [
{
path: 'user',
name: 'user',
component: () => import('./pages/user.vue'),
component: () => import('./pages/user/index.vue'),
meta: {
role: 'root',
title: '用户管理',

View File

@ -0,0 +1,53 @@
import axios from 'axios'
import { BizError } from '@/types/core';
import {getToken} from '@/service/user-store'
const axiosService = axios.create({
baseURL: '/api/v2',
timeout: 5000,
headers: {
'Authorization': getToken()
}
})
export function request<T>(options: RequestOption) {
return new Promise<T>((resolve, reject) => {
const { url, method, data, baseURL, getOriginResult } = options;
axiosService.request<APIResponse<T>>({
url,
method: method || 'get',
data,
baseURL,
}).then(res => {
if (res.status != 200) {
reject(new BizError("Service Internal Exception,Please Try Later!", res.status))
return;
}
if (getOriginResult) {
resolve(res.data as unknown as T)
return;
}
// const
const { code, message, data } = res.data
if (code == 0) {
resolve(data as unknown as T)
} else {
reject(new BizError(message, code, data))
}
}).catch(e => {
reject(new BizError(e.message, 500))
})
})
}
export function post<T>(url:string,data?: any){
return request<T>({
url,
method:'post',
data
})
}
export function get<T>(url:string){
return request<T>({url})
}

View File

@ -1,29 +1,27 @@
import {BizError} from "../../core/errors.ts";
import { sleep } from "@/core/sleep";
import { get, post } from "./request";
export type LoginModel = {
account: string;
password: string;
export function userLogin(params: LoginModel) {
return post<{ token: string; user: UserInfo }>(`/user/login`, params);
}
const fakeUser: UserInfo = {
account: "admin",
nickname: "管理员",
password: "",
role: 'root',
uid: 1
export function getInfo() {
return get<UserInfo>(`/user/info`);
}
export async function LoginService(params: LoginModel) {
if (params.account == 'admin' && params.password == 'admin') {
const loginInfo = Date.now().toString(16);
return {
userinfo: fakeUser,
token: loginInfo
};
}
throw new BizError(1001, '用户名或密码错误');
export async function getList(param: UserSearchParam) {
await sleep(200);
return await post<DataList<UserInfo>>(`/user/all`, param);
}
export async function GetLoginInfo() {
return fakeUser;
export function deleteUser(id:number){
return post(`/user/delete`,{id})
}
export const columns = [
{ title: '账号', dataIndex: 'account' },
{ title: '姓名', dataIndex: 'nickname' },
{ title: '角色', dataIndex: 'role' },
]

View File

@ -2,177 +2,221 @@ export type BaseModel = {
id: number;
name: string;
alias?: string;
}
};
export type ProductFieldValue = {
id: number;
alias?: string;
product_id: number;
field_id: number;
value: number;
}
};
export type ProductValue = {
product: BaseModel;
values: ProductFieldValue[]
}
values: ProductFieldValue[];
};
export const fields: BaseModel[] = [
{id: 1, name: '能量密度', alias: 'power'},
{id: 2, name: '蛋白Pro', alias: 'protein'},
{id: 3, name: 'Glu', alias: 'glu'},
{id: 4, name: 'Fat', alias: 'fat'},
{id: 5, name: '纤维素', alias: 'cellulose'},
{id: 6, name: 'Na', alias: 'na'},
{id: 7, name: 'K', alias: 'k'},
{id: 8, name: 'Ca', alias: 'ca'},
{id: 9, name: 'P', alias: 'p'},
{id: 10, name: 'Mg', alias: 'mg'},
]
{ id: 1, name: "能量密度", alias: "power" },
{ id: 2, name: "蛋白Pro", alias: "protein" },
{ id: 3, name: "Glu", alias: "glu" },
{ id: 4, name: "Fat", alias: "fat" },
{ id: 5, name: "纤维素", alias: "cellulose" },
{ id: 6, name: "Na", alias: "na" },
{ id: 7, name: "K", alias: "k" },
{ id: 8, name: "Ca", alias: "ca" },
{ id: 9, name: "P", alias: "p" },
{ id: 10, name: "Mg", alias: "mg" },
];
export const products: BaseModel[] = [
{id: 1, name: '爱伦多', alias: 'ailunduo'},
{id: 2, name: '维沃', alias: 'weiwo'},
{id: 3, name: '佳维体', alias: 'jiaweiti'},
{id: 4, name: '伊力佳', alias: 'yilijia'},
]
{ id: 1, name: "爱伦多", alias: "ailunduo" },
{ id: 2, name: "维沃", alias: "weiwo" },
{ id: 3, name: "佳维体", alias: "jiaweiti" },
{ id: 4, name: "伊力佳", alias: "yilijia" },
];
const PRODUCT_VALUES_KEY = 'PRODUCT_VALUES_KEY'
const PRODUCT_VALUES_KEY = "PRODUCT_VALUES_KEY";
const product_values_default: ProductFieldValue[] = [
{
"id": 1,
"product_id": 1,
"field_id": 1,
"value": 0,
"alias": "ailunduo_power"
}, {"id": 2, "product_id": 1, "field_id": 2, "value": 0, "alias": "ailunduo_protein"}, {
"id": 3,
"product_id": 1,
"field_id": 3,
"value": 0,
"alias": "ailunduo_glu"
}, {"id": 4, "product_id": 1, "field_id": 4, "value": 0, "alias": "ailunduo_fat"}, {
"id": 5,
"product_id": 1,
"field_id": 5,
"value": 0,
"alias": "ailunduo_cellulose"
}, {"id": 6, "product_id": 1, "field_id": 6, "value": 0, "alias": "ailunduo_na"}, {
"id": 7,
"product_id": 1,
"field_id": 7,
"value": 0,
"alias": "ailunduo_k"
}, {"id": 8, "product_id": 1, "field_id": 8, "value": 0, "alias": "ailunduo_ca"}, {
"id": 9,
"product_id": 1,
"field_id": 9,
"value": 0,
"alias": "ailunduo_p"
}, {"id": 10, "product_id": 1, "field_id": 10, "value": 0, "alias": "ailunduo_mg"}, {
"id": 11,
"product_id": 2,
"field_id": 1,
"value": 0,
"alias": "weiwo_power"
}, {"id": 12, "product_id": 2, "field_id": 2, "value": 0, "alias": "weiwo_protein"}, {
"id": 13,
"product_id": 2,
"field_id": 3,
"value": 0,
"alias": "weiwo_glu"
}, {"id": 14, "product_id": 2, "field_id": 4, "value": 0, "alias": "weiwo_fat"}, {
"id": 15,
"product_id": 2,
"field_id": 5,
"value": 0,
"alias": "weiwo_cellulose"
}, {"id": 16, "product_id": 2, "field_id": 6, "value": 0, "alias": "weiwo_na"}, {
"id": 17,
"product_id": 2,
"field_id": 7,
"value": 0,
"alias": "weiwo_k"
}, {"id": 18, "product_id": 2, "field_id": 8, "value": 0, "alias": "weiwo_ca"}, {
"id": 19,
"product_id": 2,
"field_id": 9,
"value": 0,
"alias": "weiwo_p"
}, {"id": 20, "product_id": 2, "field_id": 10, "value": 0, "alias": "weiwo_mg"}, {
"id": 21,
"product_id": 3,
"field_id": 1,
"value": 0,
"alias": "jiaweiti_power"
}, {"id": 22, "product_id": 3, "field_id": 2, "value": 0, "alias": "jiaweiti_protein"}, {
"id": 23,
"product_id": 3,
"field_id": 3,
"value": 0,
"alias": "jiaweiti_glu"
}, {"id": 24, "product_id": 3, "field_id": 4, "value": 0, "alias": "jiaweiti_fat"}, {
"id": 25,
"product_id": 3,
"field_id": 5,
"value": 0,
"alias": "jiaweiti_cellulose"
}, {"id": 26, "product_id": 3, "field_id": 6, "value": 0, "alias": "jiaweiti_na"}, {
"id": 27,
"product_id": 3,
"field_id": 7,
"value": 0,
"alias": "jiaweiti_k"
}, {"id": 28, "product_id": 3, "field_id": 8, "value": 0, "alias": "jiaweiti_ca"}, {
"id": 29,
"product_id": 3,
"field_id": 9,
"value": 0,
"alias": "jiaweiti_p"
}, {"id": 30, "product_id": 3, "field_id": 10, "value": 0, "alias": "jiaweiti_mg"}, {
"id": 31,
"product_id": 4,
"field_id": 1,
"value": 0,
"alias": "yilijia_power"
}, {"id": 32, "product_id": 4, "field_id": 2, "value": 0, "alias": "yilijia_protein"}, {
"id": 33,
"product_id": 4,
"field_id": 3,
"value": 0,
"alias": "yilijia_glu"
}, {"id": 34, "product_id": 4, "field_id": 4, "value": 0, "alias": "yilijia_fat"}, {
"id": 35,
"product_id": 4,
"field_id": 5,
"value": 0,
"alias": "yilijia_cellulose"
}, {"id": 36, "product_id": 4, "field_id": 6, "value": 0, "alias": "yilijia_na"}, {
"id": 37,
"product_id": 4,
"field_id": 7,
"value": 0,
"alias": "yilijia_k"
}, {"id": 38, "product_id": 4, "field_id": 8, "value": 0, "alias": "yilijia_ca"}, {
"id": 39,
"product_id": 4,
"field_id": 9,
"value": 0,
"alias": "yilijia_p"
}, {"id": 40, "product_id": 4, "field_id": 10, "value": 0, "alias": "yilijia_mg"}]
id: 1,
product_id: 1,
field_id: 1,
value: 0,
alias: "ailunduo_power",
},
{ id: 2, product_id: 1, field_id: 2, value: 0, alias: "ailunduo_protein" },
{
id: 3,
product_id: 1,
field_id: 3,
value: 0,
alias: "ailunduo_glu",
},
{ id: 4, product_id: 1, field_id: 4, value: 0, alias: "ailunduo_fat" },
{
id: 5,
product_id: 1,
field_id: 5,
value: 0,
alias: "ailunduo_cellulose",
},
{ id: 6, product_id: 1, field_id: 6, value: 0, alias: "ailunduo_na" },
{
id: 7,
product_id: 1,
field_id: 7,
value: 0,
alias: "ailunduo_k",
},
{ id: 8, product_id: 1, field_id: 8, value: 0, alias: "ailunduo_ca" },
{
id: 9,
product_id: 1,
field_id: 9,
value: 0,
alias: "ailunduo_p",
},
{ id: 10, product_id: 1, field_id: 10, value: 0, alias: "ailunduo_mg" },
{
id: 11,
product_id: 2,
field_id: 1,
value: 0,
alias: "weiwo_power",
},
{ id: 12, product_id: 2, field_id: 2, value: 0, alias: "weiwo_protein" },
{
id: 13,
product_id: 2,
field_id: 3,
value: 0,
alias: "weiwo_glu",
},
{ id: 14, product_id: 2, field_id: 4, value: 0, alias: "weiwo_fat" },
{
id: 15,
product_id: 2,
field_id: 5,
value: 0,
alias: "weiwo_cellulose",
},
{ id: 16, product_id: 2, field_id: 6, value: 0, alias: "weiwo_na" },
{
id: 17,
product_id: 2,
field_id: 7,
value: 0,
alias: "weiwo_k",
},
{ id: 18, product_id: 2, field_id: 8, value: 0, alias: "weiwo_ca" },
{
id: 19,
product_id: 2,
field_id: 9,
value: 0,
alias: "weiwo_p",
},
{ id: 20, product_id: 2, field_id: 10, value: 0, alias: "weiwo_mg" },
{
id: 21,
product_id: 3,
field_id: 1,
value: 0,
alias: "jiaweiti_power",
},
{ id: 22, product_id: 3, field_id: 2, value: 0, alias: "jiaweiti_protein" },
{
id: 23,
product_id: 3,
field_id: 3,
value: 0,
alias: "jiaweiti_glu",
},
{ id: 24, product_id: 3, field_id: 4, value: 0, alias: "jiaweiti_fat" },
{
id: 25,
product_id: 3,
field_id: 5,
value: 0,
alias: "jiaweiti_cellulose",
},
{ id: 26, product_id: 3, field_id: 6, value: 0, alias: "jiaweiti_na" },
{
id: 27,
product_id: 3,
field_id: 7,
value: 0,
alias: "jiaweiti_k",
},
{ id: 28, product_id: 3, field_id: 8, value: 0, alias: "jiaweiti_ca" },
{
id: 29,
product_id: 3,
field_id: 9,
value: 0,
alias: "jiaweiti_p",
},
{ id: 30, product_id: 3, field_id: 10, value: 0, alias: "jiaweiti_mg" },
{
id: 31,
product_id: 4,
field_id: 1,
value: 0,
alias: "yilijia_power",
},
{ id: 32, product_id: 4, field_id: 2, value: 0, alias: "yilijia_protein" },
{
id: 33,
product_id: 4,
field_id: 3,
value: 0,
alias: "yilijia_glu",
},
{ id: 34, product_id: 4, field_id: 4, value: 0, alias: "yilijia_fat" },
{
id: 35,
product_id: 4,
field_id: 5,
value: 0,
alias: "yilijia_cellulose",
},
{ id: 36, product_id: 4, field_id: 6, value: 0, alias: "yilijia_na" },
{
id: 37,
product_id: 4,
field_id: 7,
value: 0,
alias: "yilijia_k",
},
{ id: 38, product_id: 4, field_id: 8, value: 0, alias: "yilijia_ca" },
{
id: 39,
product_id: 4,
field_id: 9,
value: 0,
alias: "yilijia_p",
},
{ id: 40, product_id: 4, field_id: 10, value: 0, alias: "yilijia_mg" },
];
export function getProductValues() {
const sessionData = localStorage.getItem(PRODUCT_VALUES_KEY)
const product_values:ProductFieldValue[] =sessionData?JSON.parse(sessionData):product_values_default;
const values: Record<number, ProductFieldValue[]> = {}
product_values.forEach(s => {
if(!values[s.product_id]) values[s.product_id]= [];
const sessionData = localStorage.getItem(PRODUCT_VALUES_KEY);
const product_values: ProductFieldValue[] = sessionData
? JSON.parse(sessionData)
: product_values_default;
const values: Record<number, ProductFieldValue[]> = {};
product_values.forEach((s) => {
if (!values[s.product_id]) values[s.product_id] = [];
values[s.product_id].push(s);
})
const results: ProductValue[] = products.map(product=>({product,values: values[product.id]}));
});
const results: ProductValue[] = products.map((product) => ({
product,
values: values[product.id],
}));
return results;
}
export function saveProductValues(datas: ProductValue[]){
const values = datas.map(s=>s.values).flatMap(s=>s)
localStorage.setItem(PRODUCT_VALUES_KEY,JSON.stringify(values))
export function saveProductValues(datas: ProductValue[]) {
const values = datas.map((s) => s.values).flatMap((s) => s);
localStorage.setItem(PRODUCT_VALUES_KEY, JSON.stringify(values));
}
// const product_values: ProductFieldValue[] = []
@ -189,4 +233,3 @@ export function saveProductValues(datas: ProductValue[]){
// })
// })
// console.log(JSON.stringify(product_values))

47
src/service/useRequest.ts Normal file
View File

@ -0,0 +1,47 @@
import { onMounted, ref, watch } from "vue";
export default function useRequest<T>(
service: () => Promise<T>,
options?: {
manual?: boolean;
refreshDeps?: any[];
}
) {
const data = ref<T>();
const loading = ref(false);
const error = ref<string>();
const _request = () => {
loading.value = true
return service()
.then((res) => {
data.value = res
})
.catch((e: Error) => {
error.value = e.message
})
.finally(() => {
loading.value = false
})
}
const refresh = () => {
_request()
}
onMounted(() => {
if (!options?.manual) {
_request()
}
});
if (options?.refreshDeps) {
watch(options.refreshDeps, () => {
_request()
})
}
return {
data,
refresh,
loading,
error,
run: _request
}
}

View File

@ -1,15 +1,20 @@
import {defineStore} from "pinia"
import {onMounted, ref} from "vue"
import {GetLoginInfo, LoginService} from "./api/user";
import {BizError} from "../core/errors.ts";
import {sleep} from "../core/sleep.ts";
import { defineStore } from "pinia"
import { onMounted, ref } from "vue"
import { md5 } from "js-md5";
import { sleep } from "@/core/sleep.ts";
import { userLogin, getInfo } from "./api/user";
import { BizError } from "@/types/core.ts";
type LoginParam = {
account: string;
password: string;
}
export const LOGIN_SESSION_KEY = 'x-yy-js-data-user';
const LOGIN_SESSION_KEY = 'x-yy-js-data-user';
export function getToken() {
return localStorage.getItem('x-yy-js-data-user')
}
export const useUserStore = defineStore('counter', () => {
const userInfo = ref<UserInfo>()
@ -17,15 +22,17 @@ export const useUserStore = defineStore('counter', () => {
// 登录
const login = async (data: LoginParam) => {
await sleep(1000);
const info = await LoginService(data);
userInfo.value = info.userinfo;
await sleep(500);
const info = await userLogin({
...data,
password: md5(data.password),
});
userInfo.value = info.user;
localStorage.setItem(LOGIN_SESSION_KEY, info.token)
}
// 登出
const logout = async () => {
await sleep(1000);
localStorage.removeItem(LOGIN_SESSION_KEY)
userInfo.value = undefined;
}
@ -37,9 +44,9 @@ export const useUserStore = defineStore('counter', () => {
const getUserInfo = async () => {
const token = localStorage.getItem(LOGIN_SESSION_KEY);
if (!token) {
throw new BizError(401)
throw new BizError('need token', 401)
}
userInfo.value = await GetLoginInfo();
userInfo.value = await getInfo();
}
onMounted(() => {
@ -50,9 +57,9 @@ export const useUserStore = defineStore('counter', () => {
}
}).finally(() => {
console.log('onMounted inited')
setTimeout(()=>{
setTimeout(() => {
userInit.value = true
},500)
}, 500)
})
})

View File

@ -1,3 +1,5 @@
@use "./assets/libs" as *;
@font-face {
font-family: 'iconfont';
/* Project id 4404323 */
@ -166,6 +168,11 @@ img {
}
.ant-btn {
.anticon {
position: relative;
transform: translateY(1px);
}
&.btn-info {
background-color: var(--primary-color-2);
border-color: var(--primary-color-2);
@ -179,31 +186,81 @@ img {
}
}
.layout{
.menu{
min-width: 100px;
}
}
.menu-text{
display: block;
@include media-breakpoint-down(lg) {
font-size: 12px;;
}
@include media-breakpoint-down(md) {
display: none;
}
}
.search-form {
display: flex;
align-items: center;
margin: 20px 0;
.form-item {
display: inline-flex;
align-items: center;
}
}
.search-form {
.ant-input,
.ant-select .ant-select-selector {
background-color: #f7f8fa;
border: solid 1px transparent;
min-width: 100px;
&:focus,
&:hover {
border-color: var(--primary-color-1-hover);
}
}
}
.search-result-table {}
.data-page {
margin-top: 20px;
text-align: right;
}
.table {
table-layout: fixed;
border-collapse: collapse;
width: 100%;
th,td{
th,
td {
min-width: 0;
height: 42px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
text-align: left;
text-overflow: ellipsis;
vertical-align: middle;
border-bottom: 1px solid #e8e8ec;
padding: 0 16px;
padding: 15px 16px;
}
th {
white-space: nowrap;
overflow: hidden;
background-color: #f2f3f5;
position: relative;
height: 100%;
padding:10px 16px;
}
tr{
&:hover{
tr {
&:hover {
background-color: #ebf7ff;
}
}

26
src/types/api.d.ts vendored Normal file
View File

@ -0,0 +1,26 @@
// 请求方式
declare type RequestMethod = 'get' | 'post' | 'put' | 'delete'
declare type RequestOption = {
url: string;
method?: RequestMethod;
data?: AllType | null;
getOriginResult?: boolean;
baseURL?: string;
}
// 接口返回数据类型
declare interface APIResponse<T> {
/**
* 0:成功
*/
code: number;
/**
* 0
*/
message: string;
data?: T;
}
declare interface DataList<T>{
list: T[];
total: number;
}

13
src/types/core.ts Normal file
View File

@ -0,0 +1,13 @@
export class BizError extends Error {
/**
*
*/
code = 1;
data: any;
constructor(message: string, code = 1,data?: any) {
super(message);
this.code = code;
this.data = data;
}
}

23
src/types/user.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
declare type LoginModel = {
account: string;
password: string;
}
declare type UserSearchParam = {
page: number;
limit: number;
nickname?: string;
role?: string;
account?: string;
}
declare interface UserInfo {
id: number;
nickname: string;
account: string;
password: string;
role: string;
created_at: string;
last_login: string;
updated_at: string;
status: number;
}

11
src/vite-env.d.ts vendored
View File

@ -3,14 +3,3 @@
type int = number;
type double = number;
type bool = boolean;
type AccountRole = 'root' | 'admin' | 'user';
type UserInfo = {
uid: number;
nickname: string;
account: string;
password: string;
avatar?: string;
lastUpdate?:string;
createTime?: string;
role: AccountRole;
}

56
tailwind.config.js Normal file
View File

@ -0,0 +1,56 @@
const themeConfig = {
colors: {
'primary': '#7356f6',
'primary-bg': '#f6f6f6',
'active': '#FFE0E0',
'primary-red': '#F5222D',
'primary-red-70': 'rgba(245,34,45,0.7)',
},
widths: {
}
}
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./index.html',
'./src/**/*.{mjs,js,ts,jsx,tsx,html,vue}'
],
theme: {
extend: {
width: {
'396px': '396px',
'1200px': '1200px',
'1000px': '1000px',
'chat-input': '800px',
...themeConfig.widths,
},
margin: {
...themeConfig.widths,
},
padding: {
'basic': '20px',
...themeConfig.widths,
},
color: {
...themeConfig.colors,
},
borderColor: {
...themeConfig.colors,
},
backgroundColor: {
...themeConfig.colors,
}
},
screens: {
sm: '768px',
md: '1024px',
lg: '1200px',
xl: '1440px',
}
},
plugins: [],
}

View File

@ -13,6 +13,11 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"paths": {
"@/*": [
"./src/*"
]
},
/* Linting */
"strict": true,

View File

@ -26,9 +26,18 @@ export default defineConfig({
},
},
base: './',
// resolve:{
// alias:{
// '@': path.resolve(__dirname,"./src")
// }
// },
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
server: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
// rewrite: path => path.replace(/^\/api/, '')
}
}
}
})

1662
yarn.lock

File diff suppressed because it is too large Load Diff