一大波更新

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" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"ant-design-vue": "4.x", "ant-design-vue": "^4.2.6",
"autoprefixer": "^10.4.20",
"axios": "^1.7.9",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"pinia": "^2.1.7", "js-md5": "^0.8.3",
"vue": "^3.4.0", "pinia": "^2.3.0",
"vue-router": "4" "postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.10.5", "@types/node": "^22.10.2",
"@vitejs/plugin-vue": "^4.1.0", "@vitejs/plugin-vue": "^5.2.1",
"sass": "^1.69.5", "sass": "^1.83.0 ",
"typescript": "^5.0.2", "typescript": "^5.7.2",
"vite": "^4.3.2", "vite": "^6.0.3",
"vue-tsc": "^1.4.2" "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"> <script setup lang="ts">
import { ConfigProvider } from 'ant-design-vue' import { ConfigProvider } from 'ant-design-vue'
import PageLoading from "./components/page-loading/index.vue"; import PageLoading from "@/components/page-loading/index.vue";
import { useUserStore } from "./service/user-store.ts"; import { useUserStore } from "@/service/user-store.ts";
import Login from "./components/login/index.vue"; 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> <Button type="primary" @click="saveProductValues(productValues)">保存数据</Button>
</div> </div>
<div class="calculator"> <div class="calculator">
<Input :rows="4" placeholder="请输入计算公式"/> <Input.TextArea :rows="4" placeholder="请输入计算公式"/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,11 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import {useUserStore} from "../../service/user-store.ts";
import {BizError} from "../../core/errors.ts";
import {ref} from "vue"; 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 leftImage from './login_pic.png'
import {Button} from "../button"; import {Button} from "../button";
import {LoginModel} from "../../service/api/user";
const loading = ref(false) const loading = ref(false)
const message = ref('') const message = ref('')
@ -25,7 +26,10 @@ const handleSubmit = () => {
loading.value = true loading.value = true
store store
.login(params.value) .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) .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"> <div class="menu-link" v-for="r in currentMenus">
<router-link class="menu-item" :to="(r.path || '/')"> <router-link class="menu-item" :to="(r.path || '/')">
<div class="menu-icon" :class="r.meta?.icon"></div> <div class="menu-icon" :class="r.meta?.icon"></div>
<div>{{ r.meta?.title }}</div> <div class="menu-text">{{ r.meta?.title }}</div>
</router-link> </router-link>
</div> </div>
</div> </div>
@ -57,8 +57,8 @@ const handleMenuClick = ({ key }: MenuInfo) => {
<MenuItem key="logout">退出登录</MenuItem> <MenuItem key="logout">退出登录</MenuItem>
</Menu> </Menu>
</template> </template>
<Button> <Button class="flex item-center">
{{ store.userInfo?.nickname }} <span>{{ store.userInfo?.nickname }}</span>
<DownOutlined /> <DownOutlined />
</Button> </Button>
</Dropdown> </Dropdown>
@ -138,6 +138,7 @@ const handleMenuClick = ({ key }: MenuInfo) => {
} }
.app-main-container { .app-main-container {
min-width: 1100px;
padding: 30px; padding: 30px;
overflow: auto; overflow: auto;
background: #f0f2f0; background: #f0f2f0;

View File

@ -3,7 +3,7 @@ import { ref, h } from "vue";
import { SearchOutlined, PlusOutlined } from '@ant-design/icons-vue'; import { SearchOutlined, PlusOutlined } from '@ant-design/icons-vue';
import { import {
Pagination, Select, SelectOption, Input, Button, Pagination, Select, SelectOption, Input, Button,
Space, Table, Space,
} from 'ant-design-vue' } from 'ant-design-vue'
import PageHeader from '../components/page-header.vue' import PageHeader from '../components/page-header.vue'
import { fields, getProductValues } from "../service/data"; import { fields, getProductValues } from "../service/data";
@ -54,12 +54,12 @@ const opts = [
<thead> <thead>
<tr> <tr>
<th>营养制剂</th> <th>营养制剂</th>
<th v-for="th in columns">{{th.name}}</th> <th v-for="th in columns">{{ th.name }}</th>
<th>操作</th> <th width="150">操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <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>{{ tr.product.name }}</td>
<td v-for="it in tr.values">{{ it.value }}</td> <td v-for="it in tr.values">{{ it.value }}</td>
<td> <td>
@ -74,42 +74,9 @@ const opts = [
</div> </div>
<div class="data-page"> <div class="data-page">
<Pagination v-model:current="current" v-model:page-size="pageSize" :total="50" <Pagination v-model:current="current" v-model:page-size="pageSize" :total="50"
:show-total="total => `共 ${total} 条`" /> :show-total="total => `共 ${total} 条`" />
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss"> <style lang="scss"></style>
.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>

View File

@ -1,8 +1,10 @@
<template> <template>
<p>输出计算</p> <p>输出计算</p>
<!-- <DataField /> -->
<Button @click="showMessage">test</Button> <Button @click="showMessage">test</Button>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import DataField from "@/components/data-fields/index.vue"
import Button from "../components/button/button.vue"; import Button from "../components/button/button.vue";
import {message} from "../components/message"; 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', path: 'user',
name: 'user', name: 'user',
component: () => import('./pages/user.vue'), component: () => import('./pages/user/index.vue'),
meta: { meta: {
role: 'root', role: 'root',
title: '用户管理', 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 = { export function getInfo() {
account: "admin", return get<UserInfo>(`/user/info`);
nickname: "管理员",
password: "",
role: 'root',
uid: 1
} }
export async function LoginService(params: LoginModel) { export async function getList(param: UserSearchParam) {
if (params.account == 'admin' && params.password == 'admin') { await sleep(200);
const loginInfo = Date.now().toString(16); return await post<DataList<UserInfo>>(`/user/all`, param);
return {
userinfo: fakeUser,
token: loginInfo
};
}
throw new BizError(1001, '用户名或密码错误');
} }
export async function GetLoginInfo() { export function deleteUser(id:number){
return fakeUser; 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; id: number;
name: string; name: string;
alias?: string; alias?: string;
} };
export type ProductFieldValue = { export type ProductFieldValue = {
id: number; id: number;
alias?: string; alias?: string;
product_id: number; product_id: number;
field_id: number; field_id: number;
value: number; value: number;
} };
export type ProductValue = { export type ProductValue = {
product: BaseModel; product: BaseModel;
values: ProductFieldValue[] values: ProductFieldValue[];
} };
export const fields: BaseModel[] = [ export const fields: BaseModel[] = [
{id: 1, name: '能量密度', alias: 'power'}, { id: 1, name: "能量密度", alias: "power" },
{id: 2, name: '蛋白Pro', alias: 'protein'}, { id: 2, name: "蛋白Pro", alias: "protein" },
{id: 3, name: 'Glu', alias: 'glu'}, { id: 3, name: "Glu", alias: "glu" },
{id: 4, name: 'Fat', alias: 'fat'}, { id: 4, name: "Fat", alias: "fat" },
{id: 5, name: '纤维素', alias: 'cellulose'}, { id: 5, name: "纤维素", alias: "cellulose" },
{id: 6, name: 'Na', alias: 'na'}, { id: 6, name: "Na", alias: "na" },
{id: 7, name: 'K', alias: 'k'}, { id: 7, name: "K", alias: "k" },
{id: 8, name: 'Ca', alias: 'ca'}, { id: 8, name: "Ca", alias: "ca" },
{id: 9, name: 'P', alias: 'p'}, { id: 9, name: "P", alias: "p" },
{id: 10, name: 'Mg', alias: 'mg'}, { id: 10, name: "Mg", alias: "mg" },
] ];
export const products: BaseModel[] = [ export const products: BaseModel[] = [
{id: 1, name: '爱伦多', alias: 'ailunduo'}, { id: 1, name: "爱伦多", alias: "ailunduo" },
{id: 2, name: '维沃', alias: 'weiwo'}, { id: 2, name: "维沃", alias: "weiwo" },
{id: 3, name: '佳维体', alias: 'jiaweiti'}, { id: 3, name: "佳维体", alias: "jiaweiti" },
{id: 4, name: '伊力佳', alias: 'yilijia'}, { id: 4, name: "伊力佳", alias: "yilijia" },
] ];
const PRODUCT_VALUES_KEY = 'PRODUCT_VALUES_KEY' const PRODUCT_VALUES_KEY = "PRODUCT_VALUES_KEY";
const product_values_default: ProductFieldValue[] = [ const product_values_default: ProductFieldValue[] = [
{ {
"id": 1, id: 1,
"product_id": 1, product_id: 1,
"field_id": 1, field_id: 1,
"value": 0, value: 0,
"alias": "ailunduo_power" alias: "ailunduo_power",
}, {"id": 2, "product_id": 1, "field_id": 2, "value": 0, "alias": "ailunduo_protein"}, { },
"id": 3, { id: 2, product_id: 1, field_id: 2, value: 0, alias: "ailunduo_protein" },
"product_id": 1, {
"field_id": 3, id: 3,
"value": 0, product_id: 1,
"alias": "ailunduo_glu" field_id: 3,
}, {"id": 4, "product_id": 1, "field_id": 4, "value": 0, "alias": "ailunduo_fat"}, { value: 0,
"id": 5, alias: "ailunduo_glu",
"product_id": 1, },
"field_id": 5, { id: 4, product_id: 1, field_id: 4, value: 0, alias: "ailunduo_fat" },
"value": 0, {
"alias": "ailunduo_cellulose" id: 5,
}, {"id": 6, "product_id": 1, "field_id": 6, "value": 0, "alias": "ailunduo_na"}, { product_id: 1,
"id": 7, field_id: 5,
"product_id": 1, value: 0,
"field_id": 7, alias: "ailunduo_cellulose",
"value": 0, },
"alias": "ailunduo_k" { id: 6, product_id: 1, field_id: 6, value: 0, alias: "ailunduo_na" },
}, {"id": 8, "product_id": 1, "field_id": 8, "value": 0, "alias": "ailunduo_ca"}, { {
"id": 9, id: 7,
"product_id": 1, product_id: 1,
"field_id": 9, field_id: 7,
"value": 0, value: 0,
"alias": "ailunduo_p" alias: "ailunduo_k",
}, {"id": 10, "product_id": 1, "field_id": 10, "value": 0, "alias": "ailunduo_mg"}, { },
"id": 11, { id: 8, product_id: 1, field_id: 8, value: 0, alias: "ailunduo_ca" },
"product_id": 2, {
"field_id": 1, id: 9,
"value": 0, product_id: 1,
"alias": "weiwo_power" field_id: 9,
}, {"id": 12, "product_id": 2, "field_id": 2, "value": 0, "alias": "weiwo_protein"}, { value: 0,
"id": 13, alias: "ailunduo_p",
"product_id": 2, },
"field_id": 3, { id: 10, product_id: 1, field_id: 10, value: 0, alias: "ailunduo_mg" },
"value": 0, {
"alias": "weiwo_glu" id: 11,
}, {"id": 14, "product_id": 2, "field_id": 4, "value": 0, "alias": "weiwo_fat"}, { product_id: 2,
"id": 15, field_id: 1,
"product_id": 2, value: 0,
"field_id": 5, alias: "weiwo_power",
"value": 0, },
"alias": "weiwo_cellulose" { id: 12, product_id: 2, field_id: 2, value: 0, alias: "weiwo_protein" },
}, {"id": 16, "product_id": 2, "field_id": 6, "value": 0, "alias": "weiwo_na"}, { {
"id": 17, id: 13,
"product_id": 2, product_id: 2,
"field_id": 7, field_id: 3,
"value": 0, value: 0,
"alias": "weiwo_k" alias: "weiwo_glu",
}, {"id": 18, "product_id": 2, "field_id": 8, "value": 0, "alias": "weiwo_ca"}, { },
"id": 19, { id: 14, product_id: 2, field_id: 4, value: 0, alias: "weiwo_fat" },
"product_id": 2, {
"field_id": 9, id: 15,
"value": 0, product_id: 2,
"alias": "weiwo_p" field_id: 5,
}, {"id": 20, "product_id": 2, "field_id": 10, "value": 0, "alias": "weiwo_mg"}, { value: 0,
"id": 21, alias: "weiwo_cellulose",
"product_id": 3, },
"field_id": 1, { id: 16, product_id: 2, field_id: 6, value: 0, alias: "weiwo_na" },
"value": 0, {
"alias": "jiaweiti_power" id: 17,
}, {"id": 22, "product_id": 3, "field_id": 2, "value": 0, "alias": "jiaweiti_protein"}, { product_id: 2,
"id": 23, field_id: 7,
"product_id": 3, value: 0,
"field_id": 3, alias: "weiwo_k",
"value": 0, },
"alias": "jiaweiti_glu" { id: 18, product_id: 2, field_id: 8, value: 0, alias: "weiwo_ca" },
}, {"id": 24, "product_id": 3, "field_id": 4, "value": 0, "alias": "jiaweiti_fat"}, { {
"id": 25, id: 19,
"product_id": 3, product_id: 2,
"field_id": 5, field_id: 9,
"value": 0, value: 0,
"alias": "jiaweiti_cellulose" alias: "weiwo_p",
}, {"id": 26, "product_id": 3, "field_id": 6, "value": 0, "alias": "jiaweiti_na"}, { },
"id": 27, { id: 20, product_id: 2, field_id: 10, value: 0, alias: "weiwo_mg" },
"product_id": 3, {
"field_id": 7, id: 21,
"value": 0, product_id: 3,
"alias": "jiaweiti_k" field_id: 1,
}, {"id": 28, "product_id": 3, "field_id": 8, "value": 0, "alias": "jiaweiti_ca"}, { value: 0,
"id": 29, alias: "jiaweiti_power",
"product_id": 3, },
"field_id": 9, { id: 22, product_id: 3, field_id: 2, value: 0, alias: "jiaweiti_protein" },
"value": 0, {
"alias": "jiaweiti_p" id: 23,
}, {"id": 30, "product_id": 3, "field_id": 10, "value": 0, "alias": "jiaweiti_mg"}, { product_id: 3,
"id": 31, field_id: 3,
"product_id": 4, value: 0,
"field_id": 1, alias: "jiaweiti_glu",
"value": 0, },
"alias": "yilijia_power" { id: 24, product_id: 3, field_id: 4, value: 0, alias: "jiaweiti_fat" },
}, {"id": 32, "product_id": 4, "field_id": 2, "value": 0, "alias": "yilijia_protein"}, { {
"id": 33, id: 25,
"product_id": 4, product_id: 3,
"field_id": 3, field_id: 5,
"value": 0, value: 0,
"alias": "yilijia_glu" alias: "jiaweiti_cellulose",
}, {"id": 34, "product_id": 4, "field_id": 4, "value": 0, "alias": "yilijia_fat"}, { },
"id": 35, { id: 26, product_id: 3, field_id: 6, value: 0, alias: "jiaweiti_na" },
"product_id": 4, {
"field_id": 5, id: 27,
"value": 0, product_id: 3,
"alias": "yilijia_cellulose" field_id: 7,
}, {"id": 36, "product_id": 4, "field_id": 6, "value": 0, "alias": "yilijia_na"}, { value: 0,
"id": 37, alias: "jiaweiti_k",
"product_id": 4, },
"field_id": 7, { id: 28, product_id: 3, field_id: 8, value: 0, alias: "jiaweiti_ca" },
"value": 0, {
"alias": "yilijia_k" id: 29,
}, {"id": 38, "product_id": 4, "field_id": 8, "value": 0, "alias": "yilijia_ca"}, { product_id: 3,
"id": 39, field_id: 9,
"product_id": 4, value: 0,
"field_id": 9, alias: "jiaweiti_p",
"value": 0, },
"alias": "yilijia_p" { id: 30, product_id: 3, field_id: 10, value: 0, alias: "jiaweiti_mg" },
}, {"id": 40, "product_id": 4, "field_id": 10, "value": 0, "alias": "yilijia_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() { export function getProductValues() {
const sessionData = localStorage.getItem(PRODUCT_VALUES_KEY) const sessionData = localStorage.getItem(PRODUCT_VALUES_KEY);
const product_values:ProductFieldValue[] =sessionData?JSON.parse(sessionData):product_values_default; const product_values: ProductFieldValue[] = sessionData
const values: Record<number, ProductFieldValue[]> = {} ? JSON.parse(sessionData)
product_values.forEach(s => { : product_values_default;
if(!values[s.product_id]) values[s.product_id]= []; const values: Record<number, ProductFieldValue[]> = {};
product_values.forEach((s) => {
if (!values[s.product_id]) values[s.product_id] = [];
values[s.product_id].push(s); 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; return results;
} }
export function saveProductValues(datas: ProductValue[]){ export function saveProductValues(datas: ProductValue[]) {
const values = datas.map(s=>s.values).flatMap(s=>s) const values = datas.map((s) => s.values).flatMap((s) => s);
localStorage.setItem(PRODUCT_VALUES_KEY,JSON.stringify(values)) localStorage.setItem(PRODUCT_VALUES_KEY, JSON.stringify(values));
} }
// const product_values: ProductFieldValue[] = [] // const product_values: ProductFieldValue[] = []
@ -189,4 +233,3 @@ export function saveProductValues(datas: ProductValue[]){
// }) // })
// }) // })
// console.log(JSON.stringify(product_values)) // 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 { defineStore } from "pinia"
import {onMounted, ref} from "vue" import { onMounted, ref } from "vue"
import {GetLoginInfo, LoginService} from "./api/user"; import { md5 } from "js-md5";
import {BizError} from "../core/errors.ts"; import { sleep } from "@/core/sleep.ts";
import {sleep} from "../core/sleep.ts"; import { userLogin, getInfo } from "./api/user";
import { BizError } from "@/types/core.ts";
type LoginParam = { type LoginParam = {
account: string; account: string;
password: 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', () => { export const useUserStore = defineStore('counter', () => {
const userInfo = ref<UserInfo>() const userInfo = ref<UserInfo>()
@ -17,15 +22,17 @@ export const useUserStore = defineStore('counter', () => {
// 登录 // 登录
const login = async (data: LoginParam) => { const login = async (data: LoginParam) => {
await sleep(1000); await sleep(500);
const info = await LoginService(data); const info = await userLogin({
userInfo.value = info.userinfo; ...data,
password: md5(data.password),
});
userInfo.value = info.user;
localStorage.setItem(LOGIN_SESSION_KEY, info.token) localStorage.setItem(LOGIN_SESSION_KEY, info.token)
} }
// 登出 // 登出
const logout = async () => { const logout = async () => {
await sleep(1000);
localStorage.removeItem(LOGIN_SESSION_KEY) localStorage.removeItem(LOGIN_SESSION_KEY)
userInfo.value = undefined; userInfo.value = undefined;
} }
@ -37,22 +44,22 @@ export const useUserStore = defineStore('counter', () => {
const getUserInfo = async () => { const getUserInfo = async () => {
const token = localStorage.getItem(LOGIN_SESSION_KEY); const token = localStorage.getItem(LOGIN_SESSION_KEY);
if (!token) { if (!token) {
throw new BizError(401) throw new BizError('need token', 401)
} }
userInfo.value = await GetLoginInfo(); userInfo.value = await getInfo();
} }
onMounted(() => { onMounted(() => {
getUserInfo().catch((e: BizError) => { getUserInfo().catch((e: BizError) => {
if (e.code == 401) { if (e.code == 401) {
//router.replace(`/login?redirect=${router.currentRoute.value.path}`).then(() => ) //router.replace(`/login?redirect=${router.currentRoute.value.path}`).then(() => )
console.log('401 show login') console.log('401 show login')
} }
}).finally(() => { }).finally(() => {
console.log('onMounted inited') console.log('onMounted inited')
setTimeout(()=>{ setTimeout(() => {
userInit.value = true userInit.value = true
},500) }, 500)
}) })
}) })

View File

@ -1,3 +1,5 @@
@use "./assets/libs" as *;
@font-face { @font-face {
font-family: 'iconfont'; font-family: 'iconfont';
/* Project id 4404323 */ /* Project id 4404323 */
@ -166,6 +168,11 @@ img {
} }
.ant-btn { .ant-btn {
.anticon {
position: relative;
transform: translateY(1px);
}
&.btn-info { &.btn-info {
background-color: var(--primary-color-2); background-color: var(--primary-color-2);
border-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 {
table-layout: fixed; table-layout: fixed;
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
th,td{
th,
td {
min-width: 0; min-width: 0;
height: 42px;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
text-align: left; text-align: left;
text-overflow: ellipsis; text-overflow: ellipsis;
vertical-align: middle; vertical-align: middle;
border-bottom: 1px solid #e8e8ec; border-bottom: 1px solid #e8e8ec;
padding: 0 16px; padding: 15px 16px;
} }
th { th {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
background-color: #f2f3f5; background-color: #f2f3f5;
position: relative; position: relative;
height: 100%; height: 100%;
padding:10px 16px;
} }
tr{
&:hover{ tr {
&:hover {
background-color: #ebf7ff; 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;
}

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

@ -2,15 +2,4 @@
type int = number; type int = number;
type double = number; type double = number;
type bool = boolean; 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, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "preserve", "jsx": "preserve",
"paths": {
"@/*": [
"./src/*"
]
},
/* Linting */ /* Linting */
"strict": true, "strict": true,

View File

@ -4,31 +4,40 @@ import path from 'path'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue()],
build: { build: {
// 小于10kb直接base64 // 小于10kb直接base64
assetsInlineLimit: 10240, assetsInlineLimit: 10240,
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks(id) { manualChunks(id) {
// console.log('chunk id',id) // console.log('chunk id',id)
if (id.includes('ant-design')) { if (id.includes('ant-design')) {
return 'ui-libs' return 'ui-libs'
} }
// if (id.includes('vue')) { // if (id.includes('vue')) {
// if (id.includes('node_modules')) { // if (id.includes('node_modules')) {
// return 'vue' // return 'vue'
// } // }
// return id.toString().split('node_modules/')[1].split('/')[0].toString(); // return id.toString().split('node_modules/')[1].split('/')[0].toString();
// } // }
} }
} }
},
}, },
}, base: './',
base: './', resolve: {
// resolve:{ alias: {
// alias:{ '@': path.resolve(__dirname, './src')
// '@': 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