| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | /** å®¡æ¹æ¨¡æ¿å页æ¥è¯¢ */ |
| | | export function listApprovalTemplatePage(params) { |
| | | return request({ |
| | | url: "/approvalTemplate/listPage", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | /** å®¡æ¹æ¨¡æ¿è¯¦æ
*/ |
| | | export function getApprovalTemplateDetail(id) { |
| | | return request({ |
| | | url: `/approvalTemplate/detail/${id}`, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | /** æ°å¢å®¡æ¹æ¨¡æ¿ */ |
| | | export function addApprovalTemplate(data) { |
| | | return request({ |
| | | url: "/approvalTemplate/add", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | /** ä¿®æ¹å®¡æ¹æ¨¡æ¿ */ |
| | | export function updateApprovalTemplate(data) { |
| | | return request({ |
| | | url: "/approvalTemplate/update", |
| | | method: "put", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | /** å é¤å®¡æ¹æ¨¡æ¿ï¼ä¼ ID æ°ç»ï¼ */ |
| | | export function deleteApprovalTemplate(ids) { |
| | | return request({ |
| | | url: "/approvalTemplate/delete", |
| | | method: "post", |
| | | data: ids, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * OA 模åè·¯å¾å¸¸éï¼pages.json path ä¸å«åç¼ /ï¼ |
| | | * 导èªä½¿ç¨ï¼uni.navigateTo({ url: OA_NAV.xxx }) |
| | | */ |
| | | const P = "pages/oa"; |
| | | |
| | | export const OA_NAV = { |
| | | /** 人äºç®¡ç */ |
| | | staffArchive: `/${P}/HrManage/staff-archive/index`, |
| | | staffContract: `/${P}/HrManage/staff-contract/index`, |
| | | regularApply: `/${P}/HrManage/regular-apply/index`, |
| | | transferApply: `/${P}/HrManage/transfer-apply/index`, |
| | | resignApply: `/${P}/HrManage/resign-apply/index`, |
| | | workHandover: `/${P}/HrManage/work-handover/index`, |
| | | postManage: `/${P}/HrManage/post-manage/index`, |
| | | /** åå¤ç®¡ç */ |
| | | leaveApply: `/${P}/AttendManage/leave-apply/index`, |
| | | overtimeApply: `/${P}/AttendManage/overtime-apply/index`, |
| | | /** æ¥é管ç */ |
| | | travelReimburse: `/${P}/ReimburseManage/travel-reimburse/index`, |
| | | costReimburse: `/${P}/ReimburseManage/cost-reimburse/index`, |
| | | /** åå管ç */ |
| | | purchaseContract: `/${P}/ContractManage/purchase-contract/index`, |
| | | saleContract: `/${P}/ContractManage/sale-contract/index`, |
| | | /** 审æ¹ç®¡ç */ |
| | | approveList: `/${P}/ApproveManage/approve-list/index`, |
| | | approveTemplate: `/${P}/ApproveManage/approve-template/index`, |
| | | approveTemplateEdit: `/${P}/ApproveManage/approve-template/edit`, |
| | | approveTemplateDetail: `/${P}/ApproveManage/approve-template/detail`, |
| | | /** ä¼ä¸æ°é» / å
¬åéç¥ */ |
| | | enterpriseNews: `/${P}/EnterpriseNews/news-manage/index`, |
| | | noticeAnnouncement: `/${P}/NoticeAnnouncement/notice-manage/index`, |
| | | }; |
| | | |
| | | /** pages.json 注åç¨ pathï¼æ / åç¼ï¼ */ |
| | | export const OA_PAGE_PATHS = Object.fromEntries( |
| | | Object.entries(OA_NAV).map(([key, url]) => [ |
| | | key, |
| | | url.replace(/^\//, ""), |
| | | ]) |
| | | ); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { OA_NAV } from "./oaPaths.js"; |
| | | |
| | | /** |
| | | * OA 模ååç»ï¼å·¥ä½å°å±ç¤º / ææ¡£å¯¹ç
§ï¼ |
| | | */ |
| | | export const OA_MODULES = [ |
| | | { |
| | | key: "HrManage", |
| | | name: "人äºç®¡ç", |
| | | children: [ |
| | | { label: "å工档æ¡", icon: "/static/images/icon/renyuanxinzi.svg", path: OA_NAV.staffArchive }, |
| | | { label: "åå·¥åå", icon: "/static/images/icon/hetongguanli.svg", path: OA_NAV.staffContract }, |
| | | { label: "转æ£ç³è¯·", icon: "/static/images/icon/hetongguanli.svg", path: OA_NAV.regularApply }, |
| | | { label: "è°å²ç³è¯·", icon: "/static/images/icon/renyuanxinzi.svg", path: OA_NAV.transferApply }, |
| | | { label: "离èç³è¯·", icon: "/static/images/icon/qingjiaguanli.svg", path: OA_NAV.resignApply }, |
| | | { label: "å·¥ä½äº¤æ¥", icon: "/static/images/icon/gongchuguanli.svg", path: OA_NAV.workHandover }, |
| | | { label: "å²ä½ç®¡ç", icon: "/static/images/icon/gongxuguanli.svg", path: OA_NAV.postManage }, |
| | | ], |
| | | }, |
| | | { |
| | | key: "AttendManage", |
| | | name: "åå¤ç®¡ç", |
| | | children: [ |
| | | { label: "请åç³è¯·", icon: "/static/images/icon/qingjiaguanli.svg", path: OA_NAV.leaveApply }, |
| | | { label: "å çç³è¯·", icon: "/static/images/icon/dakaqiandao.svg", path: OA_NAV.overtimeApply }, |
| | | ], |
| | | }, |
| | | { |
| | | key: "ReimburseManage", |
| | | name: "æ¥é管ç", |
| | | children: [ |
| | | { label: "å·®æ
æ¥é", icon: "/static/images/icon/chuchaiguanli.svg", path: OA_NAV.travelReimburse }, |
| | | { label: "è´¹ç¨æ¥é", icon: "/static/images/icon/baoxiaoguanli.svg", path: OA_NAV.costReimburse }, |
| | | ], |
| | | }, |
| | | { |
| | | key: "ContractManage", |
| | | name: "åå管ç", |
| | | children: [ |
| | | { label: "éè´åå", icon: "/static/images/icon/caigoutaizhang.svg", path: OA_NAV.purchaseContract }, |
| | | { label: "éå®åå", icon: "/static/images/icon/xiaoshoutaizhang.svg", path: OA_NAV.saleContract }, |
| | | ], |
| | | }, |
| | | { |
| | | key: "ApproveManage", |
| | | name: "审æ¹ç®¡ç", |
| | | children: [ |
| | | { label: "审æ¹å表", icon: "/static/images/icon/xietongshenpi.svg", path: OA_NAV.approveList }, |
| | | { label: "å®¡æ¹æ¨¡æ¿", icon: "/static/images/icon/guizhangzhidu.svg", path: OA_NAV.approveTemplate }, |
| | | ], |
| | | }, |
| | | { |
| | | key: "EnterpriseNews", |
| | | name: "ä¼ä¸æ°é»", |
| | | children: [ |
| | | { label: "ä¼ä¸æ°é»", icon: "/static/images/icon/zhishiku.svg", path: OA_NAV.enterpriseNews }, |
| | | ], |
| | | }, |
| | | { |
| | | key: "NoticeAnnouncement", |
| | | name: "å
¬åéç¥", |
| | | children: [ |
| | | { label: "å
¬åéç¥", icon: "/static/images/icon/tongzhigonggao.svg", path: OA_NAV.noticeAnnouncement }, |
| | | ], |
| | | }, |
| | | ]; |
| | | |
| | | /** å·¥ä½å°æå¹³èåï¼çº¯å端é
ç½®ï¼ */ |
| | | export const OA_WORKBENCH_ITEMS = OA_MODULES.flatMap(module => |
| | | module.children.map(item => ({ |
| | | ...item, |
| | | module: module.name, |
| | | moduleKey: module.key, |
| | | })) |
| | | ); |
| | |
| | | "navigationBarTitleText": "å½è¿ç»è®°", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/HrManage/staff-archive/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å工档æ¡", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/HrManage/staff-contract/index", |
| | | "style": { |
| | | "navigationBarTitleText": "åå·¥åå", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/HrManage/regular-apply/index", |
| | | "style": { |
| | | "navigationBarTitleText": "转æ£ç³è¯·", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/HrManage/transfer-apply/index", |
| | | "style": { |
| | | "navigationBarTitleText": "è°å²ç³è¯·", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/HrManage/resign-apply/index", |
| | | "style": { |
| | | "navigationBarTitleText": "离èç³è¯·", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/HrManage/work-handover/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å·¥ä½äº¤æ¥", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/HrManage/post-manage/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å²ä½ç®¡ç", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/AttendManage/leave-apply/index", |
| | | "style": { |
| | | "navigationBarTitleText": "请åç³è¯·", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/AttendManage/overtime-apply/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å çç³è¯·", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ReimburseManage/travel-reimburse/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å·®æ
æ¥é", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ReimburseManage/cost-reimburse/index", |
| | | "style": { |
| | | "navigationBarTitleText": "è´¹ç¨æ¥é", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ContractManage/purchase-contract/index", |
| | | "style": { |
| | | "navigationBarTitleText": "éè´åå", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ContractManage/sale-contract/index", |
| | | "style": { |
| | | "navigationBarTitleText": "éå®åå", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ApproveManage/approve-list/index", |
| | | "style": { |
| | | "navigationBarTitleText": "审æ¹å表", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ApproveManage/approve-template/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å®¡æ¹æ¨¡æ¿", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ApproveManage/approve-template/edit", |
| | | "style": { |
| | | "navigationBarTitleText": "æ°å»ºå®¡æ¹æ¨¡æ¿", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/ApproveManage/approve-template/detail", |
| | | "style": { |
| | | "navigationBarTitleText": "模æ¿è¯¦æ
", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/EnterpriseNews/news-manage/index", |
| | | "style": { |
| | | "navigationBarTitleText": "ä¼ä¸æ°é»", |
| | | "navigationStyle": "custom" |
| | | } |
| | | }, |
| | | { |
| | | "path": "pages/oa/NoticeAnnouncement/notice-manage/index", |
| | | "style": { |
| | | "navigationBarTitleText": "å
¬åéç¥", |
| | | "navigationStyle": "custom" |
| | | } |
| | | } |
| | | ], |
| | | "subPackages": [ |
| | |
| | | |
| | | <script setup> |
| | | import { onMounted, reactive, ref } from "vue"; |
| | | import { OA_WORKBENCH_ITEMS } from "@/config/oaWorkbench.js"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | |
| | | { icon: "/static/images/icon/baojiaguanli.svg", label: "æ¥ä»·å®¡æ¹" }, |
| | | { icon: "/static/images/icon/fahuoguanli.svg", label: "å货审æ¹" }, |
| | | ], |
| | | "OAåå
¬": OA_WORKBENCH_ITEMS.map(item => ({ ...item })), |
| | | }; |
| | | |
| | | // å¤ç常ç¨åè½ç¹å» |
| | | const handleCommonItemClick = item => { |
| | | if (item.path) { |
| | | uni.navigateTo({ url: item.path }); |
| | | return; |
| | | } |
| | | const url = routeMapping[item.label]; |
| | | if (url) { |
| | | uni.navigateTo({ url }); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 审æ¹ç®¡ç / 审æ¹å表 |
| | | è·¯ç±ï¼/pages/oa/ApproveManage/approve-list/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - 审æ¹ç®¡ç - 审æ¹å表 */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "ApproveManage/approve-list"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 审æ¹ç®¡ç / å®¡æ¹æ¨¡æ¿è¯¦æ
|
| | | è·¯ç±ï¼/pages/oa/ApproveManage/approve-template/detail |
| | | --> |
| | | <template> |
| | | <view class="template-detail-page"> |
| | | <PageHeader title="模æ¿è¯¦æ
" |
| | | @back="goBack" /> |
| | | |
| | | <scroll-view class="detail-scroll" |
| | | scroll-y |
| | | :show-scrollbar="false"> |
| | | <view v-if="loading" |
| | | class="loading-wrap"> |
| | | <up-loading-icon mode="circle" /> |
| | | <text class="loading-text">å è½½ä¸...</text> |
| | | </view> |
| | | <template v-else-if="detail"> |
| | | <view class="section"> |
| | | <view class="section-title">åºæ¬ä¿¡æ¯</view> |
| | | <view class="info-list"> |
| | | <view class="info-item"> |
| | | <text class="info-label">模æ¿åç§°</text> |
| | | <text class="info-value">{{ detail.templateName || "-" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">模æ¿ç±»å</text> |
| | | <text class="info-value">{{ templateTypeText(detail.templateType) }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">å¯ç¨ç¶æ</text> |
| | | <text class="info-value" |
| | | :class="enabledClass(detail.enabled)"> |
| | | {{ enabledText(detail.enabled) }} |
| | | </text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">模æ¿è¯´æ</text> |
| | | <text class="info-value">{{ detail.description || "-" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">å建人</text> |
| | | <text class="info-value">{{ detail.createdUserName || "-" }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">å建æ¶é´</text> |
| | | <text class="info-value">{{ detail.createTime || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="section"> |
| | | <view class="section-title">å¡«æ¥é
ç½®</view> |
| | | <view class="info-list"> |
| | | <view class="info-item"> |
| | | <text class="info-label">å¡«æ¥æç¤º</text> |
| | | <text class="info-value">{{ formConfigData.prompt || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | <view v-if="formConfigData.fields.length" |
| | | class="field-block"> |
| | | <view v-for="(field, index) in formConfigData.fields" |
| | | :key="field.key || index" |
| | | class="field-card"> |
| | | <view class="field-card-head"> |
| | | <text class="field-card-name">{{ field.label }}</text> |
| | | <text class="field-tag">{{ fieldTypeLabel(field.type) }}</text> |
| | | <text v-if="field.required" |
| | | class="field-tag field-tag--req">å¿
å¡«</text> |
| | | </view> |
| | | <text v-if="field.defaultValue" |
| | | class="field-card-default"> |
| | | é»è®¤å¼ï¼{{ field.defaultValue }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="empty-hint">ææ å¡«æ¥é¡¹</view> |
| | | </view> |
| | | |
| | | <view class="section"> |
| | | <view class="section-title">å®¡æ¹æµç¨</view> |
| | | <view v-if="detail.nodes?.length" |
| | | class="flow-list"> |
| | | <view v-for="(node, index) in detail.nodes" |
| | | :key="node.id || index" |
| | | class="flow-card"> |
| | | <view class="flow-card-head"> |
| | | <text class="flow-level">第{{ levelLabel(node.levelNo || index + 1) }}级</text> |
| | | <text class="flow-type">{{ approveTypeText(node.approveType) }}</text> |
| | | </view> |
| | | <view class="approver-tags"> |
| | | <text v-for="(approver, aIdx) in node.approvers || []" |
| | | :key="approver.id || aIdx" |
| | | class="approver-tag"> |
| | | {{ approver.approverName || "-" }} |
| | | </text> |
| | | <text v-if="!(node.approvers || []).length" |
| | | class="empty-hint inline">ææ å®¡æ¹äºº</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="empty-hint">ææ å®¡æ¹èç¹</view> |
| | | </view> |
| | | </template> |
| | | <view v-else |
| | | class="empty-wrap"> |
| | | <up-empty mode="data" |
| | | text="æªè·åå°æ¨¡æ¿è¯¦æ
" /> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <FooterButtons v-if="!loading && detail" |
| | | cancel-text="è¿å" |
| | | confirm-text="ç¼è¾" |
| | | @cancel="goBack" |
| | | @confirm="goEdit" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import { getApprovalTemplateDetail } from "@/api/oa/approvalTemplate.js"; |
| | | |
| | | const EDIT_STORAGE_KEY = "oa_approve_template_edit_row"; |
| | | const LEVEL_TEXT = ["", "ä¸", "äº", "ä¸", "å", "äº", "å
", "ä¸", "å
«", "ä¹", "å"]; |
| | | |
| | | const FIELD_TYPE_MAP = { |
| | | text: "åè¡ææ¬", |
| | | textarea: "å¤è¡ææ¬", |
| | | number: "æ°å", |
| | | date: "æ¥æ", |
| | | }; |
| | | |
| | | const templateId = ref(""); |
| | | const detail = ref(null); |
| | | const loading = ref(false); |
| | | |
| | | const formConfigData = computed(() => { |
| | | const raw = detail.value?.formConfig; |
| | | if (!raw) return { prompt: "", fields: [] }; |
| | | try { |
| | | const obj = typeof raw === "string" ? JSON.parse(raw) : raw; |
| | | return { |
| | | prompt: obj?.prompt || "", |
| | | fields: Array.isArray(obj?.fields) ? obj.fields : [], |
| | | }; |
| | | } catch { |
| | | return { prompt: "", fields: [] }; |
| | | } |
| | | }); |
| | | |
| | | const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n); |
| | | |
| | | const templateTypeText = type => { |
| | | const val = Number(type); |
| | | if (val === 0) return "ç³»ç»å
ç½®"; |
| | | if (val === 1) return "èªå®ä¹"; |
| | | return "-"; |
| | | }; |
| | | |
| | | const enabledText = enabled => { |
| | | const val = String(enabled ?? ""); |
| | | if (val === "1") return "å¯ç¨"; |
| | | if (val === "0") return "åç¨"; |
| | | return "-"; |
| | | }; |
| | | |
| | | const enabledClass = enabled => { |
| | | const val = String(enabled ?? ""); |
| | | if (val === "1") return "status-on"; |
| | | if (val === "0") return "status-off"; |
| | | return ""; |
| | | }; |
| | | |
| | | const fieldTypeLabel = type => FIELD_TYPE_MAP[type] || type || "-"; |
| | | |
| | | const approveTypeText = type => (type === "OR" ? "æç¾" : "ä¼ç¾"); |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const goEdit = () => { |
| | | if (!templateId.value || !detail.value) return; |
| | | uni.setStorageSync(EDIT_STORAGE_KEY, detail.value); |
| | | uni.navigateTo({ |
| | | url: `/pages/oa/ApproveManage/approve-template/edit?id=${templateId.value}`, |
| | | }); |
| | | }; |
| | | |
| | | const loadDetail = () => { |
| | | if (!templateId.value) return; |
| | | loading.value = true; |
| | | detail.value = null; |
| | | getApprovalTemplateDetail(templateId.value) |
| | | .then(res => { |
| | | detail.value = res?.data || null; |
| | | if (!detail.value) { |
| | | uni.showToast({ title: "æªè·åå°è¯¦æ
", icon: "none" }); |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "è·å详æ
失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | if (options?.id) { |
| | | templateId.value = options.id; |
| | | loadDetail(); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .template-detail-page { |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 100vh; |
| | | background: #f0f3f8; |
| | | } |
| | | |
| | | .detail-scroll { |
| | | flex: 1; |
| | | height: 0; |
| | | padding: 10px 12px calc(96px + env(safe-area-inset-bottom)); |
| | | } |
| | | |
| | | .loading-wrap { |
| | | padding: 48px 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .loading-text { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .section { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | margin-bottom: 10px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05); |
| | | } |
| | | |
| | | .section-title { |
| | | padding: 12px 16px; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #1f2d3d; |
| | | border-bottom: 1px solid #f2f4f7; |
| | | border-left: 3px solid #2979ff; |
| | | padding-left: 13px; |
| | | } |
| | | |
| | | .info-list { |
| | | padding: 4px 0; |
| | | } |
| | | |
| | | .info-item { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | padding: 11px 16px; |
| | | border-bottom: 1px solid #f5f7fa; |
| | | gap: 12px; |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | } |
| | | |
| | | .info-label { |
| | | flex-shrink: 0; |
| | | width: 88px; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .info-value { |
| | | flex: 1; |
| | | font-size: 14px; |
| | | color: #303133; |
| | | text-align: right; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .status-on { |
| | | color: #18a058; |
| | | } |
| | | |
| | | .status-off { |
| | | color: #909399; |
| | | } |
| | | |
| | | .field-block { |
| | | padding: 0 12px 12px; |
| | | } |
| | | |
| | | .field-card { |
| | | padding: 10px 12px; |
| | | margin-bottom: 8px; |
| | | background: #f8fafc; |
| | | border-radius: 8px; |
| | | border: 1px solid #eef2f6; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| | | |
| | | .field-card-head { |
| | | display: flex; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .field-card-name { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | } |
| | | |
| | | .field-tag { |
| | | font-size: 11px; |
| | | padding: 2px 8px; |
| | | border-radius: 4px; |
| | | color: #2979ff; |
| | | background: #ecf5ff; |
| | | |
| | | &--req { |
| | | color: #f56c6c; |
| | | background: #fef0f0; |
| | | } |
| | | } |
| | | |
| | | .field-card-default { |
| | | display: block; |
| | | margin-top: 6px; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .flow-list { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .flow-card { |
| | | padding: 12px; |
| | | margin-bottom: 8px; |
| | | background: #f8fafc; |
| | | border-radius: 8px; |
| | | border: 1px solid #eef2f6; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| | | |
| | | .flow-card-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .flow-level { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .flow-type { |
| | | font-size: 13px; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .approver-tags { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .approver-tag { |
| | | padding: 4px 10px; |
| | | font-size: 13px; |
| | | color: #303133; |
| | | background: #fff; |
| | | border: 1px solid #dce8f8; |
| | | border-radius: 16px; |
| | | } |
| | | |
| | | .empty-hint { |
| | | padding: 12px 16px 16px; |
| | | font-size: 13px; |
| | | color: #909399; |
| | | |
| | | &.inline { |
| | | padding: 0; |
| | | } |
| | | } |
| | | |
| | | .empty-wrap { |
| | | padding: 48px 20px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 审æ¹ç®¡ç / æ°å»ºå®¡æ¹æ¨¡æ¿ |
| | | è·¯ç±ï¼/pages/oa/ApproveManage/approve-template/edit |
| | | --> |
| | | <template> |
| | | <view class="template-edit-page"> |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | |
| | | <scroll-view class="form-scroll" |
| | | scroll-y |
| | | :show-scrollbar="false"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="88" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <u-cell-group title="åºæ¬ä¿¡æ¯" |
| | | class="form-section"> |
| | | <up-form-item label="模æ¿åç§°" |
| | | prop="templateName" |
| | | required |
| | | class="form-item-name"> |
| | | <up-input v-model="form.templateName" |
| | | class="name-input-inline" |
| | | placeholder="请è¾å
¥æ¨¡æ¿åç§°" |
| | | maxlength="50" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="模æ¿ç±»å" |
| | | prop="templateType" |
| | | required |
| | | class="form-item-type"> |
| | | <up-radio-group v-model="form.templateType" |
| | | class="type-radio-group" |
| | | placement="row" |
| | | @change="onTemplateTypeChange"> |
| | | <up-radio v-for="opt in TEMPLATE_TYPE_OPTIONS" |
| | | :key="opt.value" |
| | | :name="opt.value" |
| | | :label="opt.name" /> |
| | | </up-radio-group> |
| | | </up-form-item> |
| | | <up-form-item label="å¯ç¨ç¶æ" |
| | | class="form-item-switch"> |
| | | <view class="switch-wrap"> |
| | | <up-switch v-model="enabledBool" /> |
| | | </view> |
| | | </up-form-item> |
| | | <up-form-item label="模æ¿è¯´æ" |
| | | class="form-item-desc" |
| | | label-position="top"> |
| | | <view class="desc-input-shell"> |
| | | <up-textarea v-model="form.description" |
| | | placeholder="éå¡«" |
| | | maxlength="200" |
| | | border="none" |
| | | height="72" /> |
| | | </view> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | |
| | | <view class="section-card"> |
| | | <view class="section-head section-head--between"> |
| | | <text class="section-title">å¡«æ¥é
ç½®</text> |
| | | <view class="head-actions"> |
| | | <text class="head-link" |
| | | @click="showPresetSheet = true">é¢è®¾</text> |
| | | <text class="head-link head-link--primary" |
| | | @click="openFieldEditor()">æ·»å </text> |
| | | </view> |
| | | </view> |
| | | <view class="section-body"> |
| | | <view class="prompt-row"> |
| | | <text class="prompt-label">å¡«æ¥æç¤º</text> |
| | | <up-input v-model="formConfig.prompt" |
| | | class="prompt-input" |
| | | placeholder="éå¡«" |
| | | maxlength="200" |
| | | clearable /> |
| | | </view> |
| | | <view v-if="formConfig.fields.length" |
| | | class="field-list"> |
| | | <view v-for="(field, index) in formConfig.fields" |
| | | :key="field.key" |
| | | class="field-item"> |
| | | <view class="field-main"> |
| | | <view class="field-title-row"> |
| | | <text class="field-name">{{ field.label }}</text> |
| | | <text class="type-tag" |
| | | :class="fieldTypeTagClass(field.type)"> |
| | | {{ fieldTypeLabel(field.type) }} |
| | | </text> |
| | | <text v-if="field.required" |
| | | class="req-tag">å¿
å¡«</text> |
| | | </view> |
| | | <text v-if="field.defaultValue" |
| | | class="field-default">é»è®¤ï¼{{ field.defaultValue }}</text> |
| | | </view> |
| | | <view class="field-actions"> |
| | | <view class="icon-btn icon-btn--edit" |
| | | @click="openFieldEditor(field, index)"> |
| | | <up-icon name="edit-pen" |
| | | size="16" |
| | | color="#2979ff" /> |
| | | </view> |
| | | <view class="icon-btn icon-btn--del" |
| | | @click="removeField(index)"> |
| | | <up-icon name="trash" |
| | | size="16" |
| | | color="#f56c6c" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="empty-mini"> |
| | | <text>ææ å¡«æ¥é¡¹</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="section-card"> |
| | | <view class="section-head"> |
| | | <text class="section-title">å®¡æ¹æµç¨</text> |
| | | </view> |
| | | <view class="flow-wrap"> |
| | | <view v-for="(node, nodeIndex) in flowNodes" |
| | | :key="node._key" |
| | | class="flow-node-block"> |
| | | <view class="flow-node-card"> |
| | | <view class="node-header"> |
| | | <view class="node-level-badge">{{ nodeIndex + 1 }}</view> |
| | | <text class="node-level-text">第{{ levelLabel(nodeIndex + 1) }}级</text> |
| | | <view v-if="flowNodes.length > 1" |
| | | class="node-delete" |
| | | @click="removeNode(nodeIndex)"> |
| | | <up-icon name="trash" |
| | | size="16" |
| | | color="#f56c6c" /> |
| | | </view> |
| | | </view> |
| | | <view class="approve-type-row"> |
| | | <view class="type-btn" |
| | | :class="{ active: node.approveType === 'AND' }" |
| | | @click="node.approveType = 'AND'"> |
| | | ä¼ç¾ |
| | | </view> |
| | | <view class="type-btn" |
| | | :class="{ active: node.approveType === 'OR' }" |
| | | @click="node.approveType = 'OR'"> |
| | | æç¾ |
| | | </view> |
| | | </view> |
| | | <view class="approver-list"> |
| | | <view v-for="(approver, approverIndex) in node.approvers" |
| | | :key="`${node._key}-${approver.approverId}-${approverIndex}`" |
| | | class="approver-chip"> |
| | | <view class="approver-avatar">{{ (approver.approverName || "?").charAt(0) }}</view> |
| | | <text class="approver-name">{{ approver.approverName }}</text> |
| | | <view class="approver-remove" |
| | | hover-class="approver-remove--active" |
| | | @tap.stop="removeApprover(nodeIndex, approverIndex)" |
| | | @click.stop="removeApprover(nodeIndex, approverIndex)"> |
| | | <text class="remove-icon">Ã</text> |
| | | </view> |
| | | </view> |
| | | <view class="add-approver" |
| | | @click="openUserPicker(nodeIndex)"> |
| | | <up-icon name="plus" |
| | | size="14" |
| | | color="#2979ff" /> |
| | | <text>æ·»å </text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-if="nodeIndex < flowNodes.length - 1" |
| | | class="flow-connector"> |
| | | <view class="flow-connector-line" /> |
| | | </view> |
| | | </view> |
| | | <view class="add-node-bar" |
| | | @click="addNode"> |
| | | <up-icon name="plus-circle" |
| | | size="20" |
| | | color="#2979ff" /> |
| | | <text>æ·»å 级次</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </up-form> |
| | | </scroll-view> |
| | | |
| | | <FooterButtons :loading="submitting" |
| | | confirm-text="ä¿å" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | |
| | | <up-action-sheet :show="showPresetSheet" |
| | | title="ä»é¢è®¾å¯¼å
¥" |
| | | :actions="presetActions" |
| | | @select="onSelectPreset" |
| | | @close="showPresetSheet = false" /> |
| | | |
| | | <up-popup :show="showFieldEditor" |
| | | mode="bottom" |
| | | round="16" |
| | | @close="closeFieldEditor"> |
| | | <view class="field-editor"> |
| | | <view class="sheet-handle" /> |
| | | <text class="editor-title">{{ editingFieldIndex >= 0 ? "ç¼è¾å¡«æ¥é¡¹" : "æ·»å å¡«æ¥é¡¹" }}</text> |
| | | <view class="editor-form"> |
| | | <view class="editor-row"> |
| | | <text class="editor-label required">åæ®µåç§°</text> |
| | | <up-input v-model="fieldDraft.label" |
| | | placeholder="请è¾å
¥" |
| | | clearable /> |
| | | </view> |
| | | <view class="editor-row editor-row--block"> |
| | | <text class="editor-label required">åæ®µç±»å</text> |
| | | <view class="type-chip-grid"> |
| | | <view v-for="opt in FIELD_TYPE_OPTIONS" |
| | | :key="opt.value" |
| | | class="type-chip" |
| | | :class="{ active: fieldDraft.type === opt.value }" |
| | | @click="selectFieldType(opt.value)"> |
| | | {{ opt.name }} |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view class="editor-row editor-row--block"> |
| | | <text class="editor-label">é»è®¤å¼</text> |
| | | <up-textarea v-if="fieldDraft.type === 'textarea'" |
| | | v-model="fieldDraft.defaultValue" |
| | | placeholder="éå¡«" |
| | | maxlength="500" |
| | | height="72" /> |
| | | <view v-else-if="fieldDraft.type === 'date'" |
| | | class="default-date-row" |
| | | @click="showDefaultDatePicker = true"> |
| | | <up-input :model-value="fieldDraft.defaultValue" |
| | | placeholder="éæ©æ¥æ" |
| | | readonly /> |
| | | <up-icon name="calendar" |
| | | size="18" |
| | | color="#909399" /> |
| | | </view> |
| | | <up-input v-else |
| | | v-model="fieldDraft.defaultValue" |
| | | :type="fieldDraft.type === 'number' ? 'digit' : 'text'" |
| | | placeholder="éå¡«" |
| | | clearable /> |
| | | </view> |
| | | <view class="editor-row editor-row--switch"> |
| | | <text class="editor-label">æ¯å¦å¿
å¡«</text> |
| | | <up-switch v-model="fieldDraft.required" /> |
| | | </view> |
| | | </view> |
| | | <view class="editor-footer"> |
| | | <view class="editor-btn editor-btn--cancel" |
| | | @click="closeFieldEditor">åæ¶</view> |
| | | <view class="editor-btn editor-btn--confirm" |
| | | @click="confirmFieldEditor">ç¡®å®</view> |
| | | </view> |
| | | </view> |
| | | </up-popup> |
| | | |
| | | <up-popup :show="showDefaultDatePicker" |
| | | mode="bottom" |
| | | @close="showDefaultDatePicker = false"> |
| | | <up-datetime-picker :show="true" |
| | | v-model="defaultDateTs" |
| | | mode="date" |
| | | @confirm="onDefaultDateConfirm" |
| | | @cancel="showDefaultDatePicker = false" /> |
| | | </up-popup> |
| | | |
| | | <up-popup :show="showUserPicker" |
| | | mode="bottom" |
| | | round="16" |
| | | @close="closeUserPicker"> |
| | | <view class="user-picker"> |
| | | <view class="sheet-handle" /> |
| | | <view class="picker-head"> |
| | | <text class="picker-cancel" |
| | | @click="closeUserPicker">åæ¶</text> |
| | | <text class="picker-title">鿩审æ¹äºº</text> |
| | | <text class="picker-confirm" |
| | | @click="confirmUserPicker"> |
| | | ç¡®å®{{ pickerSelectedIds.length ? `(${pickerSelectedIds.length})` : "" }} |
| | | </text> |
| | | </view> |
| | | <scroll-view class="user-scroll" |
| | | scroll-y> |
| | | <view v-for="user in availableUsers" |
| | | :key="user.userId" |
| | | class="user-item" |
| | | :class="{ selected: isUserSelected(user.userId) }" |
| | | @click="toggleUser(user)"> |
| | | <view class="user-avatar">{{ (user.nickName || "?").charAt(0) }}</view> |
| | | <text class="user-name">{{ user.nickName }}</text> |
| | | <view class="user-check" |
| | | :class="{ checked: isUserSelected(user.userId) }"> |
| | | <up-icon v-if="isUserSelected(user.userId)" |
| | | name="checkmark" |
| | | size="14" |
| | | color="#fff" /> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | </view> |
| | | </up-popup> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, onMounted, reactive, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import { |
| | | addApprovalTemplate, |
| | | updateApprovalTemplate, |
| | | } from "@/api/oa/approvalTemplate.js"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user"; |
| | | import { formatDateToYMD } from "@/utils/ruoyi"; |
| | | |
| | | const EDIT_STORAGE_KEY = "oa_approve_template_edit_row"; |
| | | |
| | | const LEVEL_TEXT = ["", "ä¸", "äº", "ä¸", "å", "äº", "å
", "ä¸", "å
«", "ä¹", "å"]; |
| | | |
| | | const FORM_PRESETS = [ |
| | | { |
| | | name: "éç¨æ¥é", |
| | | prompt: "è¯·å¡«åæ¥éäºç±ãéé¢ç", |
| | | fields: [ |
| | | { key: "reason", label: "æ¥éäºç±", type: "textarea", required: true }, |
| | | { key: "amount", label: "æ¥ééé¢(å
)", type: "number", required: true }, |
| | | { key: "applyDate", label: "ç³è¯·æ¥æ", type: "date", required: true }, |
| | | ], |
| | | }, |
| | | { |
| | | name: "请åç³è¯·", |
| | | prompt: "请填å请åç±»åãèµ·æ¢æ¶é´ç", |
| | | fields: [ |
| | | { key: "leaveType", label: "请åç±»å", type: "text", required: true }, |
| | | { key: "startTime", label: "å¼å§æ¶é´", type: "date", required: true }, |
| | | { key: "endTime", label: "ç»ææ¶é´", type: "date", required: true }, |
| | | { key: "reason", label: "请åäºç±", type: "textarea", required: true }, |
| | | ], |
| | | }, |
| | | { |
| | | name: "éè´ç³è¯·", |
| | | prompt: "请填åéè´äºç±ãé¢ä¼°éé¢ç", |
| | | fields: [ |
| | | { key: "title", label: "éè´äºç±", type: "textarea", required: true }, |
| | | { key: "amount", label: "é¢ä¼°éé¢(å
)", type: "number", required: true }, |
| | | ], |
| | | }, |
| | | ]; |
| | | |
| | | const FIELD_TYPE_OPTIONS = [ |
| | | { name: "åè¡ææ¬", value: "text" }, |
| | | { name: "å¤è¡ææ¬", value: "textarea" }, |
| | | { name: "æ°å", value: "number" }, |
| | | { name: "æ¥æ", value: "date" }, |
| | | ]; |
| | | |
| | | const formRef = ref(); |
| | | const submitting = ref(false); |
| | | const userList = ref([]); |
| | | const templateId = ref(null); |
| | | |
| | | const showPresetSheet = ref(false); |
| | | const showFieldEditor = ref(false); |
| | | const showUserPicker = ref(false); |
| | | const showDefaultDatePicker = ref(false); |
| | | const defaultDateTs = ref(Date.now()); |
| | | |
| | | const editingFieldIndex = ref(-1); |
| | | const editingNodeIndex = ref(-1); |
| | | const pickerSelectedIds = ref([]); |
| | | |
| | | const form = reactive({ |
| | | templateName: "", |
| | | templateType: 1, |
| | | enabled: "1", |
| | | description: "", |
| | | }); |
| | | |
| | | const formConfig = reactive({ |
| | | prompt: "", |
| | | fields: [], |
| | | }); |
| | | |
| | | const fieldDraft = reactive({ |
| | | label: "", |
| | | type: "text", |
| | | defaultValue: "", |
| | | required: true, |
| | | }); |
| | | |
| | | let nodeKeySeed = 1; |
| | | |
| | | const createNode = () => ({ |
| | | _key: `node_${nodeKeySeed++}`, |
| | | approveType: "AND", |
| | | approvers: [], |
| | | }); |
| | | |
| | | const flowNodes = ref([createNode()]); |
| | | |
| | | const rules = { |
| | | templateName: [{ required: true, message: "请è¾å
¥æ¨¡æ¿åç§°", trigger: "blur" }], |
| | | templateType: [ |
| | | { |
| | | validator: (_rule, value, callback) => { |
| | | if (value === "" || value === null || value === undefined) { |
| | | callback(new Error("è¯·éæ©æ¨¡æ¿ç±»å")); |
| | | return; |
| | | } |
| | | callback(); |
| | | }, |
| | | trigger: "change", |
| | | }, |
| | | ], |
| | | }; |
| | | |
| | | const TEMPLATE_TYPE_OPTIONS = [ |
| | | { name: "ç³»ç»å
ç½®", value: 0 }, |
| | | { name: "èªå®ä¹", value: 1 }, |
| | | ]; |
| | | |
| | | const presetActions = FORM_PRESETS.map(item => ({ |
| | | name: item.name, |
| | | value: item.name, |
| | | })); |
| | | |
| | | const enabledBool = computed({ |
| | | get: () => form.enabled === "1", |
| | | set: val => { |
| | | form.enabled = val ? "1" : "0"; |
| | | }, |
| | | }); |
| | | |
| | | const isEditMode = computed(() => templateId.value != null && templateId.value !== ""); |
| | | |
| | | const pageTitle = computed(() => |
| | | isEditMode.value ? "ç¼è¾å®¡æ¹æ¨¡æ¿" : "æ°å»ºå®¡æ¹æ¨¡æ¿" |
| | | ); |
| | | |
| | | const parseFormConfig = raw => { |
| | | if (!raw) return { prompt: "", fields: [] }; |
| | | try { |
| | | const obj = typeof raw === "string" ? JSON.parse(raw) : raw; |
| | | return { |
| | | prompt: obj?.prompt || "", |
| | | fields: Array.isArray(obj?.fields) ? obj.fields.map(f => ({ ...f })) : [], |
| | | }; |
| | | } catch { |
| | | return { prompt: "", fields: [] }; |
| | | } |
| | | }; |
| | | |
| | | const mapNodesFromRow = nodes => { |
| | | if (!Array.isArray(nodes) || !nodes.length) { |
| | | return [createNode()]; |
| | | } |
| | | return nodes.map(node => ({ |
| | | _key: `node_${nodeKeySeed++}`, |
| | | id: node.id, |
| | | templateId: node.templateId, |
| | | approveType: node.approveType || "AND", |
| | | approvers: (node.approvers || []).map((approver, idx) => ({ |
| | | id: approver.id, |
| | | nodeId: approver.nodeId, |
| | | templateId: approver.templateId, |
| | | approverId: approver.approverId, |
| | | approverName: approver.approverName, |
| | | sortNo: approver.sortNo ?? idx + 1, |
| | | })), |
| | | })); |
| | | }; |
| | | |
| | | const fillFormFromRow = row => { |
| | | if (!row) return; |
| | | templateId.value = row.id; |
| | | form.templateName = row.templateName || ""; |
| | | form.templateType = |
| | | row.templateType === 0 || row.templateType === 1 |
| | | ? row.templateType |
| | | : Number(row.templateType) || 1; |
| | | form.enabled = String(row.enabled ?? "1"); |
| | | form.description = row.description || ""; |
| | | |
| | | const config = parseFormConfig(row.formConfig); |
| | | formConfig.prompt = config.prompt; |
| | | formConfig.fields = config.fields; |
| | | flowNodes.value = mapNodesFromRow(row.nodes); |
| | | }; |
| | | |
| | | const availableUsers = computed(() => { |
| | | const node = flowNodes.value[editingNodeIndex.value]; |
| | | if (!node) return userList.value; |
| | | const selectedIds = new Set(node.approvers.map(a => a.approverId)); |
| | | return userList.value.filter(user => !selectedIds.has(user.userId)); |
| | | }); |
| | | |
| | | const levelLabel = n => LEVEL_TEXT[n] || String(n); |
| | | |
| | | const fieldTypeLabel = type => |
| | | FIELD_TYPE_OPTIONS.find(item => item.value === type)?.name || type; |
| | | |
| | | const fieldTypeTagClass = type => { |
| | | const map = { |
| | | text: "type-tag--text", |
| | | textarea: "type-tag--area", |
| | | number: "type-tag--num", |
| | | date: "type-tag--date", |
| | | }; |
| | | return map[type] || "type-tag--text"; |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const onTemplateTypeChange = () => { |
| | | formRef.value?.validateField?.("templateType"); |
| | | }; |
| | | |
| | | const onSelectPreset = action => { |
| | | const preset = FORM_PRESETS.find(item => item.name === action.value); |
| | | if (!preset) return; |
| | | formConfig.prompt = preset.prompt; |
| | | formConfig.fields = preset.fields.map(field => ({ ...field })); |
| | | showPresetSheet.value = false; |
| | | uni.showToast({ title: "已导å
¥é¢è®¾", icon: "success" }); |
| | | }; |
| | | |
| | | const selectFieldType = type => { |
| | | if (fieldDraft.type === type) return; |
| | | fieldDraft.type = type; |
| | | fieldDraft.defaultValue = ""; |
| | | }; |
| | | |
| | | const onDefaultDateConfirm = e => { |
| | | fieldDraft.defaultValue = formatDateToYMD(e.value); |
| | | showDefaultDatePicker.value = false; |
| | | }; |
| | | |
| | | const openFieldEditor = (field, index = -1) => { |
| | | editingFieldIndex.value = index; |
| | | if (field) { |
| | | fieldDraft.label = field.label; |
| | | fieldDraft.type = field.type || "text"; |
| | | fieldDraft.defaultValue = field.defaultValue ?? ""; |
| | | fieldDraft.required = !!field.required; |
| | | } else { |
| | | fieldDraft.label = ""; |
| | | fieldDraft.type = "text"; |
| | | fieldDraft.defaultValue = ""; |
| | | fieldDraft.required = true; |
| | | } |
| | | if (fieldDraft.type === "date" && fieldDraft.defaultValue) { |
| | | const parsed = Date.parse(fieldDraft.defaultValue); |
| | | defaultDateTs.value = Number.isNaN(parsed) ? Date.now() : parsed; |
| | | } else { |
| | | defaultDateTs.value = Date.now(); |
| | | } |
| | | showFieldEditor.value = true; |
| | | }; |
| | | |
| | | const closeFieldEditor = () => { |
| | | showFieldEditor.value = false; |
| | | editingFieldIndex.value = -1; |
| | | }; |
| | | |
| | | const buildFieldKey = label => { |
| | | const base = (label || "field") |
| | | .trim() |
| | | .replace(/\s+/g, "_") |
| | | .replace(/[^\w\u4e00-\u9fa5]/g, ""); |
| | | let key = base || "field"; |
| | | let index = 1; |
| | | while (formConfig.fields.some((item, idx) => item.key === key && idx !== editingFieldIndex.value)) { |
| | | key = `${base}_${index++}`; |
| | | } |
| | | return key; |
| | | }; |
| | | |
| | | const confirmFieldEditor = () => { |
| | | if (!fieldDraft.label?.trim()) { |
| | | uni.showToast({ title: "请è¾å
¥å段åç§°", icon: "none" }); |
| | | return; |
| | | } |
| | | const defaultValue = String(fieldDraft.defaultValue ?? "").trim(); |
| | | const existingKey = |
| | | editingFieldIndex.value >= 0 |
| | | ? formConfig.fields[editingFieldIndex.value]?.key |
| | | : null; |
| | | const payload = { |
| | | key: existingKey || buildFieldKey(fieldDraft.label), |
| | | label: fieldDraft.label.trim(), |
| | | type: fieldDraft.type, |
| | | required: !!fieldDraft.required, |
| | | defaultValue, |
| | | }; |
| | | if (editingFieldIndex.value >= 0) { |
| | | formConfig.fields.splice(editingFieldIndex.value, 1, payload); |
| | | } else { |
| | | formConfig.fields.push(payload); |
| | | } |
| | | closeFieldEditor(); |
| | | }; |
| | | |
| | | const removeField = index => { |
| | | formConfig.fields.splice(index, 1); |
| | | }; |
| | | |
| | | const addNode = () => { |
| | | flowNodes.value.push(createNode()); |
| | | }; |
| | | |
| | | const removeNode = index => { |
| | | if (flowNodes.value.length <= 1) { |
| | | uni.showToast({ title: "è³å°ä¿çä¸ä¸ªå®¡æ¹èç¹", icon: "none" }); |
| | | return; |
| | | } |
| | | flowNodes.value.splice(index, 1); |
| | | }; |
| | | |
| | | const openUserPicker = nodeIndex => { |
| | | editingNodeIndex.value = nodeIndex; |
| | | pickerSelectedIds.value = []; |
| | | showUserPicker.value = true; |
| | | }; |
| | | |
| | | const closeUserPicker = () => { |
| | | showUserPicker.value = false; |
| | | editingNodeIndex.value = -1; |
| | | pickerSelectedIds.value = []; |
| | | }; |
| | | |
| | | const isUserSelected = userId => pickerSelectedIds.value.includes(userId); |
| | | |
| | | const toggleUser = user => { |
| | | const ids = pickerSelectedIds.value; |
| | | const index = ids.indexOf(user.userId); |
| | | if (index >= 0) { |
| | | ids.splice(index, 1); |
| | | } else { |
| | | ids.push(user.userId); |
| | | } |
| | | }; |
| | | |
| | | const confirmUserPicker = () => { |
| | | const node = flowNodes.value[editingNodeIndex.value]; |
| | | if (!node) { |
| | | closeUserPicker(); |
| | | return; |
| | | } |
| | | const selectedUsers = userList.value.filter(user => |
| | | pickerSelectedIds.value.includes(user.userId) |
| | | ); |
| | | if (!selectedUsers.length) { |
| | | uni.showToast({ title: "è¯·éæ©å®¡æ¹äºº", icon: "none" }); |
| | | return; |
| | | } |
| | | const startSort = node.approvers.length; |
| | | selectedUsers.forEach((user, idx) => { |
| | | node.approvers.push({ |
| | | approverId: user.userId, |
| | | approverName: user.nickName, |
| | | sortNo: startSort + idx + 1, |
| | | }); |
| | | }); |
| | | closeUserPicker(); |
| | | }; |
| | | |
| | | const removeApprover = (nodeIndex, approverIndex) => { |
| | | const node = flowNodes.value[nodeIndex]; |
| | | if (!node?.approvers?.length) return; |
| | | const next = node.approvers |
| | | .filter((_, idx) => idx !== approverIndex) |
| | | .map((item, idx) => ({ |
| | | ...item, |
| | | sortNo: idx + 1, |
| | | })); |
| | | node.approvers = next; |
| | | }; |
| | | |
| | | const validateFlow = () => { |
| | | if (!flowNodes.value.length) { |
| | | uni.showToast({ title: "请é
ç½®å®¡æ¹æµç¨", icon: "none" }); |
| | | return false; |
| | | } |
| | | const emptyNode = flowNodes.value.find(node => !node.approvers.length); |
| | | if (emptyNode) { |
| | | uni.showToast({ title: "请为æ¯ä¸ªå®¡æ¹èç¹æ·»å 审æ¹äºº", icon: "none" }); |
| | | return false; |
| | | } |
| | | return true; |
| | | }; |
| | | |
| | | const buildSubmitPayload = () => { |
| | | const tid = templateId.value; |
| | | const payload = { |
| | | templateName: form.templateName.trim(), |
| | | enabled: form.enabled, |
| | | description: form.description?.trim() || "", |
| | | templateType: form.templateType, |
| | | formConfig: JSON.stringify({ |
| | | prompt: formConfig.prompt?.trim() || "", |
| | | fields: formConfig.fields, |
| | | }), |
| | | nodes: flowNodes.value.map((node, index) => { |
| | | const nodePayload = { |
| | | levelNo: index + 1, |
| | | approveType: node.approveType, |
| | | approvers: node.approvers.map((approver, approverIndex) => { |
| | | const approverPayload = { |
| | | approverId: approver.approverId, |
| | | approverName: approver.approverName, |
| | | sortNo: approverIndex + 1, |
| | | }; |
| | | if (isEditMode.value) { |
| | | if (approver.id != null) approverPayload.id = approver.id; |
| | | if (approver.nodeId != null) approverPayload.nodeId = approver.nodeId; |
| | | else if (node.id != null) approverPayload.nodeId = node.id; |
| | | if (approver.templateId != null) approverPayload.templateId = approver.templateId; |
| | | else if (tid != null) approverPayload.templateId = tid; |
| | | } |
| | | return approverPayload; |
| | | }), |
| | | }; |
| | | if (isEditMode.value) { |
| | | if (node.id != null) nodePayload.id = node.id; |
| | | if (node.templateId != null) nodePayload.templateId = node.templateId; |
| | | else if (tid != null) nodePayload.templateId = tid; |
| | | } |
| | | return nodePayload; |
| | | }), |
| | | }; |
| | | |
| | | if (isEditMode.value) { |
| | | payload.id = tid; |
| | | } |
| | | |
| | | return payload; |
| | | }; |
| | | |
| | | const handleSubmit = async () => { |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid || !validateFlow()) return; |
| | | |
| | | submitting.value = true; |
| | | const submitApi = isEditMode.value ? updateApprovalTemplate : addApprovalTemplate; |
| | | submitApi(buildSubmitPayload()) |
| | | .then(() => { |
| | | uni.showToast({ |
| | | title: isEditMode.value ? "ä¿®æ¹æå" : "ä¿åæå", |
| | | icon: "success", |
| | | }); |
| | | uni.removeStorageSync(EDIT_STORAGE_KEY); |
| | | setTimeout(() => { |
| | | uni.navigateBack(); |
| | | }, 300); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ |
| | | title: isEditMode.value ? "ä¿®æ¹å¤±è´¥" : "ä¿å失败", |
| | | icon: "none", |
| | | }); |
| | | }) |
| | | .finally(() => { |
| | | submitting.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | if (options?.id) { |
| | | const row = uni.getStorageSync(EDIT_STORAGE_KEY); |
| | | if (row && String(row.id) === String(options.id)) { |
| | | fillFormFromRow(row); |
| | | } else { |
| | | templateId.value = options.id; |
| | | uni.showToast({ title: "æªè·åå°æ¨¡æ¿æ°æ®", icon: "none" }); |
| | | } |
| | | uni.removeStorageSync(EDIT_STORAGE_KEY); |
| | | } |
| | | }); |
| | | |
| | | onMounted(() => { |
| | | userListNoPageByTenantId() |
| | | .then(res => { |
| | | userList.value = res?.data || []; |
| | | }) |
| | | .catch(() => { |
| | | userList.value = []; |
| | | }); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | $primary: #2979ff; |
| | | $primary-light: #ecf5ff; |
| | | $text: #1f2d3d; |
| | | $text-secondary: #606266; |
| | | $text-muted: #909399; |
| | | $border: #ebeef5; |
| | | $bg-page: #f0f3f8; |
| | | $radius-lg: 12px; |
| | | $radius-md: 10px; |
| | | $shadow-card: 0 2px 12px rgba(31, 45, 61, 0.05); |
| | | |
| | | .template-edit-page { |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 100vh; |
| | | background: $bg-page; |
| | | } |
| | | |
| | | .form-scroll { |
| | | flex: 1; |
| | | height: 0; |
| | | padding: 10px 12px calc(96px + env(safe-area-inset-bottom)); |
| | | } |
| | | |
| | | .section-card { |
| | | margin-bottom: 10px; |
| | | background: #fff; |
| | | border-radius: $radius-lg; |
| | | overflow: hidden; |
| | | box-shadow: $shadow-card; |
| | | } |
| | | |
| | | .section-head { |
| | | padding: 12px 16px; |
| | | border-bottom: 1px solid #f2f4f7; |
| | | |
| | | &--between { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | padding-left: 10px; |
| | | border-left: 3px solid $primary; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .head-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .head-link { |
| | | font-size: 14px; |
| | | color: $text-secondary; |
| | | |
| | | &--primary { |
| | | color: $primary; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | |
| | | .section-body { |
| | | padding: 2px 16px 14px; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 10px; |
| | | border-radius: $radius-lg; |
| | | overflow: hidden; |
| | | box-shadow: $shadow-card; |
| | | } |
| | | |
| | | :deep(.form-section .u-cell-group__title) { |
| | | padding: 12px 16px 8px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: $text !important; |
| | | background: #fff !important; |
| | | } |
| | | |
| | | :deep(.form-section .u-form-item) { |
| | | padding: 0 16px !important; |
| | | } |
| | | |
| | | :deep(.form-section .u-form-item__body) { |
| | | padding: 10px 0 !important; |
| | | min-height: auto !important; |
| | | } |
| | | |
| | | :deep(.form-item-name .u-form-item__body) { |
| | | flex-direction: row !important; |
| | | align-items: center !important; |
| | | } |
| | | |
| | | :deep(.form-item-name .u-form-item__content) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | justify-content: flex-end !important; |
| | | } |
| | | |
| | | :deep(.name-input-inline), |
| | | :deep(.name-input-inline .u-input__content) { |
| | | width: 100% !important; |
| | | flex: 1 !important; |
| | | } |
| | | |
| | | :deep(.name-input-inline input), |
| | | :deep(.name-input-inline .u-input__content__field-wrapper__field) { |
| | | width: 100% !important; |
| | | text-align: right !important; |
| | | font-size: 15px !important; |
| | | } |
| | | |
| | | :deep(.form-item-type .u-form-item__body) { |
| | | align-items: center !important; |
| | | } |
| | | |
| | | .type-radio-group { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | flex-wrap: nowrap; |
| | | } |
| | | |
| | | :deep(.type-radio-group .u-radio) { |
| | | margin-left: 20px; |
| | | } |
| | | |
| | | :deep(.form-item-switch .u-form-item__body) { |
| | | flex-direction: row !important; |
| | | align-items: center !important; |
| | | } |
| | | |
| | | :deep(.form-item-switch .u-form-item__content) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | display: flex !important; |
| | | justify-content: flex-end !important; |
| | | } |
| | | |
| | | .switch-wrap { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | align-items: center; |
| | | width: 100%; |
| | | } |
| | | |
| | | :deep(.form-item-desc .u-form-item__body) { |
| | | flex-direction: column !important; |
| | | align-items: stretch !important; |
| | | padding: 10px 0 12px !important; |
| | | } |
| | | |
| | | :deep(.form-item-desc .u-form-item__content) { |
| | | justify-content: stretch !important; |
| | | width: 100% !important; |
| | | } |
| | | |
| | | .desc-input-shell { |
| | | width: 100%; |
| | | box-sizing: border-box; |
| | | padding: 8px 12px; |
| | | background: #fff; |
| | | border: 1px solid #dcdfe6; |
| | | border-radius: 6px; |
| | | } |
| | | |
| | | :deep(.desc-input-shell .u-textarea), |
| | | :deep(.desc-input-shell textarea) { |
| | | width: 100% !important; |
| | | font-size: 15px !important; |
| | | text-align: left !important; |
| | | } |
| | | |
| | | .form-row-item { |
| | | margin: 0 !important; |
| | | padding: 0 !important; |
| | | } |
| | | |
| | | :deep(.form-row-item .u-form-item__body) { |
| | | padding: 0; |
| | | } |
| | | |
| | | :deep(.form-row-item .u-form-item__body__right__message) { |
| | | margin-top: 4px; |
| | | padding-left: 0; |
| | | } |
| | | |
| | | .form-row { |
| | | padding: 10px 0; |
| | | border-bottom: 1px solid #f5f7fa; |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | &--column { |
| | | flex-direction: column; |
| | | align-items: stretch; |
| | | gap: 8px; |
| | | } |
| | | |
| | | &--compact { |
| | | padding-top: 8px; |
| | | } |
| | | } |
| | | |
| | | .form-row-label { |
| | | display: block; |
| | | font-size: 14px; |
| | | color: $text-secondary; |
| | | margin-bottom: 8px; |
| | | |
| | | &.required::before { |
| | | content: "*"; |
| | | color: #f56c6c; |
| | | margin-right: 3px; |
| | | } |
| | | } |
| | | |
| | | .form-row--column .form-row-label { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .prompt-row { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 12px 0; |
| | | margin-bottom: 4px; |
| | | border-bottom: 1px solid #f5f7fa; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .prompt-label { |
| | | flex-shrink: 0; |
| | | width: 88px; |
| | | font-size: 14px; |
| | | color: $text-secondary; |
| | | } |
| | | |
| | | .prompt-input { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | :deep(.prompt-input), |
| | | :deep(.prompt-input .u-input__content) { |
| | | width: 100% !important; |
| | | } |
| | | |
| | | :deep(.prompt-input input), |
| | | :deep(.prompt-input .u-input__content__field-wrapper__field) { |
| | | width: 100% !important; |
| | | text-align: right !important; |
| | | font-size: 15px !important; |
| | | } |
| | | |
| | | .input-box, |
| | | .textarea-box { |
| | | background: #f7f9fc; |
| | | border-radius: 10px; |
| | | border: 1px solid #eef1f6; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .textarea-box { |
| | | padding: 4px 0; |
| | | } |
| | | |
| | | .field-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | .field-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | padding: 10px 12px; |
| | | background: #f8fafc; |
| | | border-radius: $radius-md; |
| | | border: 1px solid #eef2f6; |
| | | } |
| | | |
| | | .field-main { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .field-title-row { |
| | | display: flex; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .field-name { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | } |
| | | |
| | | .type-tag { |
| | | font-size: 11px; |
| | | padding: 2px 8px; |
| | | border-radius: 4px; |
| | | |
| | | &--text { |
| | | color: #2979ff; |
| | | background: #ecf5ff; |
| | | } |
| | | |
| | | &--area { |
| | | color: #7c5cfc; |
| | | background: #f3efff; |
| | | } |
| | | |
| | | &--num { |
| | | color: #e6a23c; |
| | | background: #fdf6ec; |
| | | } |
| | | |
| | | &--date { |
| | | color: #18a058; |
| | | background: #e8faf0; |
| | | } |
| | | } |
| | | |
| | | .req-tag { |
| | | font-size: 11px; |
| | | padding: 2px 6px; |
| | | color: #f56c6c; |
| | | background: #fef0f0; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .field-default { |
| | | display: block; |
| | | margin-top: 4px; |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | } |
| | | |
| | | .field-actions { |
| | | display: flex; |
| | | gap: 6px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .icon-btn { |
| | | width: 32px; |
| | | height: 32px; |
| | | border-radius: 8px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | |
| | | &--edit { |
| | | background: #ecf5ff; |
| | | } |
| | | |
| | | &--del { |
| | | background: #fef0f0; |
| | | } |
| | | } |
| | | |
| | | .empty-mini { |
| | | padding: 20px 0; |
| | | text-align: center; |
| | | font-size: 13px; |
| | | color: $text-muted; |
| | | } |
| | | |
| | | .flow-wrap { |
| | | padding: 10px 16px 14px; |
| | | } |
| | | |
| | | .flow-node-block { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | .flow-node-card { |
| | | background: #fafbfd; |
| | | border: 1px solid #e8eef5; |
| | | border-radius: $radius-md; |
| | | padding: 12px; |
| | | } |
| | | |
| | | .node-header { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .node-level-badge { |
| | | width: 26px; |
| | | height: 26px; |
| | | border-radius: 8px; |
| | | background: $primary; |
| | | color: #fff; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .node-level-text { |
| | | flex: 1; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | } |
| | | |
| | | .node-delete { |
| | | padding: 6px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .approve-type-row { |
| | | display: flex; |
| | | background: #f0f3f8; |
| | | border-radius: 8px; |
| | | padding: 3px; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .type-btn { |
| | | flex: 1; |
| | | text-align: center; |
| | | padding: 8px 0; |
| | | font-size: 14px; |
| | | color: $text-secondary; |
| | | border-radius: 6px; |
| | | |
| | | &.active { |
| | | background: #fff; |
| | | color: $primary; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | |
| | | .approver-list { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .approver-chip { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 6px 12px 6px 6px; |
| | | background: #fff; |
| | | border: 1px solid #dce8f8; |
| | | border-radius: 24px; |
| | | box-shadow: 0 2px 6px rgba(41, 121, 255, 0.06); |
| | | } |
| | | |
| | | .approver-avatar { |
| | | width: 26px; |
| | | height: 26px; |
| | | border-radius: 50%; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | color: #fff; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .approver-name { |
| | | font-size: 13px; |
| | | color: $text; |
| | | max-width: 80px; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .approver-remove { |
| | | flex-shrink: 0; |
| | | width: 22px; |
| | | height: 22px; |
| | | margin-left: 2px; |
| | | border-radius: 50%; |
| | | background: #f2f3f5; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .approver-remove--active { |
| | | background: #fde2e2; |
| | | } |
| | | |
| | | .remove-icon { |
| | | font-size: 16px; |
| | | line-height: 1; |
| | | color: #909399; |
| | | font-weight: 300; |
| | | } |
| | | |
| | | .add-approver { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | padding: 8px 14px; |
| | | border: 1.5px dashed #a8cfff; |
| | | border-radius: 24px; |
| | | background: $primary-light; |
| | | color: $primary; |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .flow-connector { |
| | | display: flex; |
| | | justify-content: center; |
| | | padding: 4px 0; |
| | | } |
| | | |
| | | .flow-connector-line { |
| | | width: 2px; |
| | | height: 14px; |
| | | background: #d0dff0; |
| | | } |
| | | |
| | | .add-node-bar { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 6px; |
| | | margin-top: 8px; |
| | | padding: 11px; |
| | | border: 1px dashed #c6daf5; |
| | | border-radius: $radius-md; |
| | | color: $primary; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .sheet-handle { |
| | | width: 40px; |
| | | height: 4px; |
| | | margin: 10px auto 6px; |
| | | background: #e4e7ed; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .field-editor, |
| | | .user-picker { |
| | | padding: 0 18px calc(18px + env(safe-area-inset-bottom)); |
| | | background: #fff; |
| | | max-height: 85vh; |
| | | } |
| | | |
| | | .editor-title { |
| | | display: block; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | text-align: center; |
| | | margin-bottom: 14px; |
| | | } |
| | | |
| | | .editor-form { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .editor-row { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | |
| | | &--switch { |
| | | flex-direction: row; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 4px 0; |
| | | } |
| | | } |
| | | |
| | | .editor-label { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: $text-secondary; |
| | | |
| | | &.required::before { |
| | | content: "*"; |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | } |
| | | |
| | | .editor-row .input-box, |
| | | .editor-row .textarea-box { |
| | | background: #f7f9fc; |
| | | border-radius: 10px; |
| | | border: 1px solid #eef1f6; |
| | | } |
| | | |
| | | .type-chip-grid { |
| | | display: grid; |
| | | grid-template-columns: 1fr 1fr; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .type-chip { |
| | | text-align: center; |
| | | padding: 10px 6px; |
| | | font-size: 13px; |
| | | color: $text-secondary; |
| | | background: #f7f9fc; |
| | | border: 1px solid #eef1f6; |
| | | border-radius: 8px; |
| | | |
| | | &.active { |
| | | color: $primary; |
| | | background: $primary-light; |
| | | border-color: $primary; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | |
| | | .default-date-row { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 0 12px; |
| | | min-height: 44px; |
| | | background: #f7f9fc; |
| | | border-radius: 10px; |
| | | border: 1px solid #eef1f6; |
| | | } |
| | | |
| | | .editor-footer { |
| | | display: flex; |
| | | gap: 10px; |
| | | margin-top: 16px; |
| | | padding-top: 14px; |
| | | border-top: 1px solid #f5f7fa; |
| | | } |
| | | |
| | | .editor-btn { |
| | | flex: 1; |
| | | text-align: center; |
| | | padding: 11px 0; |
| | | border-radius: 8px; |
| | | font-size: 15px; |
| | | |
| | | &--cancel { |
| | | color: $text-secondary; |
| | | background: #f5f7fa; |
| | | } |
| | | |
| | | &--confirm { |
| | | color: #fff; |
| | | background: $primary; |
| | | } |
| | | } |
| | | |
| | | .picker-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding-bottom: 14px; |
| | | border-bottom: 1px solid #f5f7fa; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .picker-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | } |
| | | |
| | | .picker-cancel { |
| | | font-size: 15px; |
| | | color: $text-muted; |
| | | min-width: 48px; |
| | | } |
| | | |
| | | .picker-confirm { |
| | | font-size: 15px; |
| | | color: $primary; |
| | | font-weight: 600; |
| | | min-width: 48px; |
| | | text-align: right; |
| | | } |
| | | |
| | | .user-scroll { |
| | | max-height: 52vh; |
| | | } |
| | | |
| | | .user-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | padding: 14px 4px; |
| | | border-bottom: 1px solid #f5f7fa; |
| | | border-radius: 10px; |
| | | margin-bottom: 4px; |
| | | transition: background 0.2s; |
| | | |
| | | &.selected { |
| | | background: #f5f9ff; |
| | | } |
| | | } |
| | | |
| | | .user-avatar { |
| | | width: 40px; |
| | | height: 40px; |
| | | border-radius: 12px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | color: #fff; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .user-name { |
| | | flex: 1; |
| | | font-size: 15px; |
| | | color: $text; |
| | | } |
| | | |
| | | .user-check { |
| | | width: 22px; |
| | | height: 22px; |
| | | border-radius: 50%; |
| | | border: 2px solid #dcdfe6; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-shrink: 0; |
| | | |
| | | &.checked { |
| | | background: $primary; |
| | | border-color: $primary; |
| | | } |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 审æ¹ç®¡ç / å®¡æ¹æ¨¡æ¿ |
| | | è·¯ç±ï¼/pages/oa/ApproveManage/approve-template/index |
| | | --> |
| | | <template> |
| | | <view class="approve-template-page sales-account"> |
| | | <PageHeader title="å®¡æ¹æ¨¡æ¿" |
| | | @back="goBack" /> |
| | | <view class="search-section"> |
| | | <view class="search-bar"> |
| | | <view class="search-input"> |
| | | <up-input v-model="queryParams.templateName" |
| | | class="search-text" |
| | | placeholder="请è¾å
¥æ¨¡æ¿åç§°" |
| | | clearable |
| | | @confirm="handleSearch" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="handleSearch"> |
| | | <up-icon name="search" |
| | | size="24" |
| | | color="#999" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <scroll-view class="list-scroll" |
| | | scroll-y |
| | | :show-scrollbar="false" |
| | | @scrolltolower="loadMore"> |
| | | <view v-if="list.length" |
| | | class="ledger-list"> |
| | | <view v-for="item in list" |
| | | :key="item.id" |
| | | class="ledger-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="file-text" |
| | | size="16" |
| | | color="#ffffff" /> |
| | | </view> |
| | | <text class="item-id">{{ item.templateName || "-" }}</text> |
| | | </view> |
| | | <u-tag :type="enabledTagType(item.enabled)" |
| | | :text="enabledText(item.enabled)" /> |
| | | </view> |
| | | <up-divider /> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">模æ¿ç±»å</text> |
| | | <text class="detail-value">{{ templateTypeText(item.templateType) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">审æ¹èç¹</text> |
| | | <text class="detail-value">{{ nodeCount(item) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">模æ¿è¯´æ</text> |
| | | <text class="detail-value">{{ item.description || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å建人</text> |
| | | <text class="detail-value">{{ item.createdUserName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">å建æ¶é´</text> |
| | | <text class="detail-value">{{ item.createTime || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="action-buttons"> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | @click.stop="goDetail(item)"> |
| | | 详æ
|
| | | </up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="primary" |
| | | @click.stop="goEdit(item)"> |
| | | ç¼è¾ |
| | | </up-button> |
| | | <up-button class="action-btn" |
| | | size="small" |
| | | type="error" |
| | | plain |
| | | @click.stop="handleDelete(item)"> |
| | | å é¤ |
| | | </up-button> |
| | | </view> |
| | | </view> |
| | | <up-loadmore :status="pageStatus" /> |
| | | </view> |
| | | <view v-else |
| | | class="empty-wrap"> |
| | | <up-empty mode="list" |
| | | text="ææ å®¡æ¹æ¨¡æ¿æ°æ®" /> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <view class="fab-button" |
| | | @click="goAdd"> |
| | | <up-icon name="plus" |
| | | size="28" |
| | | color="#ffffff" /> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive, ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { |
| | | deleteApprovalTemplate, |
| | | listApprovalTemplatePage, |
| | | } from "@/api/oa/approvalTemplate.js"; |
| | | |
| | | const EDIT_STORAGE_KEY = "oa_approve_template_edit_row"; |
| | | |
| | | const queryParams = reactive({ |
| | | templateName: "", |
| | | }); |
| | | |
| | | const list = ref([]); |
| | | const pageStatus = ref("loadmore"); |
| | | |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | |
| | | const buildListParams = () => ({ |
| | | page: { |
| | | current: page.current, |
| | | size: page.size, |
| | | }, |
| | | approvalTemplateDto: { |
| | | templateName: queryParams.templateName?.trim() || undefined, |
| | | }, |
| | | }); |
| | | |
| | | const enabledText = enabled => { |
| | | const val = String(enabled ?? ""); |
| | | if (val === "1") return "å¯ç¨"; |
| | | if (val === "0") return "åç¨"; |
| | | return "-"; |
| | | }; |
| | | |
| | | const enabledTagType = enabled => { |
| | | const val = String(enabled ?? ""); |
| | | if (val === "1") return "success"; |
| | | if (val === "0") return "info"; |
| | | return "info"; |
| | | }; |
| | | |
| | | const templateTypeText = type => { |
| | | const val = Number(type); |
| | | if (val === 0) return "ç³»ç»å
ç½®"; |
| | | if (val === 1) return "èªå®ä¹"; |
| | | return "-"; |
| | | }; |
| | | |
| | | const nodeCount = item => { |
| | | const count = item?.nodes?.length; |
| | | return count != null ? `${count} 个` : "-"; |
| | | }; |
| | | |
| | | const getList = () => { |
| | | if (pageStatus.value === "loading" || pageStatus.value === "nomore") return; |
| | | |
| | | pageStatus.value = "loading"; |
| | | listApprovalTemplatePage(buildListParams()) |
| | | .then(res => { |
| | | const pageData = res?.data || {}; |
| | | const records = pageData.records || []; |
| | | const total = pageData.total ?? 0; |
| | | |
| | | if (page.current === 1) { |
| | | list.value = records; |
| | | } else { |
| | | list.value = [...list.value, ...records]; |
| | | } |
| | | |
| | | page.total = total; |
| | | if (list.value.length >= total || records.length < page.size) { |
| | | pageStatus.value = "nomore"; |
| | | } else { |
| | | pageStatus.value = "loadmore"; |
| | | page.current += 1; |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | if (page.current === 1) { |
| | | list.value = []; |
| | | } |
| | | pageStatus.value = "loadmore"; |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | const handleSearch = () => { |
| | | page.current = 1; |
| | | pageStatus.value = "loadmore"; |
| | | list.value = []; |
| | | getList(); |
| | | }; |
| | | |
| | | const loadMore = () => { |
| | | if (pageStatus.value === "loadmore") { |
| | | getList(); |
| | | } |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const goAdd = () => { |
| | | uni.removeStorageSync(EDIT_STORAGE_KEY); |
| | | uni.navigateTo({ |
| | | url: "/pages/oa/ApproveManage/approve-template/edit", |
| | | }); |
| | | }; |
| | | |
| | | const goDetail = item => { |
| | | if (!item?.id) return; |
| | | uni.navigateTo({ |
| | | url: `/pages/oa/ApproveManage/approve-template/detail?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const goEdit = item => { |
| | | if (!item?.id) return; |
| | | uni.setStorageSync(EDIT_STORAGE_KEY, item); |
| | | uni.navigateTo({ |
| | | url: `/pages/oa/ApproveManage/approve-template/edit?id=${item.id}`, |
| | | }); |
| | | }; |
| | | |
| | | const handleDelete = item => { |
| | | if (!item?.id) return; |
| | | const name = item.templateName || "该模æ¿"; |
| | | uni.showModal({ |
| | | title: "å é¤ç¡®è®¤", |
| | | content: `ç¡®å®å é¤ã${name}ãåï¼å é¤åæ æ³æ¢å¤ã`, |
| | | confirmText: "å é¤", |
| | | confirmColor: "#f56c6c", |
| | | success: res => { |
| | | if (!res.confirm) return; |
| | | uni.showLoading({ title: "å é¤ä¸...", mask: true }); |
| | | deleteApprovalTemplate([item.id]) |
| | | .then(() => { |
| | | uni.showToast({ title: "å 餿å", icon: "success" }); |
| | | handleSearch(); |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "å é¤å¤±è´¥", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | handleSearch(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .approve-template-page { |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 100vh; |
| | | } |
| | | |
| | | .list-scroll { |
| | | flex: 1; |
| | | height: 0; |
| | | padding-bottom: calc(80px + env(safe-area-inset-bottom)); |
| | | } |
| | | |
| | | .empty-wrap { |
| | | padding: 48px 20px; |
| | | } |
| | | |
| | | .action-buttons { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 10px; |
| | | margin-top: 12px; |
| | | padding-top: 12px; |
| | | border-top: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .action-btn { |
| | | min-width: 72px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / åå¤ç®¡ç / 请åç³è¯· |
| | | è·¯ç±ï¼/pages/oa/AttendManage/leave-apply/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - åå¤ç®¡ç - 请åç³è¯· */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "AttendManage/leave-apply"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / åå¤ç®¡ç / å çç³è¯· |
| | | è·¯ç±ï¼/pages/oa/AttendManage/overtime-apply/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - åå¤ç®¡ç - å çç³è¯· */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "AttendManage/overtime-apply"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / åå管ç / éè´åå |
| | | è·¯ç±ï¼/pages/oa/ContractManage/purchase-contract/index |
| | | 说æï¼è·³è½¬è³éè´å°è´¦ /pages/procurementManagement/procurementLedger/index |
| | | --> |
| | | <template> |
| | | <view class="redirect-page" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - åå管ç - éè´ååï¼è·³è½¬éè´å°è´¦ï¼ */ |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | |
| | | onLoad(() => { |
| | | uni.redirectTo({ |
| | | url: "/pages/procurementManagement/procurementLedger/index", |
| | | }); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .redirect-page { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / åå管ç / éå®åå |
| | | è·¯ç±ï¼/pages/oa/ContractManage/sale-contract/index |
| | | 说æï¼è·³è½¬è³éå®å°è´¦ /pages/sales/salesAccount/index |
| | | --> |
| | | <template> |
| | | <view class="redirect-page" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - åå管ç - éå®ååï¼è·³è½¬éå®å°è´¦ï¼ */ |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | |
| | | onLoad(() => { |
| | | uni.redirectTo({ |
| | | url: "/pages/sales/salesAccount/index", |
| | | }); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .redirect-page { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / ä¼ä¸æ°é» |
| | | è·¯ç±ï¼/pages/oa/EnterpriseNews/news-manage/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - ä¼ä¸æ°é» */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "EnterpriseNews/news-manage"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 人äºç®¡ç / å²ä½ç®¡ç |
| | | è·¯ç±ï¼/pages/oa/HrManage/post-manage/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - 人äºç®¡ç - å²ä½ç®¡ç */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "HrManage/post-manage"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 人äºç®¡ç / 转æ£ç³è¯· |
| | | è·¯ç±ï¼/pages/oa/HrManage/regular-apply/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - 人äºç®¡ç - 转æ£ç³è¯· */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "HrManage/regular-apply"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 人äºç®¡ç / 离èç³è¯· |
| | | è·¯ç±ï¼/pages/oa/HrManage/resign-apply/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - 人äºç®¡ç - 离èç³è¯· */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "HrManage/resign-apply"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 人äºç®¡ç / åå·¥æ¡£æ¡ |
| | | è·¯ç±ï¼/pages/oa/HrManage/staff-archive/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - 人äºç®¡ç - åå·¥æ¡£æ¡ */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "HrManage/staff-archive"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 人äºç®¡ç / åå·¥åå |
| | | è·¯ç±ï¼/pages/oa/HrManage/staff-contract/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - 人äºç®¡ç - åå·¥åå */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "HrManage/staff-contract"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 人äºç®¡ç / è°å²ç³è¯· |
| | | è·¯ç±ï¼/pages/oa/HrManage/transfer-apply/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - 人äºç®¡ç - è°å²ç³è¯· */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "HrManage/transfer-apply"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / 人äºç®¡ç / å·¥ä½äº¤æ¥ |
| | | è·¯ç±ï¼/pages/oa/HrManage/work-handover/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - 人äºç®¡ç - å·¥ä½äº¤æ¥ */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "HrManage/work-handover"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / å
¬åéç¥ |
| | | è·¯ç±ï¼/pages/oa/NoticeAnnouncement/notice-manage/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - å
¬åéç¥ */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "NoticeAnnouncement/notice-manage"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / æ¥é管ç / è´¹ç¨æ¥é |
| | | è·¯ç±ï¼/pages/oa/ReimburseManage/cost-reimburse/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - æ¥é管ç - è´¹ç¨æ¥é */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "ReimburseManage/cost-reimburse"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | OA / æ¥é管ç / å·®æ
æ¥é |
| | | è·¯ç±ï¼/pages/oa/ReimburseManage/travel-reimburse/index |
| | | --> |
| | | <template> |
| | | <OaListPage v-if="config" |
| | | :page-key="pageKey" |
| | | :page-config="config" /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | /** OA - æ¥é管ç - å·®æ
æ¥é */ |
| | | import OaListPage from "../../_components/OaListPage.vue"; |
| | | import { useOaPage } from "../../_utils/useOaPage.js"; |
| | | |
| | | const pageKey = "ReimburseManage/travel-reimburse"; |
| | | const { config } = useOaPage(pageKey); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="oa-page sales-account"> |
| | | <PageHeader :title="pageConfig.title" |
| | | @back="goBack" /> |
| | | <view class="search-section"> |
| | | <view class="search-bar"> |
| | | <view class="search-input"> |
| | | <up-input v-model="keyword" |
| | | class="search-text" |
| | | :placeholder="`æç´¢${pageConfig.title}`" |
| | | clearable |
| | | @change="handleSearch" /> |
| | | </view> |
| | | <view class="filter-button" |
| | | @click="handleSearch"> |
| | | <up-icon name="search" |
| | | size="24" |
| | | color="#999" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <scroll-view class="list-scroll" |
| | | scroll-y |
| | | :show-scrollbar="false"> |
| | | <view v-if="displayList.length" |
| | | class="ledger-list"> |
| | | <view v-for="item in displayList" |
| | | :key="item.id" |
| | | class="ledger-item" |
| | | @click="openDetail(item)"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="document-icon"> |
| | | <up-icon name="file-text" |
| | | size="16" |
| | | color="#ffffff" /> |
| | | </view> |
| | | <text class="item-id">{{ item.summary || item.applicantName || pageConfig.title }}</text> |
| | | </view> |
| | | <u-tag :type="getStatusMeta(item.status).type" |
| | | :text="getStatusMeta(item.status).text" /> |
| | | </view> |
| | | <up-divider /> |
| | | <view class="item-details"> |
| | | <view v-for="field in pageConfig.fields" |
| | | :key="field.prop" |
| | | class="detail-row"> |
| | | <text class="detail-label">{{ field.label }}</text> |
| | | <text class="detail-value">{{ item[field.prop] || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç³è¯·äºº</text> |
| | | <text class="detail-value">{{ item.applicantName || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">ç³è¯·æ¶é´</text> |
| | | <text class="detail-value">{{ item.createTime || "-" }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="empty-wrap"> |
| | | <up-empty mode="list" |
| | | :text="`ææ ${pageConfig.title}æ°æ®`" /> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <view class="footer-add"> |
| | | <up-button type="primary" |
| | | text="æ°å¢" |
| | | @click="handleAdd" /> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { ensureList, saveList } from "../_utils/oaStorage.js"; |
| | | import { getStatusMeta } from "../_utils/oaPageRegistry.js"; |
| | | import { showToast } from "../_utils/oaUi.js"; |
| | | |
| | | const props = defineProps({ |
| | | pageKey: { |
| | | type: String, |
| | | required: true, |
| | | }, |
| | | pageConfig: { |
| | | type: Object, |
| | | required: true, |
| | | }, |
| | | }); |
| | | |
| | | const keyword = ref(""); |
| | | const list = ref([]); |
| | | |
| | | const displayList = computed(() => { |
| | | const kw = keyword.value.trim(); |
| | | if (!kw) return list.value; |
| | | return list.value.filter(item => { |
| | | const text = [ |
| | | item.summary, |
| | | item.applicantName, |
| | | item.deptName, |
| | | ...props.pageConfig.fields.map(f => item[f.prop]), |
| | | ] |
| | | .filter(Boolean) |
| | | .join(" "); |
| | | return text.includes(kw); |
| | | }); |
| | | }); |
| | | |
| | | const loadData = () => { |
| | | list.value = ensureList( |
| | | props.pageConfig.storageKey, |
| | | props.pageConfig.mockRows || [] |
| | | ); |
| | | }; |
| | | |
| | | const handleSearch = () => { |
| | | /* å
³é®åç± computed è¿æ»¤ */ |
| | | }; |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const openDetail = item => { |
| | | showToast(`æ¥çï¼${item.summary || props.pageConfig.title}`); |
| | | }; |
| | | |
| | | const handleAdd = () => { |
| | | const row = { |
| | | ...(props.pageConfig.mockRows?.[0] || {}), |
| | | id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, |
| | | applicantName: "å½åç¨æ·", |
| | | status: "pending", |
| | | createTime: new Date().toISOString().slice(0, 19).replace("T", " "), |
| | | summary: `æ°å»º${props.pageConfig.title}`, |
| | | }; |
| | | list.value = [row, ...list.value]; |
| | | saveList(props.pageConfig.storageKey, list.value); |
| | | showToast("å·²æ°å¢ï¼æ¬å°ç¤ºä¾ï¼", "success"); |
| | | }; |
| | | |
| | | onShow(() => { |
| | | loadData(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/sales-common.scss"; |
| | | |
| | | .oa-page { |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 100vh; |
| | | } |
| | | |
| | | .list-scroll { |
| | | flex: 1; |
| | | height: 0; |
| | | padding-bottom: 80px; |
| | | } |
| | | |
| | | .empty-wrap { |
| | | padding: 48px 20px; |
| | | } |
| | | |
| | | .footer-add { |
| | | position: fixed; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | padding: 12px 20px calc(12px + env(safe-area-inset-bottom)); |
| | | background: #fff; |
| | | box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06); |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { OA_NAV } from "@/config/oaPaths.js"; |
| | | |
| | | const STATUS_MAP = { |
| | | pending: { text: "å®¡æ ¸ä¸", type: "warning" }, |
| | | approved: { text: "å·²éè¿", type: "success" }, |
| | | rejected: { text: "已驳å", type: "error" }, |
| | | draft: { text: "è稿", type: "info" }, |
| | | published: { text: "å·²åå¸", type: "success" }, |
| | | }; |
| | | |
| | | function baseRow(extra = {}) { |
| | | return { |
| | | id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, |
| | | applicantName: "å¼ ä¸", |
| | | deptName: "ç åé¨", |
| | | status: "pending", |
| | | createTime: "2026-05-18 09:00:00", |
| | | summary: "ç¤ºä¾æ°æ®ï¼å¯å¯¹æ¥å端æ¥å£", |
| | | ...extra, |
| | | }; |
| | | } |
| | | |
| | | /** åå页é¢é
ç½®ï¼titleãstorageKeyãå表å±ç¤ºå段ãåå§ mock */ |
| | | export const OA_PAGE_REGISTRY = { |
| | | "HrManage/staff-archive": { |
| | | title: "å工档æ¡", |
| | | module: "人äºç®¡ç", |
| | | storageKey: "oa_hr_staff_archive_v1", |
| | | path: OA_NAV.staffArchive, |
| | | fields: [ |
| | | { label: "åå·¥ç¼å·", prop: "staffNo" }, |
| | | { label: "å²ä½", prop: "postJob" }, |
| | | { label: "èç³»çµè¯", prop: "phone" }, |
| | | ], |
| | | mockRows: [ |
| | | baseRow({ |
| | | staffNo: "E2026001", |
| | | postJob: "å·¥ç¨å¸", |
| | | phone: "13800000001", |
| | | summary: "ææ Â· å¨è", |
| | | }), |
| | | ], |
| | | }, |
| | | "HrManage/staff-contract": { |
| | | title: "åå·¥åå", |
| | | module: "人äºç®¡ç", |
| | | storageKey: "oa_hr_staff_contract_v1", |
| | | path: OA_NAV.staffContract, |
| | | fields: [ |
| | | { label: "ååç¼å·", prop: "contractNo" }, |
| | | { label: "ååç±»å", prop: "contractType" }, |
| | | { label: "å°ææ¥", prop: "endDate" }, |
| | | ], |
| | | mockRows: [ |
| | | baseRow({ |
| | | contractNo: "HT-2026-001", |
| | | contractType: "å³å¨åå", |
| | | endDate: "2027-12-31", |
| | | }), |
| | | ], |
| | | }, |
| | | "HrManage/regular-apply": { |
| | | title: "转æ£ç³è¯·", |
| | | module: "人äºç®¡ç", |
| | | storageKey: "oa_hr_regular_apply_v1", |
| | | path: OA_NAV.regularApply, |
| | | fields: [ |
| | | { label: "å
¥èæ¥æ", prop: "entryDate" }, |
| | | { label: "è½¬æ£æ¥æ", prop: "regularDate" }, |
| | | ], |
| | | mockRows: [baseRow({ entryDate: "2025-11-01", regularDate: "2026-05-20" })], |
| | | }, |
| | | "HrManage/transfer-apply": { |
| | | title: "è°å²ç³è¯·", |
| | | module: "人äºç®¡ç", |
| | | storageKey: "oa_hr_transfer_apply_v1", |
| | | path: OA_NAV.transferApply, |
| | | fields: [ |
| | | { label: "åå²ä½", prop: "fromPost" }, |
| | | { label: "ç®æ å²ä½", prop: "toPost" }, |
| | | ], |
| | | mockRows: [ |
| | | baseRow({ fromPost: "å¼åå·¥ç¨å¸", toPost: "é«çº§å¼åå·¥ç¨å¸" }), |
| | | ], |
| | | }, |
| | | "HrManage/resign-apply": { |
| | | title: "离èç³è¯·", |
| | | module: "人äºç®¡ç", |
| | | storageKey: "oa_hr_resign_apply_v1", |
| | | path: OA_NAV.resignApply, |
| | | fields: [ |
| | | { label: "é¢è®¡ç¦»èæ¥", prop: "leaveDate" }, |
| | | { label: "离èåå ", prop: "reason" }, |
| | | ], |
| | | mockRows: [baseRow({ leaveDate: "2026-06-30", reason: "个人åå±" })], |
| | | }, |
| | | "HrManage/work-handover": { |
| | | title: "å·¥ä½äº¤æ¥", |
| | | module: "人äºç®¡ç", |
| | | storageKey: "oa_hr_work_handover_v1", |
| | | path: OA_NAV.workHandover, |
| | | fields: [ |
| | | { label: "交æ¥äºº", prop: "handoverTo" }, |
| | | { label: "交æ¥äºé¡¹", prop: "handoverItems" }, |
| | | ], |
| | | mockRows: [ |
| | | baseRow({ handoverTo: "çäº", handoverItems: "é¡¹ç®ææ¡£ã客æ·èµæ" }), |
| | | ], |
| | | }, |
| | | "HrManage/post-manage": { |
| | | title: "å²ä½ç®¡ç", |
| | | module: "人äºç®¡ç", |
| | | storageKey: "oa_hr_post_manage_v1", |
| | | path: OA_NAV.postManage, |
| | | fields: [ |
| | | { label: "å²ä½ç¼ç ", prop: "postCode" }, |
| | | { label: "æå±é¨é¨", prop: "deptName" }, |
| | | ], |
| | | mockRows: [baseRow({ postCode: "DEV-01", summary: "å¼åå·¥ç¨å¸" })], |
| | | }, |
| | | "AttendManage/leave-apply": { |
| | | title: "请åç³è¯·", |
| | | module: "åå¤ç®¡ç", |
| | | storageKey: "oa_attend_leave_apply_v1", |
| | | path: OA_NAV.leaveApply, |
| | | fields: [ |
| | | { label: "请åç±»å", prop: "leaveType" }, |
| | | { label: "å¼å§æ¶é´", prop: "startTime" }, |
| | | { label: "ç»ææ¶é´", prop: "endTime" }, |
| | | ], |
| | | mockRows: [ |
| | | baseRow({ |
| | | leaveType: "å¹´å", |
| | | startTime: "2026-05-20 09:00", |
| | | endTime: "2026-05-21 18:00", |
| | | }), |
| | | ], |
| | | }, |
| | | "AttendManage/overtime-apply": { |
| | | title: "å çç³è¯·", |
| | | module: "åå¤ç®¡ç", |
| | | storageKey: "oa_attend_overtime_apply_v1", |
| | | path: OA_NAV.overtimeApply, |
| | | fields: [ |
| | | { label: "å çæ¥æ", prop: "overtimeDate" }, |
| | | { label: "æ¶é¿(å°æ¶)", prop: "hours" }, |
| | | ], |
| | | mockRows: [ |
| | | baseRow({ overtimeDate: "2026-05-18", hours: "3", summary: "çæ¬ä¸çº¿" }), |
| | | ], |
| | | }, |
| | | "ReimburseManage/travel-reimburse": { |
| | | title: "å·®æ
æ¥é", |
| | | module: "æ¥é管ç", |
| | | storageKey: "oa_reimburse_travel_v1", |
| | | path: OA_NAV.travelReimburse, |
| | | fields: [ |
| | | { label: "åºå·®å°ç¹", prop: "destination" }, |
| | | { label: "éé¢(å
)", prop: "amount" }, |
| | | ], |
| | | mockRows: [ |
| | | baseRow({ destination: "䏿µ·", amount: "2680.50", summary: "å®¢æ·æè®¿å·®æ
" }), |
| | | ], |
| | | }, |
| | | "ReimburseManage/cost-reimburse": { |
| | | title: "è´¹ç¨æ¥é", |
| | | module: "æ¥é管ç", |
| | | storageKey: "oa_reimburse_cost_v1", |
| | | path: OA_NAV.costReimburse, |
| | | fields: [ |
| | | { label: "è´¹ç¨ç§ç®", prop: "category" }, |
| | | { label: "éé¢(å
)", prop: "amount" }, |
| | | ], |
| | | mockRows: [ |
| | | baseRow({ category: "åå
¬ç¨å", amount: "356.00", summary: "éè´æå
·" }), |
| | | ], |
| | | }, |
| | | "ApproveManage/approve-list": { |
| | | title: "审æ¹å表", |
| | | module: "审æ¹ç®¡ç", |
| | | storageKey: "oa_unified_approve_list_v1", |
| | | path: OA_NAV.approveList, |
| | | fields: [ |
| | | { label: "审æ¹ç±»å", prop: "approvalTypeLabel" }, |
| | | { label: "å½åèç¹", prop: "currentNode" }, |
| | | ], |
| | | mockRows: [ |
| | | baseRow({ |
| | | approvalTypeLabel: "请åç³è¯·", |
| | | currentNode: "é¨é¨è´è´£äºº", |
| | | }), |
| | | ], |
| | | }, |
| | | "ApproveManage/approve-template": { |
| | | title: "å®¡æ¹æ¨¡æ¿", |
| | | module: "审æ¹ç®¡ç", |
| | | storageKey: "oa_approve_template_custom_v1", |
| | | path: OA_NAV.approveTemplate, |
| | | fields: [ |
| | | { label: "模æ¿åç§°", prop: "templateName" }, |
| | | { label: "èç¹æ°", prop: "nodeCount" }, |
| | | ], |
| | | mockRows: [ |
| | | baseRow({ |
| | | templateName: "éç¨å®¡æ¹æµ", |
| | | nodeCount: "3", |
| | | status: "approved", |
| | | summary: "ç³»ç»å
置模æ¿", |
| | | }), |
| | | ], |
| | | }, |
| | | "EnterpriseNews/news-manage": { |
| | | title: "ä¼ä¸æ°é»", |
| | | module: "ä¼ä¸æ°é»", |
| | | storageKey: "oa_enterprise_news_v1", |
| | | path: OA_NAV.enterpriseNews, |
| | | fields: [ |
| | | { label: "æ ç®", prop: "category" }, |
| | | { label: "é
读é", prop: "readCount" }, |
| | | ], |
| | | mockRows: [ |
| | | baseRow({ |
| | | category: "å
¬å¸å¨æ", |
| | | readCount: "128", |
| | | status: "published", |
| | | summary: "2026年第ä¸å£åº¦ç»è¥éæ¥", |
| | | }), |
| | | ], |
| | | }, |
| | | "NoticeAnnouncement/notice-manage": { |
| | | title: "å
¬åéç¥", |
| | | module: "å
¬åéç¥", |
| | | storageKey: "oa_notice_announcement_v1", |
| | | path: OA_NAV.noticeAnnouncement, |
| | | fields: [ |
| | | { label: "å
¬åç±»å", prop: "noticeType" }, |
| | | { label: "ä¼å
级", prop: "priority" }, |
| | | ], |
| | | mockRows: [ |
| | | baseRow({ |
| | | noticeType: "ä¼ä¸å
Œ", |
| | | priority: "æ®é", |
| | | status: "published", |
| | | summary: "äºä¸å³å¨èæ¾å宿", |
| | | }), |
| | | ], |
| | | }, |
| | | }; |
| | | |
| | | export function getOaPageConfig(pageKey) { |
| | | return OA_PAGE_REGISTRY[pageKey] || null; |
| | | } |
| | | |
| | | export function getStatusMeta(status) { |
| | | return STATUS_MAP[status] || { text: status || "â", type: "info" }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | export function loadList(storageKey, defaultRows = []) { |
| | | try { |
| | | const raw = uni.getStorageSync(storageKey); |
| | | if (!raw) { |
| | | return defaultRows.map(row => ({ ...row })); |
| | | } |
| | | const parsed = typeof raw === "string" ? JSON.parse(raw) : raw; |
| | | return Array.isArray(parsed) |
| | | ? parsed.map(row => ({ ...row })) |
| | | : defaultRows.map(row => ({ ...row })); |
| | | } catch { |
| | | return defaultRows.map(row => ({ ...row })); |
| | | } |
| | | } |
| | | |
| | | export function saveList(storageKey, rows) { |
| | | uni.setStorageSync(storageKey, JSON.stringify(rows)); |
| | | } |
| | | |
| | | export function ensureList(storageKey, defaultRows) { |
| | | const list = loadList(storageKey, defaultRows); |
| | | if (!uni.getStorageSync(storageKey)) { |
| | | saveList(storageKey, list); |
| | | } |
| | | return list; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | export function showToast(title, icon = "none") { |
| | | uni.showToast({ title, icon }); |
| | | } |
| | | |
| | | export function confirmModal(content, title = "æç¤º") { |
| | | return new Promise(resolve => { |
| | | uni.showModal({ |
| | | title, |
| | | content, |
| | | success: res => resolve(Boolean(res.confirm)), |
| | | }); |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { getOaPageConfig } from "./oaPageRegistry.js"; |
| | | |
| | | export function useOaPage(pageKey) { |
| | | const config = getOaPageConfig(pageKey); |
| | | return { pageKey, config }; |
| | | } |
| | |
| | | <template> |
| | | <view class="content"> |
| | | <!-- OAåå
¬æ¨¡å --> |
| | | <view class="common-module oa-module" |
| | | v-if="hasOaItems"> |
| | | <view class="module-header"> |
| | | <view class="module-title-container"> |
| | | <text class="module-title">OAåå
¬</text> |
| | | </view> |
| | | </view> |
| | | <view class="module-content"> |
| | | <up-grid :border="false" |
| | | col="4"> |
| | | <up-grid-item v-for="(item, index) in oaItems" |
| | | :key="index" |
| | | @click="handleCommonItemClick(item)"> |
| | | <view class="icon-container"> |
| | | <image :src="item.icon" |
| | | class="item-icon"></image> |
| | | </view> |
| | | <text class="item-label">{{item.label}}</text> |
| | | </up-grid-item> |
| | | </up-grid> |
| | | </view> |
| | | </view> |
| | | <!-- åååå
¬æ¨¡å --> |
| | | <view class="common-module collaboration-module" |
| | | v-if="hasCollaborationItems"> |
| | |
| | | import { userLoginFacotryList } from "@/api/login"; |
| | | import { getProductWorkOrderById } from "@/api/productionManagement/productionReporting"; |
| | | import DownloadProgressMask from "@/components/DownloadProgressMask.vue"; |
| | | import { OA_WORKBENCH_ITEMS } from "@/config/oaWorkbench.js"; |
| | | import modal from "@/plugins/modal"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | |
| | |
| | | label: "å®å
¨å¹è®èæ ¸", |
| | | }, |
| | | ]); |
| | | // OAåå
¬åè½æ°æ®ï¼çº¯å端é
ç½®ï¼ä¸åä¸å端æéè¿æ»¤ï¼ |
| | | const oaItems = reactive( |
| | | OA_WORKBENCH_ITEMS.map(item => ({ ...item })) |
| | | ); |
| | | |
| | | // åååå
¬åè½æ°æ® |
| | | const collaborationItems = reactive([ |
| | | { |
| | |
| | | |
| | | // å¤ç常ç¨åè½ç¹å» |
| | | const handleCommonItemClick = item => { |
| | | if (item.path) { |
| | | uni.navigateTo({ url: item.path }); |
| | | return; |
| | | } |
| | | // æ ¹æ®ä¸åçåè½é¡¹è¿è¡è·³è½¬ |
| | | switch (item.label) { |
| | | case "å®¢æ·æ¡£æ¡": |
| | |
| | | const hasAfterSalesServiceItems = computed( |
| | | () => afterSalesServiceItems.length > 0 |
| | | ); |
| | | const hasOaItems = computed(() => oaItems.length > 0); |
| | | const hasCollaborationItems = computed(() => collaborationItems.length > 0); |
| | | const hasSafetyItems = computed(() => safetyItems.length > 0); |
| | | const hasQualityItems = computed(() => qualityItems.length > 0); |
| | |
| | | --module-color: #4caf50; |
| | | } |
| | | |
| | | .oa-module { |
| | | --module-color: #673ab7; |
| | | } |
| | | |
| | | .production-module { |
| | | --module-color: #ff9800; |
| | | } |
| | |
| | | --module-color: #4caf50; |
| | | } |
| | | |
| | | .oa-module { |
| | | --module-color: #673ab7; |
| | | } |
| | | |
| | | .production-module { |
| | | --module-color: #ff9800; |
| | | } |